From fe5f233c003a24f61947be31221e13455cb02a34 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:38:04 +0200 Subject: [PATCH] feat(authorization-obligations): Dominance protocol with an order-based default Task 2.4 (Req 5.1, 5.2). Decorator and router witnesses dominate the handler; a constraint or a non-permissive value dominates the operation it sits on; a guard call before the operation dominates by contract; other statement witnesses need an early exit between them and the operation; along a path record an earlier hop dominates. Guard dominance replaces the implementation behind the same protocol later. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG --- .../semantic/obligations/dominance.py | 75 +++++++++++++++++++ tests/test_obligation_dominance.py | 75 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/openultrasast/semantic/obligations/dominance.py create mode 100644 tests/test_obligation_dominance.py diff --git a/src/openultrasast/semantic/obligations/dominance.py b/src/openultrasast/semantic/obligations/dominance.py new file mode 100644 index 0000000..eac6619 --- /dev/null +++ b/src/openultrasast/semantic/obligations/dominance.py @@ -0,0 +1,75 @@ +"""Does a discharge witness dominate an operation? (authorization-obligations, Req 5.1, 5.2) + +`Dominance` is a protocol so the checker can run before `guard-dominance-regime` exists. `OrderDominance` is the +order-based default: decorator and router witnesses dominate the whole handler; a statement witness dominates when it +precedes the operation and the function exits early between them, or when it is itself a guard call (a guard exits on +failure by contract), or when it is a constraint on the operation itself; along a path record a witness on an earlier hop +dominates. Nesting-aware dominance replaces this implementation behind the same protocol. +""" + +from __future__ import annotations + +import re +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Protocol, runtime_checkable + +from .operations import Discharge, Operation + +_EXIT = re.compile(r"^\s*(return\b|raise\b|abort\(|sys\.exit\(|exit\(|throw\b|res\.status\(|response\.status\()") + + +@runtime_checkable +class Dominance(Protocol): + def dominates(self, witness: Discharge, operation: Operation, path: object | None) -> bool: ... + + +class OrderDominance: + """Order-based dominance over source text and, when present, a path record's hops.""" + + def __init__(self, texts: Mapping[str, str]) -> None: + self._texts = dict(texts) + + def dominates(self, witness: Discharge, operation: Operation, path: object | None) -> bool: + if witness.scope == "hop" or (path is not None and (witness.path, witness.function) != (operation.path, operation.function)): + return self._dominates_along(witness, operation, path) + if (witness.path, witness.function) != (operation.path, operation.function): + return False + if witness.scope in {"decorator", "router"}: + return True + if witness.line is None: + return False + if witness.kind == "identity_constraint" or witness.kind == "non_permissive_value": + return witness.line == operation.line + if witness.line >= operation.line: + return False + if witness.kind == "path_guard": + return True + lines = self._texts.get(witness.path, "").splitlines() + between = lines[witness.line : operation.line - 1] + return any(_EXIT.match(line) for line in between) + + def _dominates_along(self, witness: Discharge, operation: Operation, path: object | None) -> bool: + hops = getattr(path, "hops", None) + if not hops: + return False + witness_index = _hop_index(hops, witness.path, witness.function) + operation_index = _hop_index(hops, operation.path, operation.function) + if witness_index is None or operation_index is None: + return False + return witness_index < operation_index + + +def _hop_index(hops: object, path: str, function: str) -> int | None: + stem = Path(path).stem + if not isinstance(hops, Sequence): + return None + for index, hop in enumerate(hops): + node = str(hop[0]) if isinstance(hop, (tuple, list)) else str(hop) + node_path, _, node_function = node.rpartition("::") + if node_function == function and (not node_path or Path(node_path).stem == stem): + return index + return None + + +__all__ = ["Dominance", "OrderDominance"] diff --git a/tests/test_obligation_dominance.py b/tests/test_obligation_dominance.py new file mode 100644 index 0000000..3992582 --- /dev/null +++ b/tests/test_obligation_dominance.py @@ -0,0 +1,75 @@ +"""authorization-obligations task 2.4: the Dominance protocol and its order-based default (offline).""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class _Path: # stub of reachability's PathRecord: (node, call line) hops ending at the operation's node + hops: tuple[tuple[str, int], ...] + + +def _op(path: str = "app.py", line: int = 9, function: str = "handler", kind: str = "protected_read"): + from openultrasast.semantic.obligations.operations import Operation + + return Operation( + path=path, + line=line, + function=function, + kind=kind, + fact_id="orm-read", + resource="book", + requires=("identity_constraint", "ownership_check"), + ) + + +def _wit( + kind: str, line: int | None, scope: str, function: str = "handler", path: str = "app.py", provenance: str = "authenticated_context" +): + from openultrasast.semantic.obligations.operations import Discharge + + return Discharge(path=path, line=line, function=function, kind=kind, fact_id="f", provenance=provenance, scope=scope) + + +SRC = ( + "def handler(book_id):\n user = current_user()\n if user is None:\n return None\n" + " book = Book.query.filter_by(id=book_id).first()\n check_owner(user, book)\n return book\n" +) + + +def test_order_dominance_decorators_and_router_witnesses_dominate_the_handler() -> None: + from openultrasast.semantic.obligations.dominance import Dominance, OrderDominance + + dom = OrderDominance(texts={"app.py": SRC}) + assert isinstance(dom, Dominance) + assert dom.dominates(_wit("ownership_check", None, "decorator"), _op(line=5), None) + assert dom.dominates(_wit("ownership_check", None, "router"), _op(line=5), None) + assert not dom.dominates(_wit("ownership_check", None, "decorator", function="other"), _op(line=5), None) + + +def test_order_dominance_statement_witness_needs_to_precede_with_an_exit_or_be_a_guard_call() -> None: + from openultrasast.semantic.obligations.dominance import OrderDominance + + dom = OrderDominance(texts={"app.py": SRC}) + assert dom.dominates(_wit("ownership_check", 2, "statement"), _op(line=5), None) # before, with `return` between + assert not dom.dominates(_wit("ownership_check", 6, "statement"), _op(line=5), None) # after the operation + no_exit = "def handler(book_id):\n user = current_user()\n book = Book.query.filter_by(id=book_id).first()\n return book\n" + dom2 = OrderDominance(texts={"app.py": no_exit}) + assert not dom2.dominates(_wit("ownership_check", 2, "statement"), _op(line=3), None) # no early exit: not a dominating check + assert dom2.dominates(_wit("path_guard", 2, "statement"), _op(line=3), None) # a guard call itself is an early exit by contract + assert dom2.dominates(_wit("identity_constraint", 3, "statement"), _op(line=3), None) # a constraint sits on the operation itself + + +def test_order_dominance_along_a_path_uses_hop_order() -> None: + from openultrasast.semantic.obligations.dominance import OrderDominance + + dom = OrderDominance(texts={}) + path = _Path(hops=(("api.py::route", 10), ("svc.py::load", 20), ("repo.py::read", 30))) + assert dom.dominates(_wit("path_guard", 4, "hop", function="route", path="api.py"), _op(path="repo.py", line=31, function="read"), path) + assert not dom.dominates( + _wit("path_guard", 4, "hop", function="read", path="repo.py"), _op(path="api.py", line=9, function="route"), path + ) + assert not dom.dominates( + _wit("path_guard", 4, "hop", function="elsewhere", path="x.py"), _op(path="repo.py", line=31, function="read"), path + )