feat(access-control-prover): 10.3 authority matching through promoted implications

This commit is contained in:
Marius Ciepluch
2026-10-29 19:11:30 +01:00
parent cf60e02804
commit 7eb491dc82
3 changed files with 164 additions and 13 deletions
+1 -1
View File
@@ -179,7 +179,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
- _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 11.1_ - _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Witness Matcher_ - _Boundary: Witness Matcher_
- _Depends: 7.2, 10.1_ - _Depends: 7.2, 10.1_
- [ ] 10.3 Match authority and promoted implications - [x] 10.3 Match authority and promoted implications
- Apply only promoted authority implications and preserve their evidence. - Apply only promoted authority implications and preserve their evidence.
- Return NO for insufficient authority and UNKNOWN when compatibility cannot be decided. - Return NO for insufficient authority and UNKNOWN when compatibility cannot be decided.
- Done when stronger, weaker, unrelated and unknown authorities produce the expected relations. - Done when stronger, weaker, unrelated and unknown authorities produce the expected relations.
@@ -1,17 +1,22 @@
"""Witness kind, scope and resource matching (task 10.2, Reqs 2.1, 2.2, 2.5-2.7, 11.1). """Witness kind, scope, resource and authority matching (tasks 10.2-10.3, Reqs 2.1, 2.2, 2.5-2.8, 11.1).
A witness governs an obligation only when every required dimension is MUST. This module A witness governs an obligation only when every required dimension is MUST. This module
matches the kind, scope and resource dimensions; authority matching (task 10.3) and matches the kind, scope, resource and authority dimensions; dominance (task 10.1) joins
dominance (task 10.1) join through the same algebra. Definite mismatches are NO, unresolved through the same algebra. Definite mismatches are NO, unresolved compatibility is UNKNOWN
compatibility is UNKNOWN (never a broad match, never a definite rejection), per Req 2.7. (never a broad match, never a definite rejection), per Req 2.7. Authority implications come
only from promoted knowledge and their fact identifiers are preserved as evidence (Req 2.8).
""" """
from .abstract_domain import ( from .abstract_domain import (
AuthorityImplication,
AuthorityRef,
EvidenceRelation, EvidenceRelation,
ResourceRef, ResourceRef,
WitnessMatch, WitnessMatch,
authority_satisfies,
conjoin_dimensions, conjoin_dimensions,
parse_resource, parse_resource,
supporting_implications,
) )
from .facts import OperationFact from .facts import OperationFact
from .operations import Discharge, Operation from .operations import Discharge, Operation
@@ -19,17 +24,25 @@ from .operations import Discharge, Operation
_WHOLE_HANDLER_SCOPES = frozenset({"decorator", "router"}) _WHOLE_HANDLER_SCOPES = frozenset({"decorator", "router"})
def match_witness(witness: Discharge, operation: Operation, fact: OperationFact | None = None) -> WitnessMatch: def match_witness(
"""Match one witness against one operation on kind, scope and resource.""" witness: Discharge,
operation: Operation,
fact: OperationFact | None = None,
*,
required_authority: AuthorityRef | None = None,
implications: tuple[AuthorityImplication, ...] = (),
) -> WitnessMatch:
"""Match one witness against one operation on kind, scope, resource and authority."""
if witness.kind not in operation.requires: if witness.kind not in operation.requires:
return _match(EvidenceRelation.NO, EvidenceRelation.NO, witness, operation) return _match(EvidenceRelation.NO, EvidenceRelation.NO, EvidenceRelation.UNKNOWN, witness, operation, ())
if _out_of_scope(witness, operation): if _out_of_scope(witness, operation):
return _match(EvidenceRelation.NO, EvidenceRelation.NO, witness, operation) return _match(EvidenceRelation.NO, EvidenceRelation.NO, EvidenceRelation.UNKNOWN, witness, operation, ())
resource_relation = _resource_relation(witness, operation, fact) resource_relation = _resource_relation(witness, operation, fact)
authority_relation, implication_ids = _authority_relation(witness, required_authority, implications)
relation = conjoin_dimensions( relation = conjoin_dimensions(
(EvidenceRelation.MUST, resource_relation), (EvidenceRelation.MUST, resource_relation, authority_relation),
) )
return _match(relation, resource_relation, witness, operation) return _match(relation, resource_relation, authority_relation, witness, operation, implication_ids)
def _out_of_scope(witness: Discharge, operation: Operation) -> bool: def _out_of_scope(witness: Discharge, operation: Operation) -> bool:
@@ -75,20 +88,41 @@ def _alias_relation(witness_resource: ResourceRef, operation_resource: ResourceR
return EvidenceRelation.UNKNOWN return EvidenceRelation.UNKNOWN
def _authority_relation(
witness: Discharge,
required_authority: AuthorityRef | None,
implications: tuple[AuthorityImplication, ...],
) -> tuple[EvidenceRelation, tuple[str, ...]]:
"""Decide the authority dimension (Req 2.5-2.8); preserve implication fact ids."""
if required_authority is None:
return EvidenceRelation.MUST, ()
held_authority = witness.authority
if held_authority is None or held_authority.is_unknown or required_authority.is_unknown:
return EvidenceRelation.UNKNOWN, ()
relation = authority_satisfies(held_authority, required_authority, implications)
used_ids = tuple(
f"implication:{implication.id}" for implication in supporting_implications(held_authority, required_authority, implications)
)
return relation, used_ids
def _match( def _match(
relation: EvidenceRelation, relation: EvidenceRelation,
resource_relation: EvidenceRelation, resource_relation: EvidenceRelation,
authority_relation: EvidenceRelation,
witness: Discharge, witness: Discharge,
operation: Operation, operation: Operation,
implication_ids: tuple[str, ...],
) -> WitnessMatch: ) -> WitnessMatch:
evidence = ( evidence = (
f"witness:{witness.fact_id}", f"witness:{witness.fact_id}",
f"operation:{operation.fact_id}", f"operation:{operation.fact_id}",
f"resource:{operation.resource or 'unknown'}", f"resource:{operation.resource or 'unknown'}",
*implication_ids,
) )
return WitnessMatch( return WitnessMatch(
relation=relation, relation=relation,
resource_relation=resource_relation, resource_relation=resource_relation,
authority_relation=EvidenceRelation.UNKNOWN, # task 10.3 owns authority matching authority_relation=authority_relation,
evidence=evidence, evidence=evidence,
) )
+118 -1
View File
@@ -2,6 +2,7 @@
from openultrasast.semantic.obligations.abstract_domain import ( from openultrasast.semantic.obligations.abstract_domain import (
UNKNOWN_RESOURCE, UNKNOWN_RESOURCE,
AuthorityRef,
EvidenceRelation, EvidenceRelation,
ResourceRef, ResourceRef,
parse_resource, parse_resource,
@@ -20,6 +21,7 @@ def _witness(
scope: str = "statement", scope: str = "statement",
function: str = "handle", function: str = "handle",
line: int | None = 3, line: int | None = 3,
authority: AuthorityRef | None = None,
) -> Discharge: ) -> Discharge:
return Discharge( return Discharge(
path=PATH, path=PATH,
@@ -30,6 +32,7 @@ def _witness(
provenance="call", provenance="call",
scope=scope, scope=scope,
resource=resource, resource=resource,
authority=authority,
) )
@@ -62,7 +65,7 @@ def test_same_resource_exact_match_is_must() -> None:
match = match_witness(_witness(resource=parse_resource("book")), _operation(resource="book"), _fact()) match = match_witness(_witness(resource=parse_resource("book")), _operation(resource="book"), _fact())
assert match.resource_relation is EvidenceRelation.MUST assert match.resource_relation is EvidenceRelation.MUST
assert match.relation is EvidenceRelation.MUST assert match.relation is EvidenceRelation.MUST
assert match.authority_relation is EvidenceRelation.UNKNOWN assert match.authority_relation is EvidenceRelation.MUST # no authority obligation: vacuous MUST (10.3)
def test_definite_resource_mismatch_is_no() -> None: def test_definite_resource_mismatch_is_no() -> None:
@@ -132,3 +135,117 @@ def test_matching_is_deterministic() -> None:
witness = _witness(resource=parse_resource("book")) witness = _witness(resource=parse_resource("book"))
operation = _operation(resource="book") operation = _operation(resource="book")
assert match_witness(witness, operation, _fact()) == match_witness(witness, operation, _fact()) assert match_witness(witness, operation, _fact()) == match_witness(witness, operation, _fact())
# --- Task 10.3: authority matching through promoted implications (Reqs 2.2, 2.5, 2.7, 2.8, 11.1) ---
def _authority_witness(authority: AuthorityRef | None) -> Discharge:
return _witness(resource=ResourceRef(kind="exact", values=("book",)), authority=authority)
def test_stronger_authority_via_promoted_implication_is_must_and_keeps_fact_id() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityImplication, AuthorityRef
witness = _authority_witness(AuthorityRef(values=("admin",)))
required = AuthorityRef(values=("supervisor",))
implications = (AuthorityImplication(id="php:admin-implies-supervisor", authority="admin", implies="supervisor"),)
match = match_witness(
witness,
_operation(),
required_authority=required,
implications=implications,
)
assert match.authority_relation is EvidenceRelation.MUST
assert "implication:php:admin-implies-supervisor" in match.evidence
def test_identity_authority_is_must() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(AuthorityRef(values=("admin",))),
_operation(),
required_authority=AuthorityRef(values=("admin",)),
)
assert match.authority_relation is EvidenceRelation.MUST
def test_weaker_authority_is_no() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(AuthorityRef(values=("user",))),
_operation(),
required_authority=AuthorityRef(values=("admin",)),
)
assert match.authority_relation is EvidenceRelation.NO
def test_unrelated_authority_is_no() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(AuthorityRef(values=("auditor",))),
_operation(),
required_authority=AuthorityRef(values=("admin",)),
)
assert match.authority_relation is EvidenceRelation.NO
def test_unknown_witness_authority_is_unknown() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(None),
_operation(),
required_authority=AuthorityRef(values=("admin",)),
)
assert match.authority_relation is EvidenceRelation.UNKNOWN
def test_unknown_required_authority_is_unknown() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(AuthorityRef(values=("admin",))),
_operation(),
required_authority=AuthorityRef(values=(), is_unknown=True),
)
assert match.authority_relation is EvidenceRelation.UNKNOWN
def test_multi_value_requirement_is_may_never_must() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(AuthorityRef(values=("admin",))),
_operation(),
required_authority=AuthorityRef(values=("admin", "guest")),
)
assert match.authority_relation is EvidenceRelation.MAY
def test_no_authority_requirement_is_vacuous_must() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
match = match_witness(
_authority_witness(AuthorityRef(values=("admin",))),
_operation(),
required_authority=None,
)
assert match.authority_relation is EvidenceRelation.MUST
def test_overall_relation_conjoins_authority() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
witness = _authority_witness(AuthorityRef(values=("user",)))
match = match_witness(
witness,
_operation(),
required_authority=AuthorityRef(values=("admin",)),
)
assert match.resource_relation is EvidenceRelation.MUST
assert match.authority_relation is EvidenceRelation.NO
assert match.relation is EvidenceRelation.NO