107 lines
3.2 KiB
Python
107 lines
3.2 KiB
Python
"""Pure data types shared by every module: the request, the decision and its outcome."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from types import MappingProxyType
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
|
|
def same_value(left: Any, right: Any) -> bool:
|
|
"""Type-strict equality, so that YAML `true` never matches `1` or `"true"`."""
|
|
return type(left) is type(right) and left == right
|
|
|
|
|
|
def frozen_mapping(value: Mapping[str, Any] | None) -> Mapping[str, Any]:
|
|
return MappingProxyType(dict(value or {}))
|
|
|
|
|
|
def frozen_tuple(value: Iterable[str] | None) -> tuple[str, ...]:
|
|
return tuple(value or ())
|
|
|
|
|
|
class Outcome(str, Enum):
|
|
AUTONOMOUS = "AUTONOMOUS"
|
|
AUTONOMOUS_WITH_OBLIGATIONS = "AUTONOMOUS_WITH_OBLIGATIONS"
|
|
ESCALATE = "ESCALATE"
|
|
DENY = "DENY"
|
|
ROOT_AUTHORIZATION_REQUIRED = "ROOT_AUTHORIZATION_REQUIRED"
|
|
|
|
@classmethod
|
|
def from_mode(cls, mode: Any) -> Outcome | None:
|
|
"""Map an envelope `mode` string to an Outcome; None for unknown modes."""
|
|
try:
|
|
return cls(str(mode).upper())
|
|
except ValueError:
|
|
return None
|
|
|
|
@property
|
|
def grants(self) -> bool:
|
|
return self in (Outcome.AUTONOMOUS, Outcome.AUTONOMOUS_WITH_OBLIGATIONS)
|
|
|
|
@property
|
|
def effect(self) -> str:
|
|
if self.grants:
|
|
return "allow"
|
|
if self is Outcome.DENY:
|
|
return "deny"
|
|
return "escalate"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Request:
|
|
"""A privileged effect an agent intends, described independently of any credential (A4)."""
|
|
|
|
request_id: str
|
|
principal: str | None
|
|
decision_class: str | None
|
|
scope: Mapping[str, Any]
|
|
attributes: Mapping[str, Any]
|
|
risk: Mapping[str, Any]
|
|
evidence: frozenset[str]
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Mapping[str, Any]) -> Request:
|
|
return cls(
|
|
request_id=str(data.get("request_id", "")),
|
|
principal=data.get("principal"),
|
|
decision_class=data.get("decision_class"),
|
|
scope=frozen_mapping(data.get("scope")),
|
|
attributes=frozen_mapping(data.get("attributes")),
|
|
risk=frozen_mapping(data.get("risk")),
|
|
evidence=frozenset(data.get("evidence") or ()),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Decision:
|
|
"""Decision record (canon B2). `missing` lists what stands between the request and an allow."""
|
|
|
|
request_id: str
|
|
decision_class: str | None
|
|
outcome: Outcome
|
|
blocking_condition: str | None = None
|
|
route_to: str | None = None
|
|
rules: tuple[str, ...] = ()
|
|
obligations: tuple[str, ...] = ()
|
|
missing: tuple[str, ...] = ()
|
|
source: str | None = None
|
|
|
|
@property
|
|
def effect(self) -> str:
|
|
return self.outcome.effect
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"request_id": self.request_id,
|
|
"decision_class": self.decision_class,
|
|
"outcome": self.outcome.value,
|
|
"effect": self.effect,
|
|
"blocking_condition": self.blocking_condition,
|
|
"route_to": self.route_to,
|
|
"rules": list(self.rules),
|
|
"obligations": list(self.obligations),
|
|
"missing": list(self.missing),
|
|
"source": self.source,
|
|
}
|