feat(access-control-prover): 7.3 authority abstract values and promoted implication facts

This commit is contained in:
Marius Ciepluch
2026-10-29 15:06:53 +01:00
parent 351583a04f
commit 2f97fb2693
4 changed files with 660 additions and 5 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
- _Requirements: 2.1, 2.5, 2.6, 2.7, 11.1_ - _Requirements: 2.1, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Resource Domain_ - _Boundary: Resource Domain_
- _Depends: 7.1_ - _Depends: 7.1_
- [ ] 7.3 Add authority abstract values and promoted implication facts - [x] 7.3 Add authority abstract values and promoted implication facts
- Represent exact, finite and unknown authorities separately from resources. - Represent exact, finite and unknown authorities separately from resources.
- Load authority implications from promoted facts and retain the supporting fact identifier. - Load authority implications from promoted facts and retain the supporting fact identifier.
- Done when sufficient, insufficient and unknown authority fixtures produce the specified relations. - Done when sufficient, insufficient and unknown authority fixtures produce the specified relations.
@@ -1,6 +1,6 @@
"""Evidence relations, the completeness value and resource abstract values (access-control-prover). """Evidence relations, the completeness value, resource and authority abstract values (access-control-prover).
Covers Req 2.1, 2.5, 2.6, 2.7, 11.1, 11.6, 11.7. Covers Req 2.1, 2.2, 2.5, 2.6, 2.7, 2.8, 11.1, 11.6, 11.7.
``EvidenceRelation`` is the abstract evidence domain: definite presence (``MUST``), definite absence ``EvidenceRelation`` is the abstract evidence domain: definite presence (``MUST``), definite absence
(``NO``), possibility (``MAY``) and unknown (``UNKNOWN``). Three separate operations keep one table (``NO``), possibility (``MAY``) and unknown (``UNKNOWN``). Three separate operations keep one table
@@ -9,16 +9,24 @@ targets or control paths, ``conjoin_dimensions`` combines one witness's kind, sc
and dominance dimensions, and ``aggregate_witnesses`` asks whether any witness definitely governs one and dominance dimensions, and ``aggregate_witnesses`` asks whether any witness definitely governs one
obligation. ``ResourceRef`` abstracts one resource binding into an exact token, a finite alias set, obligation. ``ResourceRef`` abstracts one resource binding into an exact token, a finite alias set,
request-global scope or an unknown value; ``join_resources`` unions alias sets finitely, lets request-global scope or an unknown value; ``join_resources`` unions alias sets finitely, lets
request-global scope absorb concrete bindings and never lets an unresolvable value resolve. Every request-global scope absorb concrete bindings and never lets an unresolvable value resolve.
``AuthorityRef`` abstracts one authority holding into an exact name, a finite alternative set or an
unknown value; ``authority_satisfies`` asks whether a held authority satisfies a required one by
folding ``join_alternatives`` over every held-by-required name pair, where a pair is ``MUST`` when a
promoted ``AuthorityImplication`` chain connects them and ``NO`` otherwise, so sufficient authority is
``MUST``, insufficient is ``NO`` and undecidable is ``UNKNOWN`` (Req 2.5, 2.7, 2.8). ``supporting_implications``
returns the implications used with their supporting fact ids, retained for the trace (Req 2.8). Every
operation is deterministic, pure and independent of target names; join never converts operation is deterministic, pure and independent of target names; join never converts
``MAY`` or ``UNKNOWN`` into ``MUST`` or ``NO``, so losing precision cannot manufacture a verdict. ``MAY`` or ``UNKNOWN`` into ``MUST`` or ``NO``, so losing precision cannot manufacture a verdict.
""" """
from __future__ import annotations from __future__ import annotations
from collections import deque
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from functools import reduce
from typing import Literal from typing import Literal
@@ -108,6 +116,163 @@ def join_resources(left: ResourceRef, right: ResourceRef) -> ResourceRef:
return ResourceRef(kind="aliases", values=left.values + right.values) return ResourceRef(kind="aliases", values=left.values + right.values)
@dataclass(frozen=True)
class AuthorityRef:
"""The abstract value of one authority holding (design, "Abstract evidence domain"; Req 2.1, 2.2, 2.5, 2.7, 11.1).
One value is an ``exact`` authority, several values are a ``finite`` alternative set (the authority is
one of them, not all of them) and ``is_unknown`` marks an unresolvable holding that can neither match
nor fail. Values are deduplicated and sorted, so equal holdings are equal however they were
constructed; a misshapen value fails loud rather than degrading silently.
"""
values: tuple[str, ...]
is_unknown: bool = False
def __post_init__(self) -> None:
if not all(isinstance(value, str) and value for value in self.values):
raise ValueError(f"misshapen authority values: {self.values!r}")
if self.is_unknown and self.values:
raise ValueError(f"unknown authority binds no values: {self.values!r}")
if not self.is_unknown and not self.values:
raise ValueError("authority needs at least one value")
object.__setattr__(self, "values", tuple(sorted(set(self.values))))
UNKNOWN_AUTHORITY = AuthorityRef(values=(), is_unknown=True)
def parse_authority(binding: str | Sequence[str] | None) -> AuthorityRef:
"""Parse one authority holding from fact or IR evidence into its abstract value.
A single name is ``exact``, a finite name collection is a finite alternative set, and ``None``
(no holding could be resolved) is ``unknown``; anything misshapen raises instead of quietly degrading.
"""
if binding is None:
return UNKNOWN_AUTHORITY
names = (binding,) if isinstance(binding, str) else tuple(binding)
if not names or not all(isinstance(name, str) and name for name in names):
raise ValueError(f"misshapen authority binding: {binding!r}")
return AuthorityRef(values=names)
def join_authorities(left: AuthorityRef, right: AuthorityRef) -> AuthorityRef:
"""Join authority holdings from alternative paths or call targets.
Identical exact holdings stay exact and finite sets join by finite union, mirroring ``join_resources``;
an unresolvable holding joins to ``unknown`` so losing precision cannot manufacture a match
(Req 2.7, 11.1).
"""
if left.is_unknown or right.is_unknown:
return UNKNOWN_AUTHORITY
if left.values == right.values:
return left
return AuthorityRef(values=left.values + right.values)
@dataclass(frozen=True)
class AuthorityImplication:
"""One promoted implication fact: ``authority`` also covers ``implies`` (Req 2.8).
``id`` is the supporting promoted fact row's identifier, retained so the trace can name the
implication used. The engine bakes in no implication of its own; only these facts connect authorities.
"""
id: str
authority: str
implies: str
def _implication_index(
implications: Iterable[AuthorityImplication],
) -> dict[str, tuple[AuthorityImplication, ...]]:
"""Group implications by the authority they extend, preserving first-occurrence order."""
grouped: dict[str, list[AuthorityImplication]] = {}
for implication in implications:
grouped.setdefault(implication.authority, []).append(implication)
return {authority: tuple(edges) for authority, edges in grouped.items()}
def _implication_path(
source: str,
target: str,
index: Mapping[str, tuple[AuthorityImplication, ...]],
) -> tuple[AuthorityImplication, ...] | None:
"""The implications leading from ``source`` to ``target``, or ``None`` when no promoted chain reaches it.
Breadth-first over the finite edges in fact order, so the returned chain is deterministic and shortest
by edge count; ``()`` means the names are identical and no implication is needed.
"""
if source == target:
return ()
parents: dict[str, tuple[str, AuthorityImplication] | None] = {source: None}
frontier: deque[str] = deque([source])
while frontier:
current = frontier.popleft()
for implication in index.get(current, ()):
reached = implication.implies
if reached in parents:
continue
parents[reached] = (current, implication)
if reached == target:
chain: list[AuthorityImplication] = []
node: str = target
while True:
step = parents[node]
if step is None:
return tuple(reversed(chain))
previous, edge = step
chain.append(edge)
node = previous
frontier.append(reached)
return None
def authority_satisfies(
held: AuthorityRef,
required: AuthorityRef,
implications: Iterable[AuthorityImplication] = (),
) -> EvidenceRelation:
"""Ask whether one held authority value satisfies one required authority value (Req 2.5, 2.7, 2.8, 11.1).
Each held-by-required name pair is definite: ``MUST`` when the names match or a promoted implication
chain connects them, ``NO`` otherwise. The aggregate folds ``join_alternatives`` over every pair, so a
finite holding with one satisfying alternative is ``MAY``; an unresolvable holding or requirement is
``UNKNOWN`` and can never match broadly or fail definitely.
"""
if held.is_unknown or required.is_unknown:
return EvidenceRelation.UNKNOWN
index = _implication_index(implications)
pairs = [
EvidenceRelation.MUST if _implication_path(held_name, required_name, index) is not None else EvidenceRelation.NO
for held_name in held.values
for required_name in required.values
]
return reduce(join_alternatives, pairs)
def supporting_implications(
held: AuthorityRef,
required: AuthorityRef,
implications: Iterable[AuthorityImplication] = (),
) -> tuple[AuthorityImplication, ...]:
"""The implications used on satisfying paths, with supporting fact ids retained for the trace (Req 2.8).
First-occurrence order over the normalized names, deduplicated; empty when nothing matched or the
relation could not be decided.
"""
if held.is_unknown or required.is_unknown:
return ()
index = _implication_index(implications)
used: list[AuthorityImplication] = []
for held_name in held.values:
for required_name in required.values:
chain = _implication_path(held_name, required_name, index)
if chain is not None:
used.extend(chain)
return tuple(dict.fromkeys(used))
_ALTERNATIVE_JOIN: Mapping[tuple[EvidenceRelation, EvidenceRelation], EvidenceRelation] = { _ALTERNATIVE_JOIN: Mapping[tuple[EvidenceRelation, EvidenceRelation], EvidenceRelation] = {
(EvidenceRelation.MUST, EvidenceRelation.MUST): EvidenceRelation.MUST, (EvidenceRelation.MUST, EvidenceRelation.MUST): EvidenceRelation.MUST,
(EvidenceRelation.MUST, EvidenceRelation.NO): EvidenceRelation.MAY, (EvidenceRelation.MUST, EvidenceRelation.NO): EvidenceRelation.MAY,
@@ -14,6 +14,7 @@ from pathlib import Path
from ...source import canonical_language from ...source import canonical_language
from ..facts import _ruleset_state from ..facts import _ruleset_state
from .abstract_domain import AuthorityImplication
OPERATION_KINDS = ("protected_read", "protected_write", "privileged_action", "security_setting") OPERATION_KINDS = ("protected_read", "protected_write", "privileged_action", "security_setting")
DISCHARGER_KINDS = ("path_guard", "identity_constraint", "ownership_check", "non_permissive_value", "validated_input") DISCHARGER_KINDS = ("path_guard", "identity_constraint", "ownership_check", "non_permissive_value", "validated_input")
@@ -38,6 +39,7 @@ _OPERATION_FIELDS = frozenset(
_DISCHARGER_FIELDS = frozenset( _DISCHARGER_FIELDS = frozenset(
{"id", "kind", "calls", "decorators", "identity_sources", "constraint_params", "request_sources", "permissive_values"} {"id", "kind", "calls", "decorators", "identity_sources", "constraint_params", "request_sources", "permissive_values"}
) )
_IMPLICATION_FIELDS = frozenset({"id", "authority", "implies"})
class ObligationFactsError(ValueError): class ObligationFactsError(ValueError):
@@ -79,11 +81,25 @@ class DischargerFact:
permissive_values: tuple[str, ...] = () # literal values that leave a security setting open (non_permissive_value facts) permissive_values: tuple[str, ...] = () # literal values that leave a security setting open (non_permissive_value facts)
@dataclass(frozen=True)
class ImplicationFact(AuthorityImplication):
"""One promoted ``[[implication]]`` row: ``authority`` also covers ``implies`` (Req 2.8).
Extends the abstract domain's ``AuthorityImplication`` with the loader's language scope, so loaded
facts feed ``authority_satisfies`` directly while filtering per language like every other fact row.
"""
language: str = ""
@dataclass(frozen=True) @dataclass(frozen=True)
class ObligationFacts: class ObligationFacts:
version: str version: str
operations: tuple[OperationFact, ...] operations: tuple[OperationFact, ...]
dischargers: tuple[DischargerFact, ...] dischargers: tuple[DischargerFact, ...]
# Requirement 2.8 (access-control-prover): promoted authority implications, each retaining the
# supporting fact row id so the trace can name the implication used. Default-empty; older data loads unchanged.
implications: tuple[ImplicationFact, ...] = ()
def for_language(self, language: str) -> ObligationFacts: def for_language(self, language: str) -> ObligationFacts:
key = canonical_language(language) key = canonical_language(language)
@@ -93,6 +109,7 @@ class ObligationFacts:
version=self.version, version=self.version,
operations=tuple(item for item in self.operations if item.language == key), operations=tuple(item for item in self.operations if item.language == key),
dischargers=tuple(item for item in self.dischargers if item.language == key), dischargers=tuple(item for item in self.dischargers if item.language == key),
implications=tuple(item for item in self.implications if item.language == key),
) )
@@ -102,6 +119,7 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
raise ObligationFactsError(f"obligation facts directory missing: {root}") raise ObligationFactsError(f"obligation facts directory missing: {root}")
operations: list[OperationFact] = [] operations: list[OperationFact] = []
dischargers: list[DischargerFact] = [] dischargers: list[DischargerFact] = []
implications: list[ImplicationFact] = []
versions: list[str] = [] versions: list[str] = []
for path in sorted(root.glob("*.toml")): for path in sorted(root.glob("*.toml")):
try: try:
@@ -112,6 +130,8 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
versions.append(str(payload.get("version", "1"))) versions.append(str(payload.get("version", "1")))
for row in _rows(payload.get("operation"), path): for row in _rows(payload.get("operation"), path):
operations.append(_operation_fact(row, path, language)) operations.append(_operation_fact(row, path, language))
for row in _rows(payload.get("implication"), path):
implications.append(_implication_fact(row, path, language))
for row in _rows(payload.get("discharger"), path): for row in _rows(payload.get("discharger"), path):
_only_fields(row, _DISCHARGER_FIELDS, path) _only_fields(row, _DISCHARGER_FIELDS, path)
kind = str(row.get("kind", "")) kind = str(row.get("kind", ""))
@@ -130,7 +150,12 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
permissive_values=_strings(row.get("permissive_values"), "permissive_values", path), permissive_values=_strings(row.get("permissive_values"), "permissive_values", path),
) )
) )
return ObligationFacts(version=versions[0] if versions else "1", operations=tuple(operations), dischargers=tuple(dischargers)) return ObligationFacts(
version=versions[0] if versions else "1",
operations=tuple(operations),
dischargers=tuple(dischargers),
implications=tuple(implications),
)
def _operation_fact(row: dict[str, object], path: Path, language: str) -> OperationFact: def _operation_fact(row: dict[str, object], path: Path, language: str) -> OperationFact:
@@ -166,6 +191,24 @@ def _operation_fact(row: dict[str, object], path: Path, language: str) -> Operat
) )
def _implication_fact(row: dict[str, object], path: Path, language: str) -> ImplicationFact:
"""Validate one implication row against the closed fields; both names must be non-empty strings."""
_only_fields(row, _IMPLICATION_FIELDS, path)
return ImplicationFact(
id=str(row["id"]),
authority=_implication_name(row, "authority", path),
implies=_implication_name(row, "implies", path),
language=language,
)
def _implication_name(row: dict[str, object], field: str, path: Path) -> str:
value = row.get(field)
if not isinstance(value, str) or not value:
raise ObligationFactsError(f"{path}: implication {row.get('id')!r} needs a non-empty {field}")
return value
def _optional_int(row: dict[str, object], field: str, path: Path, *, positive: bool) -> int | None: def _optional_int(row: dict[str, object], field: str, path: Path, *, positive: bool) -> int | None:
value = row.get(field) value = row.get(field)
if value is None: if value is None:
+447
View File
@@ -0,0 +1,447 @@
"""access-control-prover task 7.3: authority abstract values and promoted implication facts (offline).
Exact, finite and unknown authorities per the design's "Abstract evidence domain" section (Req 2.1, 2.2,
2.5, 2.7, 11.1), the promoted implication facts that carry one authority implying another with the
supporting fact id retained (Req 2.8), and the satisfy query stated in the EvidenceRelation algebra
from task 7.1: sufficient authority is MUST, insufficient is NO, undecidable is UNKNOWN, and a finite set
with one satisfying alternative is MAY.
"""
from __future__ import annotations
import itertools
from pathlib import Path
from typing import TYPE_CHECKING
import pytest
if TYPE_CHECKING:
from openultrasast.semantic.obligations.abstract_domain import AuthorityImplication, AuthorityRef
def _authority_fixtures() -> tuple[AuthorityRef, ...]:
from openultrasast.semantic.obligations.abstract_domain import UNKNOWN_AUTHORITY, parse_authority
return (
parse_authority("admin"),
parse_authority("staff"),
parse_authority("guest"),
parse_authority(["admin", "guest"]),
parse_authority(["staff", "owner"]),
UNKNOWN_AUTHORITY,
)
def _implication_fixtures() -> tuple[AuthorityImplication, ...]:
from openultrasast.semantic.obligations.abstract_domain import AuthorityImplication
# owner -> staff -> authenticated, plus admin -> owner: a chain promotion must be able to walk.
return (
AuthorityImplication(id="admin-covers-owner", authority="admin", implies="owner"),
AuthorityImplication(id="owner-covers-staff", authority="owner", implies="staff"),
AuthorityImplication(id="staff-covers-authenticated", authority="staff", implies="authenticated"),
)
_IMPLICATION_TOML = """version = "1"
language = "python"
[[operation]]
id = "orm-read"
kind = "protected_read"
calls = ["filter_by"]
requires = ["identity_constraint"]
sensitivity = "high"
[[implication]]
id = "admin-covers-owner"
authority = "admin"
implies = "owner"
[[implication]]
id = "owner-covers-staff"
authority = "owner"
implies = "staff"
"""
def test_parse_authority_single_name_is_exact() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef, parse_authority
authority = parse_authority("admin")
assert authority == AuthorityRef(values=("admin",))
assert authority.values == ("admin",)
assert authority.is_unknown is False
def test_parse_authority_name_collection_is_a_finite_set() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef, parse_authority
authority = parse_authority(["owner", "admin", "owner"])
assert authority == AuthorityRef(values=("admin", "owner"))
def test_parse_authority_without_a_binding_is_unknown() -> None:
from openultrasast.semantic.obligations.abstract_domain import UNKNOWN_AUTHORITY, parse_authority
authority = parse_authority(None)
assert authority == UNKNOWN_AUTHORITY
assert authority.is_unknown is True
assert authority.values == ()
def test_authority_grades_distinguish_exact_finite_and_unknown() -> None:
# Req 11.1: authority compatibility distinguishes definite, possible, absent and unknown evidence.
from openultrasast.semantic.obligations.abstract_domain import UNKNOWN_AUTHORITY, parse_authority
exact = parse_authority("admin")
finite = parse_authority(["admin", "owner"])
grades = {exact, finite, UNKNOWN_AUTHORITY}
assert len(grades) == 3
assert (len(exact.values), exact.is_unknown) == (1, False)
assert (len(finite.values), finite.is_unknown) == (2, False)
assert UNKNOWN_AUTHORITY.values == () and UNKNOWN_AUTHORITY.is_unknown
def test_authority_values_are_normalized_deterministically() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
forward = AuthorityRef(values=("owner", "admin", "owner"))
backward = AuthorityRef(values=("admin", "owner"))
assert forward == backward
assert hash(forward) == hash(backward)
assert len({forward, backward}) == 1
def test_authority_ref_fails_loud_on_misshapen_values() -> None:
from openultrasast.semantic.obligations.abstract_domain import AuthorityRef
with pytest.raises(ValueError, match="at least one"):
AuthorityRef(values=())
with pytest.raises(ValueError, match="unknown authority"):
AuthorityRef(values=("admin",), is_unknown=True)
with pytest.raises(ValueError, match="misshapen"):
AuthorityRef(values=("admin", ""))
with pytest.raises(ValueError, match="misshapen"):
AuthorityRef(values=("",))
def test_parse_authority_rejects_misshapen_bindings() -> None:
from openultrasast.semantic.obligations.abstract_domain import parse_authority
with pytest.raises(ValueError):
parse_authority("")
with pytest.raises(ValueError):
parse_authority(())
with pytest.raises(ValueError):
parse_authority(["admin", ""])
def test_join_authorities_keeps_identical_exact_authorities_exact() -> None:
from openultrasast.semantic.obligations.abstract_domain import join_authorities, parse_authority
authority = parse_authority("admin")
assert join_authorities(authority, parse_authority("admin")) == authority
def test_join_authorities_merges_different_authorities_into_a_finite_set() -> None:
from openultrasast.semantic.obligations.abstract_domain import join_authorities, parse_authority
joined = join_authorities(parse_authority("admin"), parse_authority("owner"))
assert joined == parse_authority(["admin", "owner"])
def test_join_authorities_unions_finite_sets_to_their_superset() -> None:
from openultrasast.semantic.obligations.abstract_domain import join_authorities, parse_authority
subset = parse_authority(["admin"])
superset = parse_authority(["admin", "owner"])
assert join_authorities(subset, superset) == superset
assert join_authorities(superset, subset) == superset
assert join_authorities(parse_authority(["admin"]), parse_authority(["staff"])) == parse_authority(["admin", "staff"])
def test_join_authorities_unknown_absorbs_every_fixture() -> None:
# Req 11.1: an unresolvable authority stays unresolvable; joining cannot manufacture precision.
from openultrasast.semantic.obligations.abstract_domain import UNKNOWN_AUTHORITY, join_authorities
for other in _authority_fixtures():
assert join_authorities(other, UNKNOWN_AUTHORITY) == UNKNOWN_AUTHORITY, other
assert join_authorities(UNKNOWN_AUTHORITY, other) == UNKNOWN_AUTHORITY, other
def test_join_authorities_is_commutative_and_idempotent_over_fixtures() -> None:
from openultrasast.semantic.obligations.abstract_domain import join_authorities
fixtures = _authority_fixtures()
for left, right in itertools.product(fixtures, repeat=2):
assert join_authorities(left, right) == join_authorities(right, left), (left, right)
for authority in fixtures:
assert join_authorities(authority, authority) == authority, authority
def test_sufficient_exact_authority_satisfies_the_required_authority() -> None:
# Req 2.5, 11.1: a witness holding exactly the required authority is a definite match.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
assert authority_satisfies(parse_authority("admin"), parse_authority("admin")) is EvidenceRelation.MUST
def test_sufficient_authority_via_promoted_implication_satisfies() -> None:
# Req 2.8: one authority implying another discharges the requirement, from promoted facts only.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
implications = _implication_fixtures()
assert authority_satisfies(parse_authority("staff"), parse_authority("authenticated"), implications) is EvidenceRelation.MUST
assert authority_satisfies(parse_authority("admin"), parse_authority("staff"), implications) is EvidenceRelation.MUST
def test_transitive_implication_chain_satisfies_across_every_hop() -> None:
# Req 2.8: admin -> owner -> staff -> authenticated must hold end to end.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
assert authority_satisfies(parse_authority("admin"), parse_authority("authenticated"), _implication_fixtures()) is EvidenceRelation.MUST
def test_cyclic_implications_terminate() -> None:
from openultrasast.semantic.obligations.abstract_domain import (
AuthorityImplication,
EvidenceRelation,
authority_satisfies,
parse_authority,
)
ring = (
AuthorityImplication(id="billing-covers-audit", authority="billing", implies="audit"),
AuthorityImplication(id="audit-covers-billing", authority="audit", implies="billing"),
)
assert authority_satisfies(parse_authority("billing"), parse_authority("audit"), ring) is EvidenceRelation.MUST
assert authority_satisfies(parse_authority("billing"), parse_authority("admin"), ring) is EvidenceRelation.NO
def test_implication_applies_only_when_promoted() -> None:
# Req 2.8: the engine bakes in no implication vocabulary; without the promoted fact it is NO.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
assert authority_satisfies(parse_authority("staff"), parse_authority("authenticated"), ()) is EvidenceRelation.NO
def test_insufficient_authority_is_definitely_insufficient() -> None:
# Req 2.5: an insufficient authority must not discharge the operation; the relation is NO.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
assert authority_satisfies(parse_authority("guest"), parse_authority("admin"), _implication_fixtures()) is EvidenceRelation.NO
def test_unknown_held_authority_cannot_be_decided() -> None:
# Req 2.7, 11.1: undecidable authority compatibility is UNKNOWN, never a broad match or a rejection.
from openultrasast.semantic.obligations.abstract_domain import (
UNKNOWN_AUTHORITY,
EvidenceRelation,
authority_satisfies,
parse_authority,
)
assert authority_satisfies(UNKNOWN_AUTHORITY, parse_authority("admin"), _implication_fixtures()) is EvidenceRelation.UNKNOWN
def test_unknown_required_authority_cannot_be_decided() -> None:
# Req 2.7: an unresolvable requirement is UNKNOWN, so the pair cannot become a final verdict.
from openultrasast.semantic.obligations.abstract_domain import (
UNKNOWN_AUTHORITY,
EvidenceRelation,
authority_satisfies,
parse_authority,
)
assert authority_satisfies(parse_authority("admin"), UNKNOWN_AUTHORITY, _implication_fixtures()) is EvidenceRelation.UNKNOWN
def test_finite_authority_with_one_satisfying_alternative_is_possible() -> None:
# Req 11.1: one satisfying and one failing alternative join to MAY, never to MUST or NO.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
relation = authority_satisfies(parse_authority(["admin", "guest"]), parse_authority("admin"), _implication_fixtures())
assert relation is EvidenceRelation.MAY
def test_finite_authority_with_all_alternatives_satisfying_is_definite() -> None:
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
relation = authority_satisfies(parse_authority(["admin", "owner"]), parse_authority("staff"), _implication_fixtures())
assert relation is EvidenceRelation.MUST
def test_widened_required_authority_stays_possible_not_definite() -> None:
# Req 2.7, 11.1: a requirement only imprecisely resolved to {authenticated, guest} is MAY, not MUST.
from openultrasast.semantic.obligations.abstract_domain import EvidenceRelation, authority_satisfies, parse_authority
relation = authority_satisfies(parse_authority("admin"), parse_authority(["authenticated", "guest"]), _implication_fixtures())
assert relation is EvidenceRelation.MAY
def test_authority_satisfies_folds_single_pairs_with_join_alternatives() -> None:
# The 7.1 law: the aggregate is exactly join_alternatives over every held x required singleton pair.
from openultrasast.semantic.obligations.abstract_domain import (
EvidenceRelation,
authority_satisfies,
join_alternatives,
parse_authority,
)
implications = _implication_fixtures()
for held, required in itertools.product(_authority_fixtures(), repeat=2):
aggregated = authority_satisfies(held, required, implications)
if held.is_unknown or required.is_unknown:
assert aggregated is EvidenceRelation.UNKNOWN, (held, required)
continue
folded: EvidenceRelation | None = None
for held_name in held.values:
for required_name in required.values:
single = authority_satisfies(parse_authority(held_name), parse_authority(required_name), implications)
folded = single if folded is None else join_alternatives(folded, single)
assert folded == aggregated, (held, required)
def test_widening_either_authority_never_gains_definiteness() -> None:
# Req 11.1, 11.6: coarsening a value moves the relation up the precision order or leaves it.
from openultrasast.semantic.obligations.abstract_domain import at_least_as_precise, authority_satisfies, join_authorities
implications = _implication_fixtures()
fixtures = _authority_fixtures()
for held, required, extra in itertools.product(fixtures, repeat=3):
base = authority_satisfies(held, required, implications)
wider_held = authority_satisfies(join_authorities(held, extra), required, implications)
assert at_least_as_precise(base, wider_held), (held, extra, required)
wider_required = authority_satisfies(held, join_authorities(required, extra), implications)
assert at_least_as_precise(base, wider_required), (held, required, extra)
def test_supporting_implications_retain_the_fact_ids_used() -> None:
# Req 2.8: the implications used on the satisfying path are retained with their supporting fact ids.
from openultrasast.semantic.obligations.abstract_domain import authority_satisfies, parse_authority, supporting_implications
implications = _implication_fixtures()
used = supporting_implications(parse_authority("admin"), parse_authority("authenticated"), implications)
assert [implication.id for implication in used] == [
"admin-covers-owner",
"owner-covers-staff",
"staff-covers-authenticated",
]
assert authority_satisfies(parse_authority("admin"), parse_authority("authenticated"), implications).value == "must"
def test_supporting_implications_are_empty_for_identity_and_for_failure() -> None:
from openultrasast.semantic.obligations.abstract_domain import (
UNKNOWN_AUTHORITY,
parse_authority,
supporting_implications,
)
implications = _implication_fixtures()
assert supporting_implications(parse_authority("admin"), parse_authority("admin"), implications) == ()
assert supporting_implications(parse_authority("guest"), parse_authority("admin"), implications) == ()
assert supporting_implications(UNKNOWN_AUTHORITY, parse_authority("admin"), implications) == ()
def test_supporting_implications_deduplicate_stably() -> None:
from openultrasast.semantic.obligations.abstract_domain import parse_authority, supporting_implications
implications = _implication_fixtures()
assert supporting_implications(parse_authority("owner"), parse_authority("staff"), implications + implications) == (implications[1],)
def test_implication_facts_load_and_retain_supporting_ids(tmp_path: Path) -> None:
# Req 2.8: implications come from promoted fact rows, keep the row id for the trace, and stay
# AuthorityImplication values so the checker can feed them straight into the satisfy algebra.
from openultrasast.semantic.obligations.abstract_domain import AuthorityImplication
from openultrasast.semantic.obligations.facts import ImplicationFact, load_obligation_facts
(tmp_path / "python.toml").write_text(_IMPLICATION_TOML)
facts = load_obligation_facts(tmp_path)
assert facts.implications == (
ImplicationFact(id="admin-covers-owner", authority="admin", implies="owner", language="python"),
ImplicationFact(id="owner-covers-staff", authority="owner", implies="staff", language="python"),
)
assert all(isinstance(implication, AuthorityImplication) for implication in facts.implications)
def test_implication_facts_default_to_empty(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import load_obligation_facts
(tmp_path / "python.toml").write_text(
'version = "1"\nlanguage = "python"\n\n[[operation]]\nid = "orm-read"\nkind = "protected_read"\n'
'calls = ["filter_by"]\nrequires = ["identity_constraint"]\nsensitivity = "high"\n'
)
assert load_obligation_facts(tmp_path).implications == ()
def test_implication_facts_filter_by_language(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import load_obligation_facts
(tmp_path / "python.toml").write_text(_IMPLICATION_TOML)
(tmp_path / "ruby.toml").write_text(
'version = "1"\nlanguage = "ruby"\n\n[[implication]]\nid = "ruby-maintainer-covers-reporter"\n'
'authority = "maintainer"\nimplies = "reporter"\n'
)
all_languages = load_obligation_facts(tmp_path)
assert {implication.id for implication in all_languages.implications} == {
"admin-covers-owner",
"owner-covers-staff",
"ruby-maintainer-covers-reporter",
}
python = all_languages.for_language("python")
assert [implication.id for implication in python.implications] == ["admin-covers-owner", "owner-covers-staff"]
assert all_languages.for_language("ruby").implications != python.implications
def test_implication_fact_rejects_unknown_fields(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import ObligationFactsError, load_obligation_facts
(tmp_path / "python.toml").write_text(_IMPLICATION_TOML + "weight = 3\n")
with pytest.raises(ObligationFactsError, match="weight"):
load_obligation_facts(tmp_path)
def test_implication_fact_rejects_missing_or_misshapen_names(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import ObligationFactsError, load_obligation_facts
(tmp_path / "python.toml").write_text('version = "1"\nlanguage = "python"\n\n[[implication]]\nid = "broken"\nimplies = "owner"\n')
with pytest.raises(ObligationFactsError, match="authority"):
load_obligation_facts(tmp_path)
(tmp_path / "python.toml").write_text('version = "1"\nlanguage = "python"\n\n[[implication]]\nid = "broken"\nauthority = "admin"\n')
with pytest.raises(ObligationFactsError, match="implies"):
load_obligation_facts(tmp_path)
(tmp_path / "python.toml").write_text(
'version = "1"\nlanguage = "python"\n\n[[implication]]\nid = "broken"\nauthority = "admin"\nimplies = ""\n'
)
with pytest.raises(ObligationFactsError, match="implies"):
load_obligation_facts(tmp_path)
def test_sufficient_insufficient_and_unknown_fixtures_produce_the_specified_relations() -> None:
# The task's three fixture grades: sufficient -> MUST, insufficient -> NO, unknown -> UNKNOWN.
from openultrasast.semantic.obligations.abstract_domain import (
UNKNOWN_AUTHORITY,
EvidenceRelation,
authority_satisfies,
parse_authority,
)
implications = _implication_fixtures()
sufficient = (
(parse_authority("admin"), parse_authority("admin")),
(parse_authority("staff"), parse_authority("authenticated")),
(parse_authority(["admin", "owner"]), parse_authority("staff")),
)
for held, required in sufficient:
assert authority_satisfies(held, required, implications) is EvidenceRelation.MUST, (held, required)
insufficient = (
(parse_authority("guest"), parse_authority("admin")),
(parse_authority("staff"), parse_authority("authenticated"), ()),
)
for fixture in insufficient:
held, required, *rest = fixture
assert authority_satisfies(held, required, *rest) is EvidenceRelation.NO, fixture
assert authority_satisfies(UNKNOWN_AUTHORITY, parse_authority("admin"), implications) is EvidenceRelation.UNKNOWN
assert authority_satisfies(parse_authority("admin"), UNKNOWN_AUTHORITY, implications) is EvidenceRelation.UNKNOWN