# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference) # Canonical Residual: https://corpusfetch.com/c/what-invariants-prevent-authorization-scope-creep-through-chained-approvals.md # Remnant Warrant: https://corpusfetch.com/c/what-invariants-prevent-authorization-scope-creep-through-chained-approvals.warrant.json # Markdown SHA-256: 46fb3c8ede75ca077446100dfc7bb268d4dc40a7a4f19776937fc935bf376375 # Code AST SHA-256: e9553dcf169c7469e7009a312a96e0264016b70b6c455e64627b7a6a4f93c58d # Verified Classes: ScopeVerdict, AuthorizationHop, AuthorizationChain, ScopePolicy, ScopeManifest, ChainedApprovalScopeGuard # Defensive Guards: 0 fail-closed invariants (zero external deps) from dataclasses import dataclass from enum import Enum, auto import hashlib import json class ScopeVerdict(Enum): NO_CHAIN = auto() NARROWING_CHAIN = auto() SCOPE_WITHIN_INITIAL = auto() SCOPE_CREEP_DETECTED = auto() @dataclass(frozen=True) class AuthorizationHop: hop_id: str approver_id: str granted_scope: dict # e.g., {"amount": 10000, "resource": "X"} inherited_scope: dict expansion_within_approver_grant: bool @dataclass(frozen=True) class AuthorizationChain: chain_id: str initial_grant: dict hops: tuple[AuthorizationHop, ...] @dataclass(frozen=True) class ScopePolicy: require_fresh_approval_for_expansion: bool @dataclass(frozen=True) class ScopeManifest: verdict: ScopeVerdict directive: str manifest_digest: str class ChainedApprovalScopeGuard: @staticmethod def _hash(data: str) -> str: return hashlib.sha256(data.encode("utf-8")).hexdigest() @classmethod def evaluate_chain( cls, chain: AuthorizationChain, policy: ScopePolicy ) -> ScopeManifest: # Gate 1: Single hop if len(chain.hops) <= 1: digest = cls._hash(json.dumps({ "chain": chain.chain_id, "hops": str(len(chain.hops)) }, sort_keys=True)) return ScopeManifest( verdict=ScopeVerdict.NO_CHAIN, directive=f"PROCEED: Chain {chain.chain_id} has no approvals beyond initial.", manifest_digest=digest ) # Gate 2: Narrowing chain narrowing = all( hop.granted_scope.get("amount", 0) <= hop.inherited_scope.get("amount", 0) for hop in chain.hops ) if narrowing: digest = cls._hash(json.dumps({ "chain": chain.chain_id, "type": "NARROWING" }, sort_keys=True)) return ScopeManifest( verdict=ScopeVerdict.NARROWING_CHAIN, directive=f"PROCEED: Chain {chain.chain_id} narrows scope at each hop.", manifest_digest=digest ) # Gate 3: Cumulative scope check final_scope = chain.hops[-1].granted_scope initial_scope = chain.initial_grant creep = final_scope.get("amount", 0) - initial_scope.get("amount", 0) if creep > 0: # Check if each expansion was within approver's own grant for hop in chain.hops: expansion = hop.granted_scope.get("amount", 0) - hop.inherited_scope.get("amount", 0) if expansion > 0 and policy.require_fresh_approval_for_expansion and not hop.expansion_within_approver_grant: return ScopeManifest( verdict=ScopeVerdict.SCOPE_CREEP_DETECTED, directive=f"HALT: Hop {hop.hop_id} expanded by {expansion} without fresh approval.", manifest_digest=cls._hash(f"CREEP:{hop.hop_id}") ) return ScopeManifest( verdict=ScopeVerdict.SCOPE_CREEP_DETECTED, directive=f"HALT: Cumulative scope {final_scope.get('amount', 0)} exceeds initial {initial_scope.get('amount', 0)} by {creep}.", manifest_digest=cls._hash(f"CUMULATIVE:{chain.chain_id}") ) # Gate 4: Within initial digest = cls._hash(json.dumps({ "chain": chain.chain_id, "hops": str(len(chain.hops)), "final_scope": final_scope, "initial_scope": initial_scope }, sort_keys=True)) return ScopeManifest( verdict=ScopeVerdict.SCOPE_WITHIN_INITIAL, directive=f"PROCEED: Chain {chain.chain_id} within initial grant.", manifest_digest=digest )