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:
@@ -8,6 +8,7 @@ Kept out of the CLI and routes so both share one implementation:
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
@@ -288,6 +289,34 @@ def blob_path(sha256):
|
||||
return os.path.join(_payload_dir(), sha256)
|
||||
|
||||
|
||||
def blob_referenced_by_scopes(sha256, scopenames):
|
||||
"""True when any of these scopes' CURRENT published manifest references
|
||||
sha256 as an entry payload.
|
||||
|
||||
Backs the resource-bound token check on GET /payload: a token pinned to its
|
||||
own scope(s) may only pull blobs those scopes actually ship, not any blob by
|
||||
hash. Empty scopenames -> False (a bound-but-empty token reaches nothing).
|
||||
"""
|
||||
if not scopenames:
|
||||
return False
|
||||
rows = db.session.query(ManifestPublishedVersion).join(
|
||||
ManifestScope,
|
||||
ManifestPublishedVersion.scopeid == ManifestScope.scopeid,
|
||||
).filter(
|
||||
ManifestScope.scopename.in_(list(scopenames)),
|
||||
ManifestPublishedVersion.iscurrent == True, # noqa: E712
|
||||
).all()
|
||||
for row in rows:
|
||||
try:
|
||||
doc = json.loads(row.manifestjson)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
for entry in doc.get('Applications', []) or []:
|
||||
if entry.get('PayloadSha256') == sha256:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def store_blob(rawbytes, filename, contenttype=None):
|
||||
"""Store bytes in the content-addressed payload store; return the sha256.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user