feat(access-control-prover): 7.2 resource abstract values and operation-fact fields

This commit is contained in:
Marius Ciepluch
2026-10-29 14:48:53 +01:00
parent 5697a7fe9e
commit 351583a04f
4 changed files with 370 additions and 47 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
- Done when exhaustive table tests and algebraic law tests pass for every relation pair. - Done when exhaustive table tests and algebraic law tests pass for every relation pair.
- _Requirements: 11.1, 11.6, 11.7_ - _Requirements: 11.1, 11.6, 11.7_
- _Boundary: Abstract Evidence Domain_ - _Boundary: Abstract Evidence Domain_
- [ ] 7.2 Add resource abstract values and operation-fact fields - [x] 7.2 Add resource abstract values and operation-fact fields
- Represent exact resources, finite alias sets, request-global scope and unknown resources. - Represent exact resources, finite alias sets, request-global scope and unknown resources.
- Extend operation facts to state when a request-global guard is sufficient. - Extend operation facts to state when a request-global guard is sufficient.
- Done when resource parsing and joins distinguish exact, alias, global and unknown fixtures. - Done when resource parsing and joins distinguish exact, alias, global and unknown fixtures.
@@ -1,19 +1,25 @@
"""Evidence relations and the completeness value (access-control-prover, Req 11.1, 11.6, 11.7). """Evidence relations, the completeness value and resource abstract values (access-control-prover).
Covers Req 2.1, 2.5, 2.6, 2.7, 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
from being used for the wrong quantifier: ``join_alternatives`` merges evidence from alternative call from being used for the wrong quantifier: ``join_alternatives`` merges evidence from alternative call
targets or control paths, ``conjoin_dimensions`` combines one witness's kind, scope, resource, authority targets or control paths, ``conjoin_dimensions`` combines one witness's kind, scope, resource, authority
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. Every operation is deterministic, pure and independent of target names; join never converts 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 absorb concrete bindings and never lets an unresolvable value resolve. Every
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.abc import Iterable, Mapping from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Literal
class EvidenceRelation(Enum): class EvidenceRelation(Enum):
@@ -38,6 +44,70 @@ class Completeness:
object.__setattr__(self, "reasons", tuple(dict.fromkeys(self.reasons))) object.__setattr__(self, "reasons", tuple(dict.fromkeys(self.reasons)))
_RESOURCE_KINDS = ("exact", "aliases", "request_global", "unknown")
@dataclass(frozen=True)
class ResourceRef:
"""The abstract value of one resource binding (design, "Abstract evidence domain"; Req 2.1, 2.5, 2.6, 2.7, 11.1).
``exact`` binds one named resource, ``aliases`` binds a finite set of them, ``request_global`` binds the
whole request without resource equality, and ``unknown`` is an unresolvable or unbounded binding. Values
are deduplicated and sorted, so equal bindings are equal however they were constructed; a misshapen kind
or arity fails loud rather than degrading silently.
"""
kind: Literal["exact", "aliases", "request_global", "unknown"]
values: tuple[str, ...] = ()
def __post_init__(self) -> None:
if self.kind not in _RESOURCE_KINDS:
raise ValueError(f"unknown resource kind: {self.kind!r}")
tokens = sorted(set(self.values))
if self.kind == "exact" and len(tokens) != 1:
raise ValueError(f"exact resource needs exactly one token: {self.values!r}")
if self.kind == "aliases" and not tokens:
raise ValueError("alias set needs at least one token")
if self.kind in ("request_global", "unknown") and tokens:
raise ValueError(f"{self.kind} resource binds no tokens: {self.values!r}")
object.__setattr__(self, "values", tuple(tokens))
REQUEST_GLOBAL_RESOURCE = ResourceRef(kind="request_global")
UNKNOWN_RESOURCE = ResourceRef(kind="unknown")
def parse_resource(binding: str | Sequence[str] | None) -> ResourceRef:
"""Parse one resource binding from fact or IR evidence into its abstract value.
A single identifier token is ``exact``, a finite token collection is a finite alias set, and ``None``
(no binding could be resolved) is ``unknown``; anything misshapen raises instead of quietly degrading.
"""
if binding is None:
return UNKNOWN_RESOURCE
tokens = (binding,) if isinstance(binding, str) else tuple(binding)
if not tokens or not all(isinstance(token, str) and token for token in tokens):
raise ValueError(f"misshapen resource binding: {binding!r}")
kind: Literal["exact", "aliases"] = "exact" if isinstance(binding, str) else "aliases"
return ResourceRef(kind=kind, values=tokens)
def join_resources(left: ResourceRef, right: ResourceRef) -> ResourceRef:
"""Join resource bindings from alternative paths or call targets.
Alias sets join by finite union and identical exact tokens stay exact; request-global scope absorbs any
concrete binding because it already covers every resource in the request (Req 2.6), and an unbounded or
unresolvable value joins to ``unknown`` so losing precision cannot manufacture an exact match (Req 2.7, 11.1).
"""
if left.kind == "unknown" or right.kind == "unknown":
return UNKNOWN_RESOURCE
if left.kind == "request_global" or right.kind == "request_global":
return REQUEST_GLOBAL_RESOURCE
if left.kind == right.kind == "exact" and left.values == right.values:
return left
return ResourceRef(kind="aliases", values=left.values + right.values)
_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,
+74 -43
View File
@@ -22,7 +22,18 @@ SENSITIVITIES = ("low", "medium", "high")
DEFAULT_OBLIGATION_FACTS_DIR = Path(__file__).resolve().parents[2] / "ruleset" / "obligations" DEFAULT_OBLIGATION_FACTS_DIR = Path(__file__).resolve().parents[2] / "ruleset" / "obligations"
_OPERATION_FIELDS = frozenset( _OPERATION_FIELDS = frozenset(
{"id", "kind", "calls", "resource_arg", "requires", "sensitivity", "query_document_arg", "handlers", "framework"} {
"id",
"kind",
"calls",
"resource_arg",
"requires",
"sensitivity",
"query_document_arg",
"handlers",
"framework",
"request_global_kinds",
}
) )
_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"}
@@ -50,6 +61,9 @@ class OperationFact:
query_document_arg: int | None = None query_document_arg: int | None = None
handlers: tuple[str, ...] = () handlers: tuple[str, ...] = ()
framework: str | None = None framework: str | None = None
# Requirement 2.6 (access-control-prover): discharger kinds a request-global witness may discharge for
# this operation at request scope, without resource equality. Declared by the fact, never inferred.
request_global_kinds: tuple[str, ...] = ()
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -97,48 +111,7 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
language = str(payload.get("language") or path.stem) language = str(payload.get("language") or path.stem)
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):
_only_fields(row, _OPERATION_FIELDS, path) operations.append(_operation_fact(row, path, language))
kind = str(row.get("kind", ""))
if kind not in OPERATION_KINDS:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown kind {kind!r}")
requires = _strings(row.get("requires"), "requires", path)
for item in requires:
if item not in DISCHARGER_KINDS:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} requires unknown discharger kind {item!r}")
sensitivity = str(row.get("sensitivity", "medium"))
if sensitivity not in SENSITIVITIES:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown sensitivity {sensitivity!r}")
resource_arg = row.get("resource_arg")
if resource_arg is not None and not isinstance(resource_arg, int):
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} resource_arg must be an integer")
document_arg = row.get("query_document_arg")
if document_arg is not None and (not isinstance(document_arg, int) or document_arg < 1):
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} query_document_arg must be a positive integer")
handlers = _strings(row.get("handlers"), "handlers", path)
framework = row.get("framework")
if framework is not None and (not isinstance(framework, str) or not framework):
raise ObligationFactsError("invalid operation framework")
if handlers and framework is None:
raise ObligationFactsError("handler operation needs a framework")
for pattern in handlers:
try:
re.compile(pattern)
except re.error as exc:
raise ObligationFactsError(type(exc).__name__) from exc
operations.append(
OperationFact(
id=str(row["id"]),
kind=kind,
language=language,
calls=_strings(row.get("calls"), "calls", path),
resource_arg=resource_arg,
requires=requires,
sensitivity=sensitivity,
query_document_arg=document_arg,
handlers=handlers,
framework=framework,
)
)
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", ""))
@@ -160,6 +133,64 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
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))
def _operation_fact(row: dict[str, object], path: Path, language: str) -> OperationFact:
"""Validate one operation row against the closed sets and parse it; any unknown kind or field fails loud."""
_only_fields(row, _OPERATION_FIELDS, path)
kind = str(row.get("kind", ""))
if kind not in OPERATION_KINDS:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown kind {kind!r}")
requires = _strings(row.get("requires"), "requires", path)
for item in requires:
if item not in DISCHARGER_KINDS:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} requires unknown discharger kind {item!r}")
sensitivity = str(row.get("sensitivity", "medium"))
if sensitivity not in SENSITIVITIES:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} has unknown sensitivity {sensitivity!r}")
request_global_kinds = _strings(row.get("request_global_kinds"), "request_global_kinds", path)
for item in request_global_kinds:
if item not in DISCHARGER_KINDS:
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} request_global_kinds names unknown discharger kind {item!r}")
handler_patterns, handler_framework = _handler_shape(row, path)
return OperationFact(
id=str(row["id"]),
kind=kind,
language=language,
calls=_strings(row.get("calls"), "calls", path),
resource_arg=_optional_int(row, "resource_arg", path, positive=False),
requires=requires,
sensitivity=sensitivity,
query_document_arg=_optional_int(row, "query_document_arg", path, positive=True),
handlers=handler_patterns,
framework=handler_framework,
request_global_kinds=request_global_kinds,
)
def _optional_int(row: dict[str, object], field: str, path: Path, *, positive: bool) -> int | None:
value = row.get(field)
if value is None:
return None
if not isinstance(value, int) or (positive and value < 1):
requirement = "a positive integer" if positive else "an integer"
raise ObligationFactsError(f"{path}: operation {row.get('id')!r} {field} must be {requirement}")
return value
def _handler_shape(row: dict[str, object], path: Path) -> tuple[tuple[str, ...], str | None]:
handler_patterns = _strings(row.get("handlers"), "handlers", path)
framework = row.get("framework")
if framework is not None and (not isinstance(framework, str) or not framework):
raise ObligationFactsError("invalid operation framework")
if handler_patterns and framework is None:
raise ObligationFactsError("handler operation needs a framework")
for pattern in handler_patterns:
try:
re.compile(pattern)
except re.error as exc:
raise ObligationFactsError(type(exc).__name__) from exc
return handler_patterns, framework
def _rows(value: object, path: Path) -> list[dict[str, object]]: def _rows(value: object, path: Path) -> list[dict[str, object]]:
if value is None: if value is None:
return [] return []
+222
View File
@@ -0,0 +1,222 @@
"""access-control-prover task 7.2: resource abstract values and operation-fact sufficiency fields (offline).
Exact resources, finite alias sets, request-global scope and unknown resources per the design's "Abstract
evidence domain" section (Req 2.1, 2.5, 2.6, 2.7, 11.1), plus the additive operation-fact field that declares
a request-global witness kind sufficient at request scope (Req 2.6).
"""
from __future__ import annotations
import itertools
from pathlib import Path
import pytest
def _resource_fixtures():
from openultrasast.semantic.obligations.abstract_domain import (
REQUEST_GLOBAL_RESOURCE,
UNKNOWN_RESOURCE,
ResourceRef,
)
return (
ResourceRef(kind="exact", values=("book",)),
ResourceRef(kind="exact", values=("volume",)),
ResourceRef(kind="aliases", values=("book", "volume")),
ResourceRef(kind="aliases", values=("volume", "shelf")),
REQUEST_GLOBAL_RESOURCE,
UNKNOWN_RESOURCE,
)
_OPERATION_TOML = """version = "1"
language = "python"
[[operation]]
id = "orm-read"
kind = "protected_read"
calls = ["filter_by"]
requires = ["identity_constraint"]
sensitivity = "high"
request_global_kinds = ["path_guard"]
"""
def test_parse_resource_single_token_is_exact() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef, parse_resource
binding = parse_resource("book")
assert binding == ResourceRef(kind="exact", values=("book",))
assert binding.kind == "exact"
assert binding.values == ("book",)
def test_parse_resource_token_collection_is_a_finite_alias_set() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef, parse_resource
binding = parse_resource(["volume", "book", "volume"])
assert binding == ResourceRef(kind="aliases", values=("book", "volume"))
def test_parse_resource_without_a_binding_is_unknown() -> None:
from openultrasast.semantic.obligations.abstract_domain import UNKNOWN_RESOURCE, parse_resource
binding = parse_resource(None)
assert binding == UNKNOWN_RESOURCE
assert binding.kind == "unknown"
assert binding.values == ()
def test_request_global_resource_binds_the_whole_request() -> None:
from openultrasast.semantic.obligations.abstract_domain import REQUEST_GLOBAL_RESOURCE
assert REQUEST_GLOBAL_RESOURCE.kind == "request_global"
assert REQUEST_GLOBAL_RESOURCE.values == ()
def test_resource_kinds_distinguish_exact_alias_global_and_unknown() -> None:
# Req 2.7, 11.1: the four grades stay distinct; an unknown binding never collapses onto a concrete one.
from openultrasast.semantic.obligations.abstract_domain import (
REQUEST_GLOBAL_RESOURCE,
UNKNOWN_RESOURCE,
parse_resource,
)
grades = {parse_resource("book"), parse_resource(["book"]), REQUEST_GLOBAL_RESOURCE, UNKNOWN_RESOURCE}
assert {resource.kind for resource in grades} == {"exact", "aliases", "request_global", "unknown"}
def test_alias_values_are_normalized_deterministically() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef
forward = ResourceRef(kind="aliases", values=("volume", "book", "volume"))
backward = ResourceRef(kind="aliases", values=("book", "volume"))
assert forward == backward
assert hash(forward) == hash(backward)
assert len({forward, backward}) == 1
def test_resource_ref_fails_loud_on_misshapen_kinds_and_arities() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef
with pytest.raises(ValueError, match="sometimes"):
ResourceRef(kind="sometimes", values=("book",))
with pytest.raises(ValueError, match="exact"):
ResourceRef(kind="exact")
with pytest.raises(ValueError, match="exact"):
ResourceRef(kind="exact", values=("book", "volume"))
with pytest.raises(ValueError, match="alias"):
ResourceRef(kind="aliases", values=())
with pytest.raises(ValueError, match="request_global"):
ResourceRef(kind="request_global", values=("book",))
def test_parse_resource_rejects_misshapen_bindings() -> None:
from openultrasast.semantic.obligations.abstract_domain import parse_resource
with pytest.raises(ValueError):
parse_resource("")
with pytest.raises(ValueError):
parse_resource(())
with pytest.raises(ValueError):
parse_resource(["book", ""])
def test_join_resources_keeps_identical_exact_tokens_exact() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef, join_resources
binding = ResourceRef(kind="exact", values=("book",))
assert join_resources(binding, ResourceRef(kind="exact", values=("book",))) == binding
def test_join_resources_merges_different_exact_tokens_into_an_alias_set() -> None:
# Req 2.5: a different resource never collapses onto one exact binding; it widens to the finite alias set.
from openultrasast.semantic.obligations.abstract_domain import ResourceRef, join_resources
joined = join_resources(
ResourceRef(kind="exact", values=("book",)),
ResourceRef(kind="exact", values=("volume",)),
)
assert joined == ResourceRef(kind="aliases", values=("book", "volume"))
def test_join_resources_adds_exact_tokens_into_alias_sets() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef, join_resources
joined = join_resources(
ResourceRef(kind="exact", values=("shelf",)),
ResourceRef(kind="aliases", values=("book", "volume")),
)
assert joined == ResourceRef(kind="aliases", values=("book", "shelf", "volume"))
def test_join_resources_unions_alias_sets_to_their_superset() -> None:
from openultrasast.semantic.obligations.abstract_domain import ResourceRef, join_resources
subset = ResourceRef(kind="aliases", values=("book",))
superset = ResourceRef(kind="aliases", values=("book", "volume"))
assert join_resources(subset, superset) == superset
assert join_resources(superset, subset) == superset
disjoint = join_resources(subset, ResourceRef(kind="aliases", values=("volume",)))
assert disjoint == ResourceRef(kind="aliases", values=("book", "volume"))
def test_join_resources_request_global_absorbs_concrete_bindings() -> None:
# Req 2.6: request-global scope already covers every resource in the request.
from openultrasast.semantic.obligations.abstract_domain import (
REQUEST_GLOBAL_RESOURCE,
ResourceRef,
join_resources,
)
for concrete in (
ResourceRef(kind="exact", values=("book",)),
ResourceRef(kind="aliases", values=("book", "volume")),
):
assert join_resources(concrete, REQUEST_GLOBAL_RESOURCE) == REQUEST_GLOBAL_RESOURCE
assert join_resources(REQUEST_GLOBAL_RESOURCE, concrete) == REQUEST_GLOBAL_RESOURCE
assert join_resources(REQUEST_GLOBAL_RESOURCE, REQUEST_GLOBAL_RESOURCE) == REQUEST_GLOBAL_RESOURCE
def test_join_resources_unknown_absorbs_every_fixture() -> None:
# Req 2.7, 11.1: an unresolvable binding stays unresolvable; joining cannot manufacture precision.
from openultrasast.semantic.obligations.abstract_domain import UNKNOWN_RESOURCE, join_resources
for other in _resource_fixtures():
assert join_resources(other, UNKNOWN_RESOURCE) == UNKNOWN_RESOURCE, other
assert join_resources(UNKNOWN_RESOURCE, other) == UNKNOWN_RESOURCE, other
def test_join_resources_is_commutative_and_idempotent_over_fixtures() -> None:
from openultrasast.semantic.obligations.abstract_domain import join_resources
fixtures = _resource_fixtures()
for left, right in itertools.product(fixtures, repeat=2):
assert join_resources(left, right) == join_resources(right, left), (left, right)
for resource in fixtures:
assert join_resources(resource, resource) == resource, resource
def test_operation_fact_declares_request_global_sufficient_kinds(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import load_obligation_facts
(tmp_path / "python.toml").write_text(_OPERATION_TOML)
(row,) = load_obligation_facts(tmp_path).operations
assert row.request_global_kinds == ("path_guard",)
def test_operation_fact_without_sufficiency_declares_none(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import load_obligation_facts
(tmp_path / "python.toml").write_text(_OPERATION_TOML.replace('request_global_kinds = ["path_guard"]\n', ""))
(row,) = load_obligation_facts(tmp_path).operations
assert row.request_global_kinds == ()
def test_operation_fact_rejects_unknown_request_global_kind(tmp_path: Path) -> None:
from openultrasast.semantic.obligations.facts import ObligationFactsError, load_obligation_facts
(tmp_path / "python.toml").write_text(_OPERATION_TOML.replace('["path_guard"]', '["vibes"]'))
with pytest.raises(ObligationFactsError, match="vibes"):
load_obligation_facts(tmp_path)