Files
aios/active-runtime/app/aios_baseline/authorization.py
T

1012 lines
38 KiB
Python

"""Durable signed authorization chain for the AIOS baseline 1.1 runtime.
Owner grants are long-lived policy roots. The local authorization service
stores and verifies those roots, then issues short machine-signed operational
epochs and task credentials. Authorization always rereads durable grant state;
there is deliberately no revocation cache.
"""
from __future__ import annotations
import base64
import binascii
import hashlib
import json
import math
import os
from pathlib import Path
import re
import secrets
import sqlite3
import time
from typing import Any, Callable, Mapping
import uuid
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
Ed25519PublicKey,
)
GRANT_SCHEMA = "aios.owner-delegation-grant.v1"
EPOCH_SCHEMA = "aios.operational-epoch.v1"
PRINCIPAL_SCHEMA = "aios.task-principal.v1"
SIGNATURE_ALGORITHM = "Ed25519"
MAX_PRINCIPAL_TTL_SECONDS = 300
DESTRUCTIVE_CEILINGS = frozenset({"NONE", "REVERSIBLE", "IRREVERSIBLE"})
_TEXT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,255}$")
_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
_DOMAIN = b"AIOS-AUTHORIZATION\x00"
_SCOPE_FIELDS = (
"mission_ids",
"task_ids",
"operations",
"component_ids",
"environments",
)
_BINDING_FIELDS = (
"tenant_id",
"subject_id",
"mission_id",
"task_id",
"operation",
"component_id",
"environment",
)
_ROOT_FIELDS = (
"root_grant_id",
"root_grant_digest",
"root_grant_generation",
)
class AuthorizationError(RuntimeError):
"""Raised when an authorization object or state transition is invalid."""
def _plain_object(value: Any, name: str) -> dict[str, Any]:
if type(value) is not dict:
raise AuthorizationError(f"{name} must be a plain object")
return value
def _exact_fields(value: dict[str, Any], expected: set[str], name: str) -> None:
if set(value) != expected:
raise AuthorizationError(f"{name} fields are invalid")
def _text(value: Any, name: str) -> str:
if type(value) is not str or not _TEXT_RE.fullmatch(value):
raise AuthorizationError(f"{name} is invalid")
return value
def _positive_int(value: Any, name: str) -> int:
if type(value) is not int or value <= 0:
raise AuthorizationError(f"{name} must be a positive integer")
return value
def _timestamp(value: Any, name: str) -> int:
return _positive_int(value, name)
def _canonical_json(value: Any) -> bytes:
try:
encoded = json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
)
except (TypeError, ValueError) as exc:
raise AuthorizationError("authorization object is not canonical JSON") from exc
return encoded.encode("utf-8")
def _json_copy(value: dict[str, Any]) -> dict[str, Any]:
copied = json.loads(_canonical_json(value))
return _plain_object(copied, "authorization envelope")
def _unsigned_envelope(envelope: dict[str, Any]) -> dict[str, Any]:
signing = _plain_object(envelope.get("signing"), "signing")
_exact_fields(signing, {"algorithm", "key_id", "signature"}, "signing")
unsigned = {key: value for key, value in envelope.items() if key != "signing"}
unsigned["signing"] = {
"algorithm": signing["algorithm"],
"key_id": signing["key_id"],
}
return unsigned
def _signature_message(envelope: dict[str, Any]) -> bytes:
schema = envelope.get("schema")
if type(schema) is not str:
raise AuthorizationError("authorization schema is invalid")
return _DOMAIN + schema.encode("ascii", "strict") + b"\x00" + _canonical_json(
_unsigned_envelope(envelope)
)
def _signature_bytes(value: Any) -> bytes:
if type(value) is not str or not value:
raise AuthorizationError("signature is invalid")
try:
decoded = base64.b64decode(value, validate=True)
except (ValueError, binascii.Error) as exc:
raise AuthorizationError("signature encoding is invalid") from exc
if len(decoded) != 64:
raise AuthorizationError("signature length is invalid")
return decoded
def _sign(
schema: str,
payload: dict[str, Any],
private_key: Ed25519PrivateKey,
key_id: str,
*,
extra: dict[str, Any] | None = None,
) -> dict[str, Any]:
if not isinstance(private_key, Ed25519PrivateKey):
raise AuthorizationError("Ed25519 private key is required")
_text(key_id, "key_id")
envelope: dict[str, Any] = {"schema": schema, "payload": payload}
if extra:
envelope.update(extra)
envelope["signing"] = {
"algorithm": SIGNATURE_ALGORITHM,
"key_id": key_id,
"signature": "",
}
signature = private_key.sign(_signature_message(envelope))
envelope["signing"]["signature"] = base64.b64encode(signature).decode("ascii")
return envelope
def _verify_signature(
envelope: dict[str, Any],
public_keys: Mapping[str, Ed25519PublicKey],
*,
trust_name: str,
) -> None:
signing = _plain_object(envelope.get("signing"), "signing")
_exact_fields(signing, {"algorithm", "key_id", "signature"}, "signing")
if signing["algorithm"] != SIGNATURE_ALGORITHM:
raise AuthorizationError("signature algorithm is invalid")
key_id = _text(signing["key_id"], "signing.key_id")
key = public_keys.get(key_id)
if key is None:
raise AuthorizationError(f"signing key is not a trusted {trust_name} key")
try:
key.verify(_signature_bytes(signing["signature"]), _signature_message(envelope))
except InvalidSignature as exc:
raise AuthorizationError("authorization signature is invalid") from exc
def _scope_list(value: Any, name: str) -> list[str]:
if type(value) is not list or not value or len(value) > 256:
raise AuthorizationError(f"{name} must be a non-empty list")
checked: list[str] = []
for item in value:
if item == "*":
checked.append(item)
else:
checked.append(_text(item, name))
if len(set(checked)) != len(checked):
raise AuthorizationError(f"{name} contains duplicates")
if "*" in checked and checked != ["*"]:
raise AuthorizationError(f"{name} wildcard must be the only entry")
return checked
def _validate_grant_payload(payload: Any) -> dict[str, Any]:
value = _plain_object(payload, "grant payload")
_exact_fields(
value,
{
"grant_id",
"generation",
"tenant_id",
"subject_id",
"scope",
"issued_at",
"validity",
"limits",
"destructive_ceiling",
},
"grant payload",
)
_text(value["grant_id"], "grant_id")
_positive_int(value["generation"], "generation")
_text(value["tenant_id"], "tenant_id")
_text(value["subject_id"], "subject_id")
if type(value["destructive_ceiling"]) is not str or value["destructive_ceiling"] not in DESTRUCTIVE_CEILINGS:
raise AuthorizationError("destructive_ceiling is invalid")
issued_at = _timestamp(value["issued_at"], "issued_at")
scope = _plain_object(value["scope"], "grant scope")
_exact_fields(scope, set(_SCOPE_FIELDS), "grant scope")
for field in _SCOPE_FIELDS:
_scope_list(scope[field], f"scope.{field}")
validity = _plain_object(value["validity"], "grant validity")
mode = validity.get("mode")
if mode == "UNTIL_REVOKED":
_exact_fields(validity, {"mode"}, "grant validity")
elif mode == "EXPIRES_AT":
_exact_fields(validity, {"mode", "expires_at"}, "grant validity")
expires_at = _timestamp(validity["expires_at"], "expires_at")
if expires_at <= issued_at:
raise AuthorizationError("expires_at must be after issued_at")
else:
raise AuthorizationError("grant validity mode is invalid")
limits = _plain_object(value["limits"], "grant limits")
if set(limits) not in (set(), {"max_effects"}):
raise AuthorizationError("grant limits fields are invalid")
if "max_effects" in limits:
_positive_int(limits["max_effects"], "max_effects")
return value
def _validate_grant_envelope(
envelope: Any,
owner_public_keys: Mapping[str, Ed25519PublicKey],
owner_key_scopes: Mapping[str, Mapping[str, frozenset[str]]],
) -> dict[str, Any]:
value = _plain_object(envelope, "grant envelope")
_exact_fields(value, {"schema", "payload", "signing"}, "grant envelope")
if value["schema"] != GRANT_SCHEMA:
raise AuthorizationError("grant schema is invalid")
_validate_grant_payload(value["payload"])
_verify_signature(value, owner_public_keys, trust_name="owner")
key_id = value["signing"]["key_id"]
trust_scope = owner_key_scopes[key_id]
payload = value["payload"]
if (
"*" not in trust_scope["tenant_ids"]
and payload["tenant_id"] not in trust_scope["tenant_ids"]
) or (
"*" not in trust_scope["subject_ids"]
and payload["subject_id"] not in trust_scope["subject_ids"]
):
raise AuthorizationError("grant is outside trusted Owner key scope")
return value
def grant_digest(envelope: Mapping[str, Any]) -> str:
"""Return the canonical SHA-256 digest of a signed grant envelope."""
value = _plain_object(envelope, "grant envelope")
return hashlib.sha256(_canonical_json(value)).hexdigest()
def sign_owner_delegation_grant(
owner_private_key: Ed25519PrivateKey,
owner_key_id: str,
*,
grant_id: str,
generation: int,
tenant_id: str,
subject_id: str,
mission_ids: tuple[str, ...] | list[str],
task_ids: tuple[str, ...] | list[str],
operations: tuple[str, ...] | list[str],
component_ids: tuple[str, ...] | list[str],
environments: tuple[str, ...] | list[str],
issued_at: int,
validity_mode: str,
expires_at: int | None = None,
max_effects: int | None = None,
destructive_ceiling: str = "NONE",
) -> dict[str, Any]:
"""Build and sign a grant with an explicitly supplied Owner private key.
``validity_mode`` is required so omitting an expiry cannot silently create an
until-revoked grant.
"""
if validity_mode == "UNTIL_REVOKED":
if expires_at is not None:
raise AuthorizationError("expires_at is forbidden for UNTIL_REVOKED")
validity: dict[str, Any] = {"mode": "UNTIL_REVOKED"}
elif validity_mode == "EXPIRES_AT":
if expires_at is None:
raise AuthorizationError("expires_at is required for EXPIRES_AT")
validity = {"mode": "EXPIRES_AT", "expires_at": expires_at}
else:
raise AuthorizationError("validity_mode is invalid")
scope_inputs = {
"mission_ids": mission_ids,
"task_ids": task_ids,
"operations": operations,
"component_ids": component_ids,
"environments": environments,
}
scope: dict[str, list[str]] = {}
for name, items in scope_inputs.items():
if type(items) not in (list, tuple):
raise AuthorizationError(f"{name} must be a list or tuple")
scope[name] = list(items)
limits = {} if max_effects is None else {"max_effects": max_effects}
payload = {
"grant_id": grant_id,
"generation": generation,
"tenant_id": tenant_id,
"subject_id": subject_id,
"scope": scope,
"issued_at": issued_at,
"validity": validity,
"limits": limits,
"destructive_ceiling": destructive_ceiling,
}
_validate_grant_payload(payload)
return _sign(GRANT_SCHEMA, payload, owner_private_key, owner_key_id)
def _public_key_bytes(key: Ed25519PublicKey) -> bytes:
return key.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
def _keyring(
value: Mapping[str, Ed25519PublicKey] | None, name: str
) -> dict[str, Ed25519PublicKey]:
if value is None:
return {}
if not isinstance(value, Mapping):
raise AuthorizationError(f"{name} must be a key mapping")
result: dict[str, Ed25519PublicKey] = {}
for key_id, key in value.items():
_text(key_id, f"{name} key_id")
if not isinstance(key, Ed25519PublicKey):
raise AuthorizationError(f"{name} contains a non-Ed25519 key")
result[key_id] = key
return result
def _owner_scopes(
value: Mapping[str, Mapping[str, list[str]]] | None,
owner_public_keys: Mapping[str, Ed25519PublicKey],
) -> dict[str, dict[str, frozenset[str]]]:
if type(value) is not dict or set(value) != set(owner_public_keys):
raise AuthorizationError(
"owner_key_scopes must exactly cover owner_public_keys"
)
result: dict[str, dict[str, frozenset[str]]] = {}
for key_id, raw_scope in value.items():
if type(raw_scope) is not dict or set(raw_scope) != {
"tenant_ids",
"subject_ids",
}:
raise AuthorizationError("owner_key_scopes fields are invalid")
tenants = _scope_list(
raw_scope["tenant_ids"], f"owner_key_scopes.{key_id}.tenant_ids"
)
subjects = _scope_list(
raw_scope["subject_ids"], f"owner_key_scopes.{key_id}.subject_ids"
)
result[key_id] = {
"tenant_ids": frozenset(tenants),
"subject_ids": frozenset(subjects),
}
return result
class AuthorizationService:
"""Local authorization service backed by durable SQLite state.
The caller supplies trusted Owner keys and the machine signing identity.
Revocation is disabled unless ``control_context_verifier`` validates the
authenticated local control-service context passed to :meth:`revoke_grant`.
"""
def __init__(
self,
db_path: str | os.PathLike[str],
owner_public_keys: Mapping[str, Ed25519PublicKey],
machine_private_key: Ed25519PrivateKey,
machine_key_id: str,
*,
owner_key_scopes: Mapping[str, Mapping[str, list[str]]],
clock: Callable[[], int | float] = time.time,
control_context_verifier: Callable[[object], bool] | None = None,
trusted_machine_public_keys: Mapping[str, Ed25519PublicKey] | None = None,
) -> None:
if not isinstance(machine_private_key, Ed25519PrivateKey):
raise AuthorizationError("machine_private_key must be Ed25519")
if not callable(clock):
raise AuthorizationError("clock must be callable")
if control_context_verifier is not None and not callable(control_context_verifier):
raise AuthorizationError("control_context_verifier must be callable")
self.db_path = Path(db_path)
self.owner_public_keys = _keyring(owner_public_keys, "owner_public_keys")
if not self.owner_public_keys:
raise AuthorizationError("at least one trusted owner key is required")
self.owner_key_scopes = _owner_scopes(
owner_key_scopes, self.owner_public_keys
)
self.machine_private_key = machine_private_key
self.machine_key_id = _text(machine_key_id, "machine_key_id")
self.machine_public_keys = _keyring(
trusted_machine_public_keys, "trusted_machine_public_keys"
)
current_public = machine_private_key.public_key()
existing = self.machine_public_keys.get(self.machine_key_id)
if existing is not None and _public_key_bytes(existing) != _public_key_bytes(current_public):
raise AuthorizationError("current machine key conflicts with trusted key mapping")
self.machine_public_keys[self.machine_key_id] = current_public
self.clock = clock
self.control_context_verifier = control_context_verifier
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._initialize()
@staticmethod
def grant_digest(envelope: Mapping[str, Any]) -> str:
return grant_digest(envelope)
def _now(self) -> int:
value = self.clock()
if type(value) is int:
now = value
elif type(value) is float and math.isfinite(value):
now = math.floor(value)
else:
raise AuthorizationError("clock must return a finite timestamp")
if now <= 0:
raise AuthorizationError("clock returned an invalid timestamp")
return now
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.db_path, timeout=30, isolation_level=None)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
connection.execute("PRAGMA busy_timeout = 30000")
return connection
def _initialize(self) -> None:
with self._connect() as connection:
connection.executescript(
"""
PRAGMA journal_mode = WAL;
CREATE TABLE IF NOT EXISTS authorization_grants (
grant_id TEXT PRIMARY KEY,
generation INTEGER NOT NULL CHECK (generation > 0),
digest TEXT NOT NULL,
envelope_json TEXT NOT NULL,
revoked INTEGER NOT NULL CHECK (revoked IN (0, 1)),
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS authorization_revocations (
grant_id TEXT NOT NULL,
generation INTEGER NOT NULL CHECK (generation > 0),
revoked_at INTEGER NOT NULL,
PRIMARY KEY (grant_id, generation)
);
CREATE TABLE IF NOT EXISTS authorization_effect_reservations (
grant_id TEXT NOT NULL,
grant_generation INTEGER NOT NULL CHECK (grant_generation > 0),
effect_id TEXT NOT NULL,
reserved_at INTEGER NOT NULL,
PRIMARY KEY (grant_id, grant_generation, effect_id)
);
"""
)
def register_grant(self, signed_envelope: dict[str, Any]) -> dict[str, Any]:
envelope = _json_copy(
_validate_grant_envelope(
signed_envelope, self.owner_public_keys, self.owner_key_scopes
)
)
payload = envelope["payload"]
now = self._now()
self._check_grant_time(payload, now)
digest = grant_digest(envelope)
encoded = _canonical_json(envelope).decode("utf-8")
with self._connect() as connection:
try:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute(
"SELECT generation, digest, revoked FROM authorization_grants WHERE grant_id = ?",
(payload["grant_id"],),
).fetchone()
if row is None:
connection.execute(
"INSERT INTO authorization_grants "
"(grant_id, generation, digest, envelope_json, revoked, updated_at) "
"VALUES (?, ?, ?, ?, 0, ?)",
(
payload["grant_id"],
payload["generation"],
digest,
encoded,
now,
),
)
elif payload["generation"] < row["generation"]:
raise AuthorizationError("grant generation is stale")
elif payload["generation"] == row["generation"]:
if digest != row["digest"]:
raise AuthorizationError("grant generation conflicts with durable state")
if row["revoked"]:
raise AuthorizationError("grant is revoked")
else:
connection.execute(
"UPDATE authorization_grants SET generation = ?, digest = ?, "
"envelope_json = ?, revoked = 0, updated_at = ? WHERE grant_id = ?",
(
payload["generation"],
digest,
encoded,
now,
payload["grant_id"],
),
)
connection.commit()
except Exception:
connection.rollback()
raise
return _json_copy(envelope)
@staticmethod
def _check_grant_time(payload: dict[str, Any], now: int) -> None:
if payload["issued_at"] > now:
raise AuthorizationError("grant is not yet valid")
validity = payload["validity"]
if validity["mode"] == "EXPIRES_AT" and now >= validity["expires_at"]:
raise AuthorizationError("grant has expired")
def _load_active_grant(
self, connection: sqlite3.Connection, grant_id: str, now: int
) -> tuple[dict[str, Any], str, int]:
row = connection.execute(
"SELECT generation, digest, envelope_json, revoked "
"FROM authorization_grants WHERE grant_id = ?",
(grant_id,),
).fetchone()
if row is None:
raise AuthorizationError("grant is not registered")
if row["revoked"]:
raise AuthorizationError("grant is revoked")
try:
decoded = json.loads(row["envelope_json"])
except (TypeError, json.JSONDecodeError) as exc:
raise AuthorizationError("durable grant state is invalid") from exc
envelope = _validate_grant_envelope(
decoded, self.owner_public_keys, self.owner_key_scopes
)
payload = envelope["payload"]
digest = grant_digest(envelope)
if (
payload["grant_id"] != grant_id
or payload["generation"] != row["generation"]
or digest != row["digest"]
):
raise AuthorizationError("durable grant state failed integrity validation")
self._check_grant_time(payload, now)
return envelope, digest, row["generation"]
@staticmethod
def _bindings(
*,
tenant_id: str,
subject_id: str,
mission_id: str,
task_id: str,
operation: str,
component_id: str,
environment: str,
) -> dict[str, str]:
values = {
"tenant_id": tenant_id,
"subject_id": subject_id,
"mission_id": mission_id,
"task_id": task_id,
"operation": operation,
"component_id": component_id,
"environment": environment,
}
for field, value in values.items():
_text(value, field)
return values
@staticmethod
def _check_narrowing(grant: dict[str, Any], bindings: dict[str, str]) -> None:
payload = grant["payload"]
if bindings["tenant_id"] != payload["tenant_id"] or bindings["subject_id"] != payload["subject_id"]:
raise AuthorizationError("requested identity is outside grant scope")
scope_names = {
"mission_id": "mission_ids",
"task_id": "task_ids",
"operation": "operations",
"component_id": "component_ids",
"environment": "environments",
}
for binding_name, scope_name in scope_names.items():
allowed = payload["scope"][scope_name]
if allowed != ["*"] and bindings[binding_name] not in allowed:
raise AuthorizationError("requested value is outside grant scope")
def issue_principal(
self,
grant_id: str,
*,
tenant_id: str,
subject_id: str,
mission_id: str,
task_id: str,
operation: str,
component_id: str,
environment: str,
ttl_seconds: int,
) -> dict[str, Any]:
_text(grant_id, "grant_id")
if type(ttl_seconds) is not int or not (1 <= ttl_seconds <= MAX_PRINCIPAL_TTL_SECONDS):
raise AuthorizationError("ttl_seconds is invalid")
bindings = self._bindings(
tenant_id=tenant_id,
subject_id=subject_id,
mission_id=mission_id,
task_id=task_id,
operation=operation,
component_id=component_id,
environment=environment,
)
now = self._now()
with self._connect() as connection:
root, digest, generation = self._load_active_grant(connection, grant_id, now)
self._check_narrowing(root, bindings)
expires_at = now + ttl_seconds
validity = root["payload"]["validity"]
if validity["mode"] == "EXPIRES_AT":
expires_at = min(expires_at, validity["expires_at"])
if expires_at <= now:
raise AuthorizationError("grant has expired")
root_binding = {
"root_grant_id": grant_id,
"root_grant_digest": digest,
"root_grant_generation": generation,
}
epoch_payload = {
"epoch_id": str(uuid.uuid4()),
**root_binding,
**bindings,
"issued_at": now,
"expires_at": expires_at,
}
epoch = _sign(
EPOCH_SCHEMA,
epoch_payload,
self.machine_private_key,
self.machine_key_id,
)
principal_payload = {
"credential_id": str(uuid.uuid4()),
"nonce": secrets.token_hex(24),
"parent_epoch_digest": hashlib.sha256(_canonical_json(epoch)).hexdigest(),
**root_binding,
**bindings,
"issued_at": now,
"expires_at": expires_at,
}
return _sign(
PRINCIPAL_SCHEMA,
principal_payload,
self.machine_private_key,
self.machine_key_id,
extra={"operational_epoch": epoch},
)
@staticmethod
def _validate_chain_payload(
payload: Any, *, principal: bool
) -> dict[str, Any]:
value = _plain_object(
payload, "principal payload" if principal else "operational epoch payload"
)
unique = {"credential_id", "nonce", "parent_epoch_digest"} if principal else {"epoch_id"}
expected = unique | set(_ROOT_FIELDS) | set(_BINDING_FIELDS) | {
"issued_at",
"expires_at",
}
_exact_fields(
value,
expected,
"principal payload" if principal else "operational epoch payload",
)
for field in _BINDING_FIELDS:
_text(value[field], field)
_text(value["root_grant_id"], "root_grant_id")
if type(value["root_grant_digest"]) is not str or not _DIGEST_RE.fullmatch(
value["root_grant_digest"]
):
raise AuthorizationError("root_grant_digest is invalid")
_positive_int(value["root_grant_generation"], "root_grant_generation")
issued = _timestamp(value["issued_at"], "issued_at")
expires = _timestamp(value["expires_at"], "expires_at")
if expires <= issued or expires - issued > MAX_PRINCIPAL_TTL_SECONDS:
raise AuthorizationError("credential validity interval is invalid")
if principal:
_text(value["credential_id"], "credential_id")
_text(value["nonce"], "nonce")
if type(value["parent_epoch_digest"]) is not str or not _DIGEST_RE.fullmatch(
value["parent_epoch_digest"]
):
raise AuthorizationError("parent_epoch_digest is invalid")
else:
_text(value["epoch_id"], "epoch_id")
return value
def _verify_epoch(self, epoch: Any, now: int) -> dict[str, Any]:
value = _plain_object(epoch, "operational epoch")
_exact_fields(value, {"schema", "payload", "signing"}, "operational epoch")
if value["schema"] != EPOCH_SCHEMA:
raise AuthorizationError("operational epoch schema is invalid")
payload = self._validate_chain_payload(value["payload"], principal=False)
_verify_signature(value, self.machine_public_keys, trust_name="machine")
if payload["issued_at"] > now:
raise AuthorizationError("operational epoch is not yet valid")
if now >= payload["expires_at"]:
raise AuthorizationError("operational epoch has expired")
return value
def _verify_principal(self, principal: Any, now: int) -> dict[str, Any]:
value = _plain_object(principal, "task principal")
_exact_fields(
value,
{"schema", "payload", "operational_epoch", "signing"},
"task principal",
)
if value["schema"] != PRINCIPAL_SCHEMA:
raise AuthorizationError("task principal schema is invalid")
epoch = self._verify_epoch(value["operational_epoch"], now)
payload = self._validate_chain_payload(value["payload"], principal=True)
_verify_signature(value, self.machine_public_keys, trust_name="machine")
if payload["issued_at"] > now:
raise AuthorizationError("task principal is not yet valid")
if now >= payload["expires_at"]:
raise AuthorizationError("task principal has expired")
expected_parent = hashlib.sha256(_canonical_json(epoch)).hexdigest()
if payload["parent_epoch_digest"] != expected_parent:
raise AuthorizationError("task principal parent digest is invalid")
epoch_payload = epoch["payload"]
for field in (*_ROOT_FIELDS, *_BINDING_FIELDS, "issued_at", "expires_at"):
if payload[field] != epoch_payload[field]:
raise AuthorizationError("task principal does not match parent epoch")
return value
def _authorize_with_connection(
self,
connection: sqlite3.Connection,
principal: dict[str, Any],
bindings: dict[str, str],
now: int,
) -> dict[str, Any]:
credential = self._verify_principal(principal, now)
payload = credential["payload"]
for field, expected in bindings.items():
if payload[field] != expected:
raise AuthorizationError("task principal scope does not match request")
root, digest, generation = self._load_active_grant(
connection, payload["root_grant_id"], now
)
if payload["root_grant_generation"] != generation:
raise AuthorizationError("task principal root generation is stale")
if payload["root_grant_digest"] != digest:
raise AuthorizationError("task principal root digest is stale")
self._check_narrowing(root, bindings)
return root
def authorize(
self,
principal: dict[str, Any],
*,
tenant_id: str,
subject_id: str,
mission_id: str,
task_id: str,
operation: str,
component_id: str,
environment: str,
) -> dict[str, Any]:
bindings = self._bindings(
tenant_id=tenant_id,
subject_id=subject_id,
mission_id=mission_id,
task_id=task_id,
operation=operation,
component_id=component_id,
environment=environment,
)
now = self._now()
with self._connect() as connection:
root = self._authorize_with_connection(connection, principal, bindings, now)
return _json_copy(root)
def assert_effect_bounds(
self,
principal: dict[str, Any],
operation_definition: Any,
*,
tenant_id: str,
subject_id: str,
mission_id: str,
task_id: str,
operation: str,
component_id: str,
environment: str,
) -> dict[str, Any]:
"""Authorize a trusted operation definition against the signed ceiling.
This gate limits effect class; it does not replace the separately
required Job authorization, intent commit, claim lease, receiver fence,
readback, or reconciliation checks for a mutating operation.
"""
from .operations import OperationDefinition
if not isinstance(operation_definition, OperationDefinition):
raise AuthorizationError("trusted OperationDefinition is required")
if operation_definition.operation_id != operation:
raise AuthorizationError("operation definition does not match authorization scope")
root = self.authorize(
principal,
tenant_id=tenant_id,
subject_id=subject_id,
mission_id=mission_id,
task_id=task_id,
operation=operation,
component_id=component_id,
environment=environment,
)
ceiling = root["payload"]["destructive_ceiling"]
effect_class = operation_definition.effect_class
if ceiling == "NONE" and effect_class != "READ_ONLY":
raise AuthorizationError("operation exceeds grant destructive ceiling")
if ceiling == "REVERSIBLE" and effect_class == "IRREVERSIBLE":
raise AuthorizationError("irreversible operation exceeds grant destructive ceiling")
if ceiling == "REVERSIBLE" and effect_class != "READ_ONLY":
if operation_definition.rollback.get("mode") not in {"OPERATION", "COMPENSATE"}:
raise AuthorizationError("reversible ceiling requires a rollback policy")
if (
operation_definition.readback.get("mode") not in {"STATE", "EXACT"}
or operation_definition.readback.get("required") is not True
or operation_definition.reconcile.get("mode") == "NONE"
):
raise AuthorizationError(
"reversible ceiling requires readback and reconciliation"
)
return root
def reserve_effect(
self,
grant_id: str,
effect_id: str,
principal: dict[str, Any],
*,
tenant_id: str,
subject_id: str,
mission_id: str,
task_id: str,
operation: str,
component_id: str,
environment: str,
) -> bool:
"""Atomically reserve one stable effect identifier against a grant budget.
Returns ``False`` for an already-reserved identifier, so intent retries do
not consume the bounded grant twice. Grants without ``max_effects`` leave
mission-level budget enforcement to their caller.
"""
_text(grant_id, "grant_id")
_text(effect_id, "effect_id")
bindings = self._bindings(
tenant_id=tenant_id,
subject_id=subject_id,
mission_id=mission_id,
task_id=task_id,
operation=operation,
component_id=component_id,
environment=environment,
)
now = self._now()
with self._connect() as connection:
try:
connection.execute("BEGIN IMMEDIATE")
root = self._authorize_with_connection(connection, principal, bindings, now)
payload = principal["payload"]
if payload["root_grant_id"] != grant_id:
raise AuthorizationError("effect reservation grant does not match principal")
generation = payload["root_grant_generation"]
existing = connection.execute(
"SELECT 1 FROM authorization_effect_reservations "
"WHERE grant_id = ? AND grant_generation = ? AND effect_id = ?",
(grant_id, generation, effect_id),
).fetchone()
if existing is not None:
connection.commit()
return False
max_effects = root["payload"]["limits"].get("max_effects")
if max_effects is not None:
count = connection.execute(
"SELECT COUNT(*) FROM authorization_effect_reservations "
"WHERE grant_id = ? AND grant_generation = ?",
(grant_id, generation),
).fetchone()[0]
if count >= max_effects:
raise AuthorizationError("grant effect budget is exhausted")
connection.execute(
"INSERT INTO authorization_effect_reservations "
"(grant_id, grant_generation, effect_id, reserved_at) VALUES (?, ?, ?, ?)",
(grant_id, generation, effect_id, now),
)
connection.commit()
return True
except Exception:
connection.rollback()
raise
def revoke_grant(self, grant_id: str, *, control_context: object) -> int:
"""Revoke a grant from an authenticated local control-service context."""
_text(grant_id, "grant_id")
verifier = self.control_context_verifier
trusted = False
if verifier is not None:
try:
trusted = verifier(control_context) is True
except Exception:
trusted = False
if not trusted:
raise AuthorizationError("trusted control context is required for revocation")
now = self._now()
with self._connect() as connection:
try:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute(
"SELECT generation, revoked FROM authorization_grants WHERE grant_id = ?",
(grant_id,),
).fetchone()
if row is None:
raise AuthorizationError("grant is not registered")
if row["revoked"]:
connection.commit()
return row["generation"]
generation = row["generation"] + 1
connection.execute(
"UPDATE authorization_grants SET generation = ?, revoked = 1, "
"updated_at = ? WHERE grant_id = ?",
(generation, now, grant_id),
)
connection.execute(
"INSERT INTO authorization_revocations "
"(grant_id, generation, revoked_at) VALUES (?, ?, ?)",
(grant_id, generation, now),
)
connection.commit()
return generation
except Exception:
connection.rollback()
raise
__all__ = [
"AuthorizationError",
"AuthorizationService",
"DESTRUCTIVE_CEILINGS",
"EPOCH_SCHEMA",
"GRANT_SCHEMA",
"MAX_PRINCIPAL_TTL_SECONDS",
"PRINCIPAL_SCHEMA",
"grant_digest",
"sign_owner_delegation_grant",
]