Files
aios/services/aios-autonomous-release-candidate/aios_autonomous_release_candidate/secret_manager.py
T

214 lines
8.4 KiB
Python

"""AIOS Secret Manager V2 reference-only Candidate adapter."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Mapping, Protocol, Sequence
from .errors import CredentialUnavailable
from .models import CredentialRequest
PROVIDER_REFERENCES = {
"openclaw": "systemd-credential:openclaw-gateway-candidate",
"codex": "systemd-credential:codex-app-server-candidate",
"native_adapter_control": "systemd-credential:session-native-adapter-control-candidate",
"gitea": "systemd-credential:gitea-admin-api-candidate",
}
FORBIDDEN_RESPONSE_FIELDS = {
"api_key",
"token",
"password",
"cookie",
"private_key",
"credential_body",
"plaintext",
}
class SecretManagerTransport(Protocol):
def exchange(self, operation: str, payload: Mapping[str, Any]) -> Mapping[str, Any]: ...
@dataclass(frozen=True, slots=True)
class SecretProjectionReceipt:
task_id: str
credential_id: str
reference: str
provider: str
audience: str
consumer_service: str
state: str
expires_at: datetime
plaintext_secret_returned: bool = False
class SecretManagerV2:
"""Issue, project, inspect and revoke without receiving authentication material."""
def __init__(self, transport: SecretManagerTransport) -> None:
self.transport = transport
@staticmethod
def _assert_safe(value: Any) -> None:
if isinstance(value, Mapping):
if FORBIDDEN_RESPONSE_FIELDS.intersection(str(key).lower() for key in value):
raise CredentialUnavailable(
"SECRET_MANAGER_PLAINTEXT_REJECTED",
"Secret Manager response included a forbidden plaintext field",
)
for item in value.values():
SecretManagerV2._assert_safe(item)
elif isinstance(value, (list, tuple)):
for item in value:
SecretManagerV2._assert_safe(item)
def _exchange(self, operation: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
try:
response = self.transport.exchange(operation, payload)
except CredentialUnavailable:
raise
except Exception as error:
raise CredentialUnavailable(
"SECRET_MANAGER_UNAVAILABLE", "Secret Manager V2 operation failed"
) from error
if not isinstance(response, Mapping):
raise CredentialUnavailable(
"SECRET_MANAGER_RESPONSE_INVALID", "Secret Manager V2 returned a non-object"
)
self._assert_safe(response)
if response.get("plaintext_secret_returned") is not False:
raise CredentialUnavailable(
"SECRET_MANAGER_PLAINTEXT_REJECTED",
"Secret Manager did not attest plaintext_secret_returned=false",
)
return response
@staticmethod
def _payload(request: CredentialRequest) -> dict[str, Any]:
return {
"approval_plan_id": request.approval_plan_id,
"task_id": request.task_id,
"request_id": request.request_id,
"session_id": request.session_id,
"worker_id": request.worker_id,
"provider": request.provider,
"audience": request.audience,
"consumer_service": request.consumer_service,
"runtime_user": request.runtime_user,
"repository": request.binding.repository,
"branch": request.binding.branch,
"branch_oid": request.binding.base_oid,
"allowed_paths": list(request.binding.allowed_paths),
"operations": list(request.operations),
"not_before": request.not_before.astimezone(timezone.utc).isoformat(),
"expires_at": request.expires_at.astimezone(timezone.utc).isoformat(),
"nonce": request.nonce,
"ttl_seconds": request.ttl_seconds,
"max_uses": request.max_uses,
}
def issue(
self, request: CredentialRequest, now: datetime | None = None
) -> SecretProjectionReceipt:
now = now or datetime.now(timezone.utc)
if now < request.not_before or now >= request.expires_at:
raise CredentialUnavailable(
"SECRET_MANAGER_APPROVAL_WINDOW_INVALID", "credential request is not currently valid"
)
expected_reference = PROVIDER_REFERENCES.get(request.provider)
if expected_reference is None:
raise CredentialUnavailable(
"SECRET_MANAGER_PROVIDER_DENIED", "provider is not Candidate-approved"
)
response = self._exchange("issue", self._payload(request))
credential_id = response.get("credential_id")
if (
response.get("task_id") != request.task_id
or not isinstance(credential_id, str)
or not credential_id
or response.get("credential_reference") != expected_reference
or response.get("state") != "ISSUED"
):
raise CredentialUnavailable(
"SECRET_MANAGER_BINDING_MISMATCH", "issued projection does not match the request"
)
expires_at = datetime.fromisoformat(str(response.get("expires_at")))
if expires_at > request.expires_at or expires_at <= now:
raise CredentialUnavailable(
"SECRET_MANAGER_EXPIRY_INVALID", "issued projection expiry is outside approval"
)
return SecretProjectionReceipt(
task_id=request.task_id,
credential_id=credential_id,
reference=expected_reference,
provider=request.provider,
audience=request.audience,
consumer_service=request.consumer_service,
state="ISSUED",
expires_at=expires_at,
plaintext_secret_returned=False,
)
@staticmethod
def _receipt_payload(receipt: SecretProjectionReceipt) -> dict[str, Any]:
return {
"task_id": receipt.task_id,
"credential_id": receipt.credential_id,
"credential_reference": receipt.reference,
"provider": receipt.provider,
"audience": receipt.audience,
"consumer_service": receipt.consumer_service,
}
def _state_operation(
self, operation: str, receipt: SecretProjectionReceipt, expected_state: str
) -> Mapping[str, Any]:
response = self._exchange(operation, self._receipt_payload(receipt))
if (
response.get("task_id") != receipt.task_id
or response.get("credential_id") != receipt.credential_id
or response.get("state") != expected_state
):
raise CredentialUnavailable(
"SECRET_MANAGER_STATE_MISMATCH", f"{operation} response is not task-bound"
)
return response
def project(self, receipt: SecretProjectionReceipt) -> None:
self._state_operation("project", receipt, "PROJECTED")
def status(self, receipt: SecretProjectionReceipt) -> str:
return str(self._state_operation("status", receipt, "PROJECTED")["state"])
def revoke(self, receipt: SecretProjectionReceipt) -> None:
self._state_operation("revoke", receipt, "REVOKED")
def verify_revocation(self, receipt: SecretProjectionReceipt) -> None:
response = self._state_operation("verify_revocation", receipt, "REVOKED")
if response.get("post_revocation_auth") != "REJECTED":
raise CredentialUnavailable(
"SECRET_MANAGER_REVOCATION_UNVERIFIED",
"post-revocation authentication was not rejected",
)
def audit(self, receipt: SecretProjectionReceipt) -> Sequence[Mapping[str, Any]]:
response = self._exchange("audit", self._receipt_payload(receipt))
if (
response.get("task_id") != receipt.task_id
or response.get("credential_id") != receipt.credential_id
):
raise CredentialUnavailable(
"SECRET_MANAGER_AUDIT_MISMATCH", "audit response is not task-bound"
)
events = response.get("events")
if not isinstance(events, list) or any(
not isinstance(event, Mapping) or event.get("secrets_included") is not False
for event in events
):
raise CredentialUnavailable(
"SECRET_MANAGER_AUDIT_INVALID", "audit events are absent or unsafe"
)
return events