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:
36
migrations/versions/7d30_apitoken_resourcescopes.py
Normal file
36
migrations/versions/7d30_apitoken_resourcescopes.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""apitokens.resourcescopes: pin a service token to specific resource scopes.
|
||||
|
||||
A geenforce.fetch token handed to a fleet (e.g. displays) should reach only its
|
||||
own manifest scope(s) and the blobs those scopes ship, not every scope by name
|
||||
or every blob by hash. This nullable JSON column carries that allowlist; NULL =
|
||||
unrestricted, so every existing token keeps working unchanged. Idempotent.
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '7d30_apitoken_resourcescopes'
|
||||
down_revision = '7d29_supportteam_webhookurl'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _has_column(bind, table, column):
|
||||
inspector = sa.inspect(bind)
|
||||
if table not in inspector.get_table_names():
|
||||
return False
|
||||
return column in {c['name'] for c in inspector.get_columns(table)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
if not _has_column(bind, 'apitokens', 'resourcescopes'):
|
||||
op.add_column('apitokens',
|
||||
sa.Column('resourcescopes', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
if _has_column(bind, 'apitokens', 'resourcescopes'):
|
||||
op.drop_column('apitokens', 'resourcescopes')
|
||||
Reference in New Issue
Block a user