geenforce: IP allowlist for client endpoints + admin Settings tab

Fleet PCs on a trusted (vaulted) network can now reach the GE-Enforce client
endpoints (manifest, payload, report) without a per-PC token: the auth path
accepts a valid geenforce.fetch/report token OR a source IP in the configured
allowlist (setting geenforce_allowed_cidrs). Fail-closed; an empty allowlist
means the token stays the only path, so existing deployments are unchanged.

Rationale: the client token lives in HKLM on every kiosk, so it does not
defend against a compromised kiosk anyway - network-perimeter trust is the
same practical strength with far less provisioning + no token-rotation churn
on a DB wipe. Documented in-UI that this is perimeter trust, not per-device
identity.

- _ip_allowlisted() (ipaddress, X-Forwarded-For-aware via _client_ip)
- /geenforce/config GET/PUT extended with allowedcidrs, server-validated +
  normalized (bad CIDR -> 400)
- new GE-Enforce > Settings tab (GeEnforceSettings.vue) to edit the allowlist
  in admin, no SQL
- 3 regression tests (allow by IP, reject outside list, empty = token required)
This commit is contained in:
cproudlock
2026-07-27 14:06:40 -04:00
parent 19876a5640
commit 0860aa85c5
5 changed files with 232 additions and 11 deletions

View File

@@ -115,6 +115,38 @@ def test_unauthenticated_rejected(client, db, app):
assert resp.status_code == 401
def _set_allowlist(app, value):
from shopdb.api import Setting
with app.app_context():
Setting.set('geenforce_allowed_cidrs', value, 'string', 'geenforce')
service.db.session.commit()
def test_ip_allowlist_allows_without_token(client, db, app):
# An allowlisted caller reaches the manifest with NO token (vault trust).
_seed_and_publish(app)
_set_allowlist(app, '127.0.0.0/8, 10.134.48.0/23') # test client is 127.0.0.1
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
assert resp.status_code == 200, resp.data
assert b'Alpha' in resp.data
def test_ip_not_in_allowlist_still_rejected(client, db, app):
# A caller outside the allowlist and with no token is refused (fail-closed).
_seed_and_publish(app)
_set_allowlist(app, '10.0.0.0/8') # test client 127.0.0.1 is NOT in 10/8
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
assert resp.status_code == 401
def test_empty_allowlist_keeps_token_required(client, db, app):
# Empty/unset allowlist = disabled; token stays the only path (back-compat).
_seed_and_publish(app)
_set_allowlist(app, '')
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm')
assert resp.status_code == 401
def test_wrong_scope_rejected(client, db, app, auth_headers):
_seed_and_publish(app)
resp = client.post('/api/apitokens',