745 lines
29 KiB
Python
745 lines
29 KiB
Python
"""AIOS V2 最小权限 Run Provenance 子 Grant。
|
||
|
||
父 Workflow Provenance 只绑定 run/tenant/task/agent/principal 身份,不直接授予
|
||
操作权限。每个子 Grant 只允许一个 ExecutionPath;Git Grant 还必须限定具体
|
||
GitOperation。子 Grant 具备独立 TTL、nonce、状态、撤销和重放防护。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import sqlite3
|
||
import threading
|
||
import uuid
|
||
from dataclasses import asdict, dataclass, field, replace
|
||
from datetime import datetime, timedelta, timezone
|
||
from enum import Enum
|
||
from pathlib import Path
|
||
from typing import Any, Mapping, Sequence
|
||
|
||
from .provenance import (
|
||
ExecutionPath,
|
||
GitOperation,
|
||
ProvenanceDenied,
|
||
ProvenanceExpired,
|
||
ProvenanceIssuer,
|
||
ProvenanceNotFound,
|
||
ProvenanceReplay,
|
||
ProvenanceRevoked,
|
||
ProvenanceSignatureError,
|
||
ProvenanceStatus,
|
||
ProvenanceVerifier,
|
||
RunProvenance,
|
||
SQLiteProvenanceStore,
|
||
Signer,
|
||
SignatureVerifier,
|
||
_validate_digest,
|
||
canonical_json_bytes,
|
||
parse_timestamp,
|
||
required,
|
||
utc_now,
|
||
)
|
||
|
||
WORKFLOW_POLICY_VERSION = "aios.v2.workflow.v1"
|
||
GRANT_SCHEMA_VERSION = "aios.v2.provenance-grant.v1"
|
||
|
||
|
||
class GrantType(str, Enum):
|
||
CODE_CHANGE = "code_change"
|
||
GIT = "git"
|
||
DEPLOYMENT = "deployment"
|
||
RUNTIME = "runtime"
|
||
EVIDENCE = "evidence"
|
||
ROLLBACK = "rollback"
|
||
AUDIT = "audit"
|
||
|
||
|
||
GRANT_PATHS: Mapping[GrantType, ExecutionPath] = {
|
||
GrantType.CODE_CHANGE: ExecutionPath.CODE_CHANGE,
|
||
GrantType.GIT: ExecutionPath.GIT_OPERATION,
|
||
GrantType.DEPLOYMENT: ExecutionPath.DEPLOYMENT,
|
||
GrantType.RUNTIME: ExecutionPath.RUNTIME_EXECUTE,
|
||
GrantType.EVIDENCE: ExecutionPath.EVIDENCE_WRITE,
|
||
GrantType.ROLLBACK: ExecutionPath.ROLLBACK,
|
||
GrantType.AUDIT: ExecutionPath.AUDIT,
|
||
}
|
||
|
||
|
||
class GrantStatus(str, Enum):
|
||
ISSUED = "issued"
|
||
VERIFIED = "verified"
|
||
USED = "used"
|
||
EXPIRED = "expired"
|
||
REVOKED = "revoked"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class ProvenanceGrant:
|
||
grant_id: str
|
||
parent_provenance_id: str
|
||
grant_type: GrantType
|
||
execution_path: ExecutionPath
|
||
run_id: str
|
||
issuer: str
|
||
tenant_id: str
|
||
task_id: str
|
||
agent_id: str
|
||
principal_id: str
|
||
allowed_git_operations: Sequence[str]
|
||
issued_at: str
|
||
expires_at: str
|
||
policy_version: str
|
||
key_id: str
|
||
nonce: str
|
||
signature: str = ""
|
||
schema_version: str = GRANT_SCHEMA_VERSION
|
||
|
||
def __post_init__(self) -> None:
|
||
for name in (
|
||
"grant_id",
|
||
"parent_provenance_id",
|
||
"run_id",
|
||
"issuer",
|
||
"tenant_id",
|
||
"task_id",
|
||
"agent_id",
|
||
"principal_id",
|
||
"issued_at",
|
||
"expires_at",
|
||
"policy_version",
|
||
"key_id",
|
||
"nonce",
|
||
"schema_version",
|
||
):
|
||
object.__setattr__(self, name, required(getattr(self, name), name))
|
||
if self.schema_version != GRANT_SCHEMA_VERSION:
|
||
raise ValueError(f"unsupported grant schema: {self.schema_version}")
|
||
grant_type = GrantType(self.grant_type)
|
||
path = ExecutionPath(self.execution_path)
|
||
if GRANT_PATHS[grant_type] != path:
|
||
raise ValueError("grant_type does not match execution_path")
|
||
object.__setattr__(self, "grant_type", grant_type)
|
||
object.__setattr__(self, "execution_path", path)
|
||
operations = tuple(
|
||
sorted({GitOperation(item).value for item in self.allowed_git_operations})
|
||
)
|
||
if grant_type == GrantType.GIT and not operations:
|
||
raise ValueError("Git Grant requires explicit Git operations")
|
||
if grant_type != GrantType.GIT and operations:
|
||
raise ValueError("only Git Grant may contain Git operations")
|
||
object.__setattr__(self, "allowed_git_operations", operations)
|
||
issued = parse_timestamp(self.issued_at)
|
||
expires = parse_timestamp(self.expires_at)
|
||
if expires <= issued:
|
||
raise ValueError("grant expires_at must be later than issued_at")
|
||
object.__setattr__(self, "signature", str(self.signature or "").strip())
|
||
|
||
def unsigned_dict(self) -> dict[str, Any]:
|
||
return {
|
||
"schema_version": self.schema_version,
|
||
"grant_id": self.grant_id,
|
||
"parent_provenance_id": self.parent_provenance_id,
|
||
"grant_type": self.grant_type.value,
|
||
"execution_path": self.execution_path.value,
|
||
"run_id": self.run_id,
|
||
"issuer": self.issuer,
|
||
"tenant_id": self.tenant_id,
|
||
"task_id": self.task_id,
|
||
"agent_id": self.agent_id,
|
||
"principal_id": self.principal_id,
|
||
"allowed_git_operations": list(self.allowed_git_operations),
|
||
"issued_at": self.issued_at,
|
||
"expires_at": self.expires_at,
|
||
"policy_version": self.policy_version,
|
||
"key_id": self.key_id,
|
||
"nonce": self.nonce,
|
||
}
|
||
|
||
def canonical_bytes(self) -> bytes:
|
||
return canonical_json_bytes(self.unsigned_dict())
|
||
|
||
def as_dict(self) -> dict[str, Any]:
|
||
value = self.unsigned_dict()
|
||
value["signature"] = self.signature
|
||
return value
|
||
|
||
@classmethod
|
||
def from_dict(cls, value: Mapping[str, Any]) -> "ProvenanceGrant":
|
||
return cls(**dict(value))
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class GrantAuthorization:
|
||
authorization_id: str
|
||
grant_id: str
|
||
parent_provenance_id: str
|
||
run_id: str
|
||
tenant_id: str
|
||
task_id: str
|
||
agent_id: str
|
||
principal_id: str
|
||
execution_path: str
|
||
git_operation: str | None
|
||
request_digest: str
|
||
policy_version: str
|
||
key_id: str
|
||
public_key_fingerprint: str
|
||
authorized_at: str = field(default_factory=utc_now)
|
||
|
||
@property
|
||
def provenance_id(self) -> str:
|
||
return self.parent_provenance_id
|
||
|
||
def as_dict(self) -> dict[str, Any]:
|
||
return asdict(self)
|
||
|
||
|
||
class WorkflowProvenanceIssuer:
|
||
"""签发不携带操作权限的父 Workflow Provenance。"""
|
||
|
||
def __init__(self, signer: Signer, store: SQLiteProvenanceStore) -> None:
|
||
self._issuer = ProvenanceIssuer(signer, store)
|
||
|
||
def issue(
|
||
self,
|
||
*,
|
||
run_id: str,
|
||
issuer: str,
|
||
tenant_id: str,
|
||
task_id: str,
|
||
agent_id: str,
|
||
principal_id: str,
|
||
ttl_seconds: int = 14400,
|
||
provenance_id: str | None = None,
|
||
nonce: str | None = None,
|
||
now: datetime | None = None,
|
||
) -> RunProvenance:
|
||
return self._issuer.issue(
|
||
run_id=run_id,
|
||
issuer=issuer,
|
||
tenant_id=tenant_id,
|
||
task_id=task_id,
|
||
agent_id=agent_id,
|
||
principal_id=principal_id,
|
||
allowed_paths=(),
|
||
allowed_git_operations=(),
|
||
ttl_seconds=ttl_seconds,
|
||
policy_version=WORKFLOW_POLICY_VERSION,
|
||
provenance_id=provenance_id,
|
||
nonce=nonce,
|
||
now=now,
|
||
)
|
||
|
||
|
||
class SQLiteGrantStore:
|
||
"""在 Provenance SQLite 数据库中持久化子 Grant 与使用记录。"""
|
||
|
||
def __init__(self, path: str | Path) -> None:
|
||
self.path = Path(path).resolve()
|
||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||
self._lock = threading.RLock()
|
||
self._connection = sqlite3.connect(
|
||
str(self.path), timeout=30.0, check_same_thread=False
|
||
)
|
||
self._connection.row_factory = sqlite3.Row
|
||
with self._lock:
|
||
self._connection.execute("PRAGMA journal_mode=WAL")
|
||
self._connection.execute("PRAGMA synchronous=FULL")
|
||
self._connection.execute("PRAGMA foreign_keys=ON")
|
||
self._connection.executescript(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS provenance_grants (
|
||
grant_id TEXT PRIMARY KEY,
|
||
parent_provenance_id TEXT NOT NULL,
|
||
grant_type TEXT NOT NULL,
|
||
execution_path TEXT NOT NULL,
|
||
payload_json TEXT NOT NULL,
|
||
status TEXT NOT NULL,
|
||
signature_verified INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_grants_parent
|
||
ON provenance_grants(parent_provenance_id, grant_type, status);
|
||
CREATE TABLE IF NOT EXISTS provenance_grant_usage (
|
||
authorization_id TEXT PRIMARY KEY,
|
||
grant_id TEXT NOT NULL,
|
||
execution_path TEXT NOT NULL,
|
||
git_operation TEXT NOT NULL DEFAULT '',
|
||
request_digest TEXT NOT NULL,
|
||
authorized_at TEXT NOT NULL,
|
||
receipt_json TEXT NOT NULL,
|
||
UNIQUE(grant_id, execution_path, git_operation, request_digest),
|
||
FOREIGN KEY(grant_id) REFERENCES provenance_grants(grant_id)
|
||
);
|
||
CREATE TABLE IF NOT EXISTS provenance_grant_events (
|
||
event_id TEXT PRIMARY KEY,
|
||
grant_id TEXT NOT NULL,
|
||
event_type TEXT NOT NULL,
|
||
details_json TEXT NOT NULL,
|
||
created_at TEXT NOT NULL,
|
||
FOREIGN KEY(grant_id) REFERENCES provenance_grants(grant_id)
|
||
);
|
||
"""
|
||
)
|
||
self._connection.commit()
|
||
|
||
def close(self) -> None:
|
||
with self._lock:
|
||
self._connection.close()
|
||
|
||
def _event(
|
||
self,
|
||
grant_id: str,
|
||
event_type: str,
|
||
details: Mapping[str, Any] | None = None,
|
||
) -> None:
|
||
self._connection.execute(
|
||
"INSERT INTO provenance_grant_events VALUES (?, ?, ?, ?, ?)",
|
||
(
|
||
f"gevt_{uuid.uuid4().hex}",
|
||
grant_id,
|
||
event_type,
|
||
json.dumps(dict(details or {}), ensure_ascii=False, sort_keys=True),
|
||
utc_now(),
|
||
),
|
||
)
|
||
|
||
def save_issued(self, grant: ProvenanceGrant) -> None:
|
||
payload = json.dumps(
|
||
grant.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||
)
|
||
now = utc_now()
|
||
with self._lock:
|
||
self._connection.execute("BEGIN IMMEDIATE")
|
||
try:
|
||
self._connection.execute(
|
||
"INSERT INTO provenance_grants VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)",
|
||
(
|
||
grant.grant_id,
|
||
grant.parent_provenance_id,
|
||
grant.grant_type.value,
|
||
grant.execution_path.value,
|
||
payload,
|
||
GrantStatus.ISSUED.value,
|
||
now,
|
||
now,
|
||
),
|
||
)
|
||
self._event(grant.grant_id, "issued")
|
||
self._connection.commit()
|
||
except sqlite3.IntegrityError as exc:
|
||
self._connection.rollback()
|
||
raise ProvenanceDenied("grant_id already exists") from exc
|
||
except Exception:
|
||
self._connection.rollback()
|
||
raise
|
||
|
||
def get(self, grant_id: str) -> ProvenanceGrant:
|
||
with self._lock:
|
||
row = self._connection.execute(
|
||
"SELECT payload_json FROM provenance_grants WHERE grant_id = ?",
|
||
(required(grant_id, "grant_id"),),
|
||
).fetchone()
|
||
if row is None:
|
||
raise ProvenanceNotFound("provenance grant not found")
|
||
value = json.loads(row["payload_json"])
|
||
if not isinstance(value, Mapping):
|
||
raise ProvenanceDenied("stored grant is invalid")
|
||
return ProvenanceGrant.from_dict(value)
|
||
|
||
def status(self, grant_id: str) -> GrantStatus:
|
||
with self._lock:
|
||
row = self._connection.execute(
|
||
"SELECT status FROM provenance_grants WHERE grant_id = ?",
|
||
(required(grant_id, "grant_id"),),
|
||
).fetchone()
|
||
if row is None:
|
||
raise ProvenanceNotFound("provenance grant not found")
|
||
return GrantStatus(row["status"])
|
||
|
||
def mark_verified(self, grant_id: str, *, fingerprint: str) -> None:
|
||
with self._lock:
|
||
self._connection.execute("BEGIN IMMEDIATE")
|
||
try:
|
||
current = self.status(grant_id)
|
||
if current in {GrantStatus.REVOKED, GrantStatus.EXPIRED}:
|
||
raise ProvenanceDenied(f"cannot verify grant in state {current.value}")
|
||
next_status = (
|
||
GrantStatus.USED.value
|
||
if current == GrantStatus.USED
|
||
else GrantStatus.VERIFIED.value
|
||
)
|
||
self._connection.execute(
|
||
"UPDATE provenance_grants SET status = ?, signature_verified = 1, updated_at = ? WHERE grant_id = ?",
|
||
(next_status, utc_now(), grant_id),
|
||
)
|
||
self._event(
|
||
grant_id,
|
||
"verified",
|
||
{"public_key_fingerprint": fingerprint},
|
||
)
|
||
self._connection.commit()
|
||
except Exception:
|
||
self._connection.rollback()
|
||
raise
|
||
|
||
def mark_expired(self, grant_id: str) -> None:
|
||
with self._lock:
|
||
self._connection.execute("BEGIN IMMEDIATE")
|
||
try:
|
||
if self.status(grant_id) == GrantStatus.REVOKED:
|
||
self._connection.rollback()
|
||
return
|
||
self._connection.execute(
|
||
"UPDATE provenance_grants SET status = ?, updated_at = ? WHERE grant_id = ?",
|
||
(GrantStatus.EXPIRED.value, utc_now(), grant_id),
|
||
)
|
||
self._event(grant_id, "expired")
|
||
self._connection.commit()
|
||
except Exception:
|
||
self._connection.rollback()
|
||
raise
|
||
|
||
def revoke(self, grant_id: str, *, reason: str, principal_id: str) -> None:
|
||
with self._lock:
|
||
self._connection.execute("BEGIN IMMEDIATE")
|
||
try:
|
||
self.get(grant_id)
|
||
self._connection.execute(
|
||
"UPDATE provenance_grants SET status = ?, updated_at = ? WHERE grant_id = ?",
|
||
(GrantStatus.REVOKED.value, utc_now(), grant_id),
|
||
)
|
||
self._event(
|
||
grant_id,
|
||
"revoked",
|
||
{
|
||
"reason": required(reason, "reason"),
|
||
"principal_id": required(principal_id, "principal_id"),
|
||
},
|
||
)
|
||
self._connection.commit()
|
||
except Exception:
|
||
self._connection.rollback()
|
||
raise
|
||
|
||
def record_authorization(self, authorization: GrantAuthorization) -> None:
|
||
receipt = json.dumps(
|
||
authorization.as_dict(), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
||
)
|
||
with self._lock:
|
||
self._connection.execute("BEGIN IMMEDIATE")
|
||
try:
|
||
current = self.status(authorization.grant_id)
|
||
if current in {GrantStatus.REVOKED, GrantStatus.EXPIRED}:
|
||
raise ProvenanceDenied(f"grant state is {current.value}")
|
||
try:
|
||
self._connection.execute(
|
||
"INSERT INTO provenance_grant_usage VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||
(
|
||
authorization.authorization_id,
|
||
authorization.grant_id,
|
||
authorization.execution_path,
|
||
authorization.git_operation or "",
|
||
authorization.request_digest,
|
||
authorization.authorized_at,
|
||
receipt,
|
||
),
|
||
)
|
||
except sqlite3.IntegrityError as exc:
|
||
raise ProvenanceReplay(
|
||
"identical grant path/operation/request digest already used"
|
||
) from exc
|
||
self._connection.execute(
|
||
"UPDATE provenance_grants SET status = ?, updated_at = ? WHERE grant_id = ?",
|
||
(GrantStatus.USED.value, authorization.authorized_at, authorization.grant_id),
|
||
)
|
||
self._event(
|
||
authorization.grant_id,
|
||
"used",
|
||
{
|
||
"authorization_id": authorization.authorization_id,
|
||
"execution_path": authorization.execution_path,
|
||
"git_operation": authorization.git_operation,
|
||
"request_digest": authorization.request_digest,
|
||
},
|
||
)
|
||
self._connection.commit()
|
||
except Exception:
|
||
self._connection.rollback()
|
||
raise
|
||
|
||
def list_for_parent(self, parent_provenance_id: str) -> list[ProvenanceGrant]:
|
||
with self._lock:
|
||
rows = self._connection.execute(
|
||
"SELECT payload_json FROM provenance_grants WHERE parent_provenance_id = ? ORDER BY grant_type, grant_id",
|
||
(required(parent_provenance_id, "parent_provenance_id"),),
|
||
).fetchall()
|
||
return [ProvenanceGrant.from_dict(json.loads(row["payload_json"])) for row in rows]
|
||
|
||
|
||
class GrantIssuer:
|
||
"""从父 Workflow Provenance 派生单一路径最小权限 Grant。"""
|
||
|
||
def __init__(
|
||
self,
|
||
signer: Signer,
|
||
parent_store: SQLiteProvenanceStore,
|
||
grant_store: SQLiteGrantStore,
|
||
) -> None:
|
||
self.signer = signer
|
||
self.parent_store = parent_store
|
||
self.grant_store = grant_store
|
||
|
||
def issue(
|
||
self,
|
||
*,
|
||
parent_provenance_id: str,
|
||
grant_type: GrantType | str,
|
||
ttl_seconds: int = 900,
|
||
allowed_git_operations: Sequence[str] = (),
|
||
grant_id: str | None = None,
|
||
nonce: str | None = None,
|
||
now: datetime | None = None,
|
||
) -> ProvenanceGrant:
|
||
if ttl_seconds <= 0:
|
||
raise ValueError("grant ttl_seconds must be positive")
|
||
parent = self.parent_store.get(parent_provenance_id)
|
||
if parent.policy_version != WORKFLOW_POLICY_VERSION:
|
||
raise ProvenanceDenied("child grants require a workflow provenance parent")
|
||
if parent.allowed_paths or parent.allowed_git_operations:
|
||
raise ProvenanceDenied("workflow provenance parent must not contain operation rights")
|
||
parent_status = self.parent_store.status(parent.provenance_id)
|
||
if parent_status == ProvenanceStatus.REVOKED:
|
||
raise ProvenanceRevoked("workflow provenance is revoked")
|
||
issued = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
||
parent_expiry = parse_timestamp(parent.expires_at)
|
||
expires = min(issued + timedelta(seconds=ttl_seconds), parent_expiry)
|
||
if expires <= issued:
|
||
raise ProvenanceExpired("workflow provenance has expired")
|
||
normalized_type = GrantType(grant_type)
|
||
operations = tuple(allowed_git_operations)
|
||
unsigned = ProvenanceGrant(
|
||
grant_id=grant_id or f"grant_{uuid.uuid4().hex}",
|
||
parent_provenance_id=parent.provenance_id,
|
||
grant_type=normalized_type,
|
||
execution_path=GRANT_PATHS[normalized_type],
|
||
run_id=parent.run_id,
|
||
issuer=parent.issuer,
|
||
tenant_id=parent.tenant_id,
|
||
task_id=parent.task_id,
|
||
agent_id=parent.agent_id,
|
||
principal_id=parent.principal_id,
|
||
allowed_git_operations=operations,
|
||
issued_at=issued.isoformat().replace("+00:00", "Z"),
|
||
expires_at=expires.isoformat().replace("+00:00", "Z"),
|
||
policy_version=f"{WORKFLOW_POLICY_VERSION}.{normalized_type.value}",
|
||
key_id=self.signer.key_id,
|
||
nonce=nonce or uuid.uuid4().hex,
|
||
)
|
||
signed = replace(unsigned, signature=self.signer.sign(unsigned.canonical_bytes()))
|
||
self.grant_store.save_issued(signed)
|
||
return signed
|
||
|
||
|
||
class GrantVerifier:
|
||
"""验证父 Workflow 与子 Grant,确保单一路径最小权限。"""
|
||
|
||
def __init__(
|
||
self,
|
||
parent_store: SQLiteProvenanceStore,
|
||
grant_store: SQLiteGrantStore,
|
||
verifiers: Mapping[str, SignatureVerifier],
|
||
) -> None:
|
||
if not verifiers:
|
||
raise ValueError("at least one trusted verifier is required")
|
||
self.parent_store = parent_store
|
||
self.grant_store = grant_store
|
||
self.verifiers = {required(key, "key_id"): value for key, value in verifiers.items()}
|
||
|
||
def _verify_parent(self, parent: RunProvenance, *, now: datetime) -> SignatureVerifier:
|
||
status = self.parent_store.status(parent.provenance_id)
|
||
if status == ProvenanceStatus.REVOKED:
|
||
raise ProvenanceRevoked("workflow provenance is revoked")
|
||
if now >= parse_timestamp(parent.expires_at):
|
||
self.parent_store.mark_expired(parent.provenance_id)
|
||
raise ProvenanceExpired("workflow provenance is expired")
|
||
verifier = self.verifiers.get(parent.key_id)
|
||
if verifier is None or not parent.signature or not verifier.verify(
|
||
parent.canonical_bytes(), parent.signature
|
||
):
|
||
raise ProvenanceSignatureError("invalid workflow provenance signature")
|
||
if parent.policy_version != WORKFLOW_POLICY_VERSION:
|
||
raise ProvenanceDenied("parent is not a workflow provenance")
|
||
if parent.allowed_paths or parent.allowed_git_operations:
|
||
raise ProvenanceDenied("workflow provenance must not contain operation rights")
|
||
return verifier
|
||
|
||
def authorize(
|
||
self,
|
||
*,
|
||
grant_id: str,
|
||
parent_provenance_id: str,
|
||
run_id: str,
|
||
tenant_id: str,
|
||
task_id: str,
|
||
agent_id: str,
|
||
principal_id: str,
|
||
execution_path: str,
|
||
request_digest: str,
|
||
git_operation: str | None = None,
|
||
now: datetime | None = None,
|
||
) -> GrantAuthorization:
|
||
current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
||
parent = self.parent_store.get(parent_provenance_id)
|
||
self._verify_parent(parent, now=current)
|
||
grant = self.grant_store.get(grant_id)
|
||
grant_status = self.grant_store.status(grant.grant_id)
|
||
if grant_status == GrantStatus.REVOKED:
|
||
raise ProvenanceRevoked("provenance grant is revoked")
|
||
if current >= parse_timestamp(grant.expires_at):
|
||
self.grant_store.mark_expired(grant.grant_id)
|
||
raise ProvenanceExpired("provenance grant is expired")
|
||
verifier = self.verifiers.get(grant.key_id)
|
||
if verifier is None or not grant.signature or not verifier.verify(
|
||
grant.canonical_bytes(), grant.signature
|
||
):
|
||
raise ProvenanceSignatureError("invalid provenance grant signature")
|
||
expected = {
|
||
"parent_provenance_id": parent.provenance_id,
|
||
"run_id": required(run_id, "run_id"),
|
||
"tenant_id": required(tenant_id, "tenant_id"),
|
||
"task_id": required(task_id, "task_id"),
|
||
"agent_id": required(agent_id, "agent_id"),
|
||
"principal_id": required(principal_id, "principal_id"),
|
||
}
|
||
for field_name, expected_value in expected.items():
|
||
if getattr(grant, field_name) != expected_value:
|
||
raise ProvenanceDenied(f"grant {field_name} mismatch")
|
||
if field_name != "parent_provenance_id" and getattr(parent, field_name) != expected_value:
|
||
raise ProvenanceDenied(f"workflow {field_name} mismatch")
|
||
path = ExecutionPath(execution_path)
|
||
if grant.execution_path != path:
|
||
raise ProvenanceDenied(
|
||
f"grant permits {grant.execution_path.value}, not {path.value}"
|
||
)
|
||
operation: str | None = None
|
||
if path == ExecutionPath.GIT_OPERATION:
|
||
if git_operation is None:
|
||
raise ProvenanceDenied("Git operation is required")
|
||
operation = GitOperation(git_operation).value
|
||
if operation not in grant.allowed_git_operations:
|
||
raise ProvenanceDenied(f"Git operation is not allowed: {operation}")
|
||
elif git_operation is not None:
|
||
raise ProvenanceDenied("git_operation is only valid for a Git Grant")
|
||
digest = _validate_digest(request_digest)
|
||
self.grant_store.mark_verified(grant.grant_id, fingerprint=verifier.fingerprint)
|
||
authorization = GrantAuthorization(
|
||
authorization_id=f"gauth_{uuid.uuid4().hex}",
|
||
grant_id=grant.grant_id,
|
||
parent_provenance_id=parent.provenance_id,
|
||
run_id=grant.run_id,
|
||
tenant_id=grant.tenant_id,
|
||
task_id=grant.task_id,
|
||
agent_id=grant.agent_id,
|
||
principal_id=grant.principal_id,
|
||
execution_path=path.value,
|
||
git_operation=operation,
|
||
request_digest=digest,
|
||
policy_version=grant.policy_version,
|
||
key_id=grant.key_id,
|
||
public_key_fingerprint=verifier.fingerprint,
|
||
)
|
||
self.grant_store.record_authorization(authorization)
|
||
return authorization
|
||
|
||
|
||
class LeastPrivilegeProvenanceWriter:
|
||
"""按路径选择独立子 Grant 的生产 Writer。
|
||
|
||
``provenance_id`` 参数始终指父 Workflow Provenance;Grant ID 来自受保护
|
||
配置映射,调用方无法通过请求体临时扩大权限。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
verifier: GrantVerifier,
|
||
receipt_root: str | Path,
|
||
grant_ids: Mapping[str, str],
|
||
*,
|
||
writer_id: str = "aios-v2-least-privilege-writer",
|
||
) -> None:
|
||
self.verifier = verifier
|
||
self.receipt_root = Path(receipt_root).resolve()
|
||
self.receipt_root.mkdir(parents=True, exist_ok=True)
|
||
self.grant_ids = {
|
||
ExecutionPath(path).value: required(grant_id, f"grant_id[{path}]")
|
||
for path, grant_id in grant_ids.items()
|
||
}
|
||
missing = sorted(path.value for path in ExecutionPath if path.value not in self.grant_ids)
|
||
if missing:
|
||
raise ValueError(f"missing least-privilege Grant mappings: {missing}")
|
||
self.writer_id = required(writer_id, "writer_id")
|
||
self._lock = threading.RLock()
|
||
|
||
@staticmethod
|
||
def _component(value: str) -> str:
|
||
normalized = required(value, "path component")
|
||
if normalized in {".", ".."} or "/" in normalized or "\\" in normalized:
|
||
raise ProvenanceDenied("invalid receipt path component")
|
||
return normalized
|
||
|
||
def authorize(self, **values: Any) -> GrantAuthorization:
|
||
path = ExecutionPath(values["execution_path"]).value
|
||
grant_id = self.grant_ids[path]
|
||
parent_provenance_id = values.pop("provenance_id")
|
||
authorization = self.verifier.authorize(
|
||
grant_id=grant_id,
|
||
parent_provenance_id=parent_provenance_id,
|
||
**values,
|
||
)
|
||
tenant = self._component(authorization.tenant_id)
|
||
parent = self._component(authorization.parent_provenance_id)
|
||
grant = self._component(authorization.grant_id)
|
||
target_dir = self.receipt_root / tenant / parent / grant
|
||
target = target_dir / f"{authorization.authorization_id}.json"
|
||
payload = authorization.as_dict()
|
||
payload["writer_id"] = self.writer_id
|
||
with self._lock:
|
||
target_dir.mkdir(parents=True, exist_ok=True)
|
||
temporary = target.with_suffix(f".tmp-{os.getpid()}")
|
||
temporary.write_text(
|
||
json.dumps(payload, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||
encoding="utf-8",
|
||
)
|
||
os.chmod(temporary, 0o640)
|
||
os.replace(temporary, target)
|
||
return authorization
|
||
|
||
def authorize_code_change(self, **values: Any) -> GrantAuthorization:
|
||
return self.authorize(execution_path=ExecutionPath.CODE_CHANGE.value, **values)
|
||
|
||
def authorize_git_operation(
|
||
self, *, git_operation: str, **values: Any
|
||
) -> GrantAuthorization:
|
||
return self.authorize(
|
||
execution_path=ExecutionPath.GIT_OPERATION.value,
|
||
git_operation=git_operation,
|
||
**values,
|
||
)
|
||
|
||
def authorize_deployment(self, **values: Any) -> GrantAuthorization:
|
||
return self.authorize(execution_path=ExecutionPath.DEPLOYMENT.value, **values)
|
||
|
||
def authorize_runtime_execute(self, **values: Any) -> GrantAuthorization:
|
||
return self.authorize(execution_path=ExecutionPath.RUNTIME_EXECUTE.value, **values)
|
||
|
||
def authorize_evidence_write(self, **values: Any) -> GrantAuthorization:
|
||
return self.authorize(execution_path=ExecutionPath.EVIDENCE_WRITE.value, **values)
|
||
|
||
def authorize_rollback(self, **values: Any) -> GrantAuthorization:
|
||
return self.authorize(execution_path=ExecutionPath.ROLLBACK.value, **values)
|
||
|
||
def authorize_audit(self, **values: Any) -> GrantAuthorization:
|
||
return self.authorize(execution_path=ExecutionPath.AUDIT.value, **values)
|