"""Minimal lab: the seed of the system under test. Deliberately small. Users own resources, share them with read or write permission, and revoke that sharing. Every state change is audited. Two access paths exist, and the distinction is the whole point: * the **enforcement path** (`request`) is what an actor uses. It checks authorization and can therefore be *wrong* — that is where a seeded authorization defect lives. * the **observation channel** (`ObservationChannel`) reads stored state directly, without authorization. It is the independent channel required by decision D-07. An oracle that consulted only stored state would verify the framework's own reimplementation of the rules rather than the system's enforcement of them. An oracle that consulted only the enforcement path would have no way to notice that enforcement and record disagree. test-driver observes both, and treats disagreement between them as meaningful in its own right — that disagreement is the precise signature of the M05 authorization defect. TD-WP-0002-T05 grows this into the full lab with an HTTP API, a browser UI and the labelled mutation catalogue. It is kept in-process here so that T04 can prove the kernel without dragging in a web stack. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Literal Permission = Literal["READ", "WRITE"] class Denied(Exception): """The enforcement path refused the request.""" @dataclass(slots=True) class AuditRecord: sequence: int event: str actor_id: str resource_id: str | None = None subject_id: str | None = None permission: str | None = None @dataclass(slots=True) class MinimalLab: """In-process resource-sharing service.""" version: str = "lab-0.1.0-baseline" users: dict[str, str] = field(default_factory=dict) # user_id -> token _tokens: dict[str, str] = field(default_factory=dict) # token -> user_id resources: dict[str, dict[str, Any]] = field(default_factory=dict) grants: dict[tuple[str, str], Permission] = field(default_factory=dict) audit: list[AuditRecord] = field(default_factory=list) _seq: int = 0 # -- setup ----------------------------------------------------------- def add_user(self, user_id: str) -> str: token = f"tok-{user_id}" self.users[user_id] = token self._tokens[token] = user_id return token def _audit(self, event: str, actor_id: str, **kw: Any) -> None: self._seq += 1 self.audit.append(AuditRecord(self._seq, event, actor_id, **kw)) def _whoami(self, token: str) -> str: if token not in self._tokens: raise Denied("unknown token") return self._tokens[token] # -- enforcement path (what actors use) ------------------------------ def request(self, token: str, op: str, **args: Any) -> Any: """The single entry point actors go through. Authorization is enforced here.""" user_id = self._whoami(token) handler = getattr(self, f"_op_{op}", None) if handler is None: raise Denied(f"unknown operation {op!r}") return handler(user_id, **args) def _op_create_resource(self, user_id: str, resource_id: str, content: str) -> dict: self.resources[resource_id] = {"owner": user_id, "content": content} self._audit("create", user_id, resource_id=resource_id) return {"resource_id": resource_id} def _op_read_resource(self, user_id: str, resource_id: str) -> dict: resource = self.resources.get(resource_id) if resource is None: raise Denied("no such resource") if not self._may_read(user_id, resource_id): raise Denied("not authorized to read") return {"resource_id": resource_id, "content": resource["content"]} def _op_grant( self, user_id: str, resource_id: str, subject_id: str, permission: Permission ) -> dict: resource = self.resources.get(resource_id) if resource is None or resource["owner"] != user_id: raise Denied("only the owner may grant") self.grants[(resource_id, subject_id)] = permission self._audit( "grant", user_id, resource_id=resource_id, subject_id=subject_id, permission=permission, ) return {"granted": permission} def _op_revoke(self, user_id: str, resource_id: str, subject_id: str) -> dict: resource = self.resources.get(resource_id) if resource is None or resource["owner"] != user_id: raise Denied("only the owner may revoke") self.grants.pop((resource_id, subject_id), None) self._audit("revoke", user_id, resource_id=resource_id, subject_id=subject_id) return {"revoked": True} def _may_read(self, user_id: str, resource_id: str) -> bool: """The authorization rule as the system actually enforces it.""" resource = self.resources.get(resource_id) if resource is None: return False if resource["owner"] == user_id: return True return (resource_id, user_id) in self.grants class ObservationChannel: """Independent read access to lab state — decision D-07. Bypasses authorization deliberately. This is the channel test-driver requires of any system under test, and the main integration burden the framework imposes on an adopter. """ def __init__(self, lab: MinimalLab) -> None: self._lab = lab @property def version(self) -> str: return self._lab.version def state_permission(self, user_id: str, resource_id: str) -> str | None: """What the stored record says, independent of any enforcement decision.""" resource = self._lab.resources.get(resource_id) if resource is None: return None if resource["owner"] == user_id: return "OWNER" return self._lab.grants.get((resource_id, user_id)) def probe_read(self, user_id: str, resource_id: str) -> bool: """Exercise the enforcement path out-of-band and report what it did. This uses the subject's own credentials, which can look like a violation of actor isolation but is not: independence means the *actor's report* is never the evidence. The observer issues its own request and records the raw outcome. No actor is ever asked whether it succeeded. """ token = self._lab.users.get(user_id) if token is None: return False try: self._lab.request(token, "read_resource", resource_id=resource_id) except Denied: return False return True def audit_events(self, resource_id: str) -> list[dict[str, Any]]: return [ { "sequence": r.sequence, "event": r.event, "actor_id": r.actor_id, "subject_id": r.subject_id, "permission": r.permission, } for r in self._lab.audit if r.resource_id == resource_id ] def build_baseline() -> tuple[MinimalLab, dict[str, str]]: """Known initial state, so that runs replay from the same starting point.""" lab = MinimalLab() tokens = {user: lab.add_user(user) for user in ("alice", "bob", "carol")} return lab, tokens