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,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
(``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
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
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.
"""
from __future__ import annotations
from collections.abc import Iterable, Mapping
from collections.abc import Iterable, Mapping, Sequence
from dataclasses import dataclass
from enum import Enum
from typing import Literal
class EvidenceRelation(Enum):
@@ -38,6 +44,70 @@ class Completeness:
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] = {
(EvidenceRelation.MUST, EvidenceRelation.MUST): EvidenceRelation.MUST,
(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"
_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(
{"id", "kind", "calls", "decorators", "identity_sources", "constraint_params", "request_sources", "permissive_values"}
@@ -50,6 +61,9 @@ class OperationFact:
query_document_arg: int | None = None
handlers: tuple[str, ...] = ()
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)
@@ -97,48 +111,7 @@ def _load_obligation_facts_uncached(directory: Path | None = None) -> Obligation
language = str(payload.get("language") or path.stem)
versions.append(str(payload.get("version", "1")))
for row in _rows(payload.get("operation"), path):
_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}")
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,
)
)
operations.append(_operation_fact(row, path, language))
for row in _rows(payload.get("discharger"), path):
_only_fields(row, _DISCHARGER_FIELDS, path)
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))
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]]:
if value is None:
return []