geenforce: resource-scope binding for fetch tokens (0.15.0)

A geenforce.fetch token can now be pinned to specific manifest scopes so a
fleet-wide key (a display's, delivered by DSC or baked into the image) is not a
skeleton key for the whole content store. NULL binding = unrestricted, so every
existing service token keeps working.

Core:
- ApiToken.resourcescopes column + resourcescopelist property (migration
  7d30_apitoken_resourcescopes; NULL = unrestricted).
- apitokens API create/update accept + persist an optional resourcescopes list
  (a resource-name allowlist; not permission-catalog names).
- New contract helper authorized_service_token(scope): same check as
  service_token_authorized but returns the ApiToken so a plugin can read its
  binding. Contract 0.14.0 -> 0.15.0; also export SupportTeam.

GE-Enforce enforcement:
- get_manifest: a bound token requesting a scope outside its allowlist -> 403.
- get_payload: a bound token may only pull a blob its own scope(s) reference
  (service.blob_referenced_by_scopes); anything else -> 404 (no hash probing).
- Decorator stashes the authorized token on g for the route to read.

Also fixes a pre-existing contract-surface violation: the printers/printedparts
alert helpers imported shopdb.core.models / shopdb.extensions directly; now
via shopdb.api (SupportTeam newly exported). Docs: GE-ENFORCE-DISPLAY.md
provisioning note, PLUGIN-HOOKS.md, CLAUDE.md.

9 new resource-binding tests; full suite 1131 passing.
This commit is contained in:
cproudlock
2026-07-23 09:02:42 -04:00
parent d0bf37ced7
commit 75386d2f51
15 changed files with 344 additions and 25 deletions

View File

@@ -9,7 +9,7 @@ The contract is locked in [ADR-001](../docs/adr/ADR-001-asset-as-platform-contra
The framework declares its contract version in `shopdb/__init__.py`:
```python
__contract_version__ = '0.14.0'
__contract_version__ = '0.15.0'
```
Each plugin's `manifest.json` declares the range of contract versions it supports:
@@ -497,6 +497,12 @@ What `shopdb.api` exposes:
(`service_token_authorized(scope)` returns True when the request carries a
managed service token scoped for `scope` whose owner holds that permission -
for unattended plugin endpoints like the GE-Enforce fetch API)
- `authorized_service_token(scope)` (0.15.0) - same check as
`service_token_authorized` but returns the `ApiToken` itself (or None), so a
plugin can honor the token's optional resource binding
(`token.resourcescopelist`: an allowlist of resource names the token may
reach, NULL = unrestricted). GE-Enforce uses it to pin a display's fetch
token to its own manifest scope + that scope's blobs.
- Helpers: `audit_log`, `resolve_asset_position`, `resolve_dualpath_pairs`,
`dualpath_single_machine_enabled`
- Import mode: `apply_import_timestamps`, `import_mode_active`,
@@ -506,6 +512,8 @@ What `shopdb.api` exposes:
`cmmc_usb_connection`
- `User` / `Role` (0.13.0) - the account and role models, e.g. resolving
alert recipients' emails from selected user ids or role membership
- `SupportTeam` (0.15.0) - the support-team model (carries a `webhookurl`), so
an alerting plugin can route a notification to a chosen team's Teams webhook
- Mailer (0.12.0): `send_email(to, subject, html, text=None)` and
`send_alert(subject, html, text=None)` - settings-first, no-op safe when
email is unconfigured; send_alert targets the site's alert_recipients