Add governed encryption administration
This commit is contained in:
@@ -19,6 +19,7 @@ from govoplan_core.core.modules import (
|
||||
CapabilityDocumentation,
|
||||
DocumentationLink,
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
MigrationSpec,
|
||||
ModuleContext,
|
||||
ModuleInterfaceProvider,
|
||||
@@ -26,6 +27,7 @@ from govoplan_core.core.modules import (
|
||||
ModuleUninstallGuardResult,
|
||||
PermissionDefinition,
|
||||
RoleTemplate,
|
||||
ViewSurface,
|
||||
)
|
||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
from govoplan_core.db.base import Base
|
||||
@@ -190,6 +192,43 @@ manifest = ModuleManifest(
|
||||
permissions=PERMISSIONS,
|
||||
role_templates=ROLE_TEMPLATES,
|
||||
route_factory=_router,
|
||||
frontend=FrontendModule(
|
||||
module_id=MODULE_ID,
|
||||
package_name="@govoplan/encryption-webui",
|
||||
view_surfaces=(
|
||||
ViewSurface(
|
||||
id="encryption.admin.operations",
|
||||
module_id=MODULE_ID,
|
||||
kind="section",
|
||||
label="Encryption administration",
|
||||
order=10,
|
||||
),
|
||||
ViewSurface(
|
||||
id="encryption.admin.vaults",
|
||||
module_id=MODULE_ID,
|
||||
kind="section",
|
||||
label="Key vaults",
|
||||
parent_id="encryption.admin.operations",
|
||||
order=20,
|
||||
),
|
||||
ViewSurface(
|
||||
id="encryption.admin.migrations",
|
||||
module_id=MODULE_ID,
|
||||
kind="section",
|
||||
label="Protection migrations",
|
||||
parent_id="encryption.admin.operations",
|
||||
order=30,
|
||||
),
|
||||
ViewSurface(
|
||||
id="encryption.admin.recovery",
|
||||
module_id=MODULE_ID,
|
||||
kind="section",
|
||||
label="Recovery ceremonies",
|
||||
parent_id="encryption.admin.operations",
|
||||
order=40,
|
||||
),
|
||||
),
|
||||
),
|
||||
capability_factories={
|
||||
CAPABILITY_ENCRYPTION_KEY_VAULT: _service,
|
||||
CAPABILITY_ENCRYPTION_CONTENT_PROTECTION: _service,
|
||||
@@ -319,6 +358,50 @@ manifest = ModuleManifest(
|
||||
),
|
||||
),
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="encryption.administration",
|
||||
title="Administer encryption operations",
|
||||
summary=(
|
||||
"Inspect safe vault and envelope metadata, govern key lifecycle, "
|
||||
"coordinate migrations, and verify disable readiness."
|
||||
),
|
||||
body=(
|
||||
"Encryption administration exposes bounded tenant metadata but "
|
||||
"never provider key references, wrapped keys, ciphertext locations, "
|
||||
"or plaintext. Rotation creates a new current version while existing "
|
||||
"envelopes remain version-bound. Revocation and destruction cannot "
|
||||
"recall material already obtained and can make content unavailable. "
|
||||
"Migrations remain two-phase: the owning module performs the durable "
|
||||
"content operation and records evidence before success. Disable "
|
||||
"preflight blocks until every envelope has a terminal disposition."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin",),
|
||||
audience=("administrator", "security_officer", "auditor"),
|
||||
related_modules=OPTIONAL_DEPENDENCIES,
|
||||
order=110,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="encryption.recovery",
|
||||
title="Run an encryption recovery ceremony",
|
||||
summary=(
|
||||
"Request and decide time-bounded recovery with high assurance and "
|
||||
"a distinct-custodian quorum."
|
||||
),
|
||||
body=(
|
||||
"A recovery requester supplies policy and recent high-assurance "
|
||||
"evidence and cannot approve the same ceremony. Each custodian can "
|
||||
"decide once; a rejection terminates the request and approvals must "
|
||||
"reach the vault quorum before expiry. Approval authorizes a later "
|
||||
"provider operation. It does not release key material, transfer "
|
||||
"resource ownership, or prove that recovery execution succeeded."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("administrator", "security_officer", "auditor"),
|
||||
related_modules=("identity_trust", "access", "audit", "policy"),
|
||||
order=120,
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="institutional_foundation",
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
@@ -20,18 +20,26 @@ from govoplan_core.core.encryption import (
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_encryption.backend.schemas import (
|
||||
DisablePreflightResponse,
|
||||
EnvelopeOperationalListResponse,
|
||||
EnvelopeOperationalSummaryResponse,
|
||||
EnvelopePayload,
|
||||
EnvelopeRegistrationPayload,
|
||||
EnvelopeResponse,
|
||||
KeyLifecyclePayload,
|
||||
KeyRotationPayload,
|
||||
MigrationOutcomePayload,
|
||||
MigrationOperationalListResponse,
|
||||
MigrationOperationalSummaryResponse,
|
||||
MigrationRequestPayload,
|
||||
MigrationResponse,
|
||||
RecoveryDecisionPayload,
|
||||
RecoveryOperationalListResponse,
|
||||
RecoveryOperationalSummaryResponse,
|
||||
RecoveryRequestPayload,
|
||||
RecoveryResponse,
|
||||
VaultCreatePayload,
|
||||
VaultOperationalListResponse,
|
||||
VaultOperationalSummaryResponse,
|
||||
VaultResponse,
|
||||
)
|
||||
from govoplan_encryption.backend.service import EncryptionError, SqlEncryptionService
|
||||
@@ -41,6 +49,28 @@ def create_router(registry: object | None = None) -> APIRouter:
|
||||
router = APIRouter(prefix="/encryption", tags=["encryption"])
|
||||
service = SqlEncryptionService(registry)
|
||||
|
||||
@router.get("/vaults", response_model=VaultOperationalListResponse)
|
||||
def list_vaults(
|
||||
state_filter: str | None = Query(default=None, alias="state", max_length=40),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> VaultOperationalListResponse:
|
||||
_require(principal, "encryption:vault:admin", "encryption:recovery:approve")
|
||||
values = service.list_vaults(
|
||||
session,
|
||||
principal,
|
||||
tenant_id=principal.tenant_id,
|
||||
state=state_filter,
|
||||
limit=limit,
|
||||
)
|
||||
return VaultOperationalListResponse(
|
||||
items=[
|
||||
VaultOperationalSummaryResponse(**_serializable(value))
|
||||
for value in values
|
||||
]
|
||||
)
|
||||
|
||||
@router.post("/vaults", response_model=VaultResponse)
|
||||
def create_vault(
|
||||
payload: VaultCreatePayload,
|
||||
@@ -173,6 +203,32 @@ def create_router(registry: object | None = None) -> APIRouter:
|
||||
session.commit()
|
||||
return _vault_response(value)
|
||||
|
||||
@router.get("/envelopes", response_model=EnvelopeOperationalListResponse)
|
||||
def list_envelopes(
|
||||
vault_id: str | None = Query(default=None, max_length=255),
|
||||
owner_module: str | None = Query(default=None, max_length=120),
|
||||
state_filter: str | None = Query(default=None, alias="state", max_length=40),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> EnvelopeOperationalListResponse:
|
||||
_require(principal, "encryption:vault:admin")
|
||||
values = service.list_envelopes(
|
||||
session,
|
||||
principal,
|
||||
tenant_id=principal.tenant_id,
|
||||
vault_id=vault_id,
|
||||
owner_module=owner_module,
|
||||
state=state_filter,
|
||||
limit=limit,
|
||||
)
|
||||
return EnvelopeOperationalListResponse(
|
||||
items=[
|
||||
EnvelopeOperationalSummaryResponse(**_serializable(value))
|
||||
for value in values
|
||||
]
|
||||
)
|
||||
|
||||
@router.post("/envelopes", response_model=EnvelopeResponse)
|
||||
def register_envelope(
|
||||
payload: EnvelopeRegistrationPayload,
|
||||
@@ -226,6 +282,28 @@ def create_router(registry: object | None = None) -> APIRouter:
|
||||
)
|
||||
return _envelope_response(value)
|
||||
|
||||
@router.get("/migrations", response_model=MigrationOperationalListResponse)
|
||||
def list_migrations(
|
||||
state_filter: str | None = Query(default=None, alias="state", max_length=40),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MigrationOperationalListResponse:
|
||||
_require(principal, "encryption:vault:admin")
|
||||
values = service.list_migrations(
|
||||
session,
|
||||
principal,
|
||||
tenant_id=principal.tenant_id,
|
||||
state=state_filter,
|
||||
limit=limit,
|
||||
)
|
||||
return MigrationOperationalListResponse(
|
||||
items=[
|
||||
MigrationOperationalSummaryResponse(**_serializable(value))
|
||||
for value in values
|
||||
]
|
||||
)
|
||||
|
||||
@router.post("/migrations", response_model=MigrationResponse)
|
||||
def request_migration(
|
||||
payload: MigrationRequestPayload,
|
||||
@@ -289,6 +367,53 @@ def create_router(registry: object | None = None) -> APIRouter:
|
||||
session.commit()
|
||||
return _migration_response(value)
|
||||
|
||||
@router.post("/migrations/{migration_id}/reconcile", response_model=MigrationResponse)
|
||||
def reconcile_migration(
|
||||
migration_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> MigrationResponse:
|
||||
_require(principal, "encryption:vault:admin")
|
||||
value = _call(
|
||||
lambda: service.reconcile_migration(
|
||||
session,
|
||||
principal,
|
||||
migration_id=migration_id,
|
||||
)
|
||||
)
|
||||
_audit(
|
||||
session,
|
||||
principal,
|
||||
"encryption.migration.reconciled",
|
||||
"protection_migration",
|
||||
migration_id,
|
||||
{"state": value.state},
|
||||
)
|
||||
session.commit()
|
||||
return _migration_response(value)
|
||||
|
||||
@router.get("/recoveries", response_model=RecoveryOperationalListResponse)
|
||||
def list_recoveries(
|
||||
state_filter: str | None = Query(default=None, alias="state", max_length=40),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> RecoveryOperationalListResponse:
|
||||
_require(principal, "encryption:vault:admin", "encryption:recovery:approve")
|
||||
values = service.list_recoveries(
|
||||
session,
|
||||
principal,
|
||||
tenant_id=principal.tenant_id,
|
||||
state=state_filter,
|
||||
limit=limit,
|
||||
)
|
||||
return RecoveryOperationalListResponse(
|
||||
items=[
|
||||
RecoveryOperationalSummaryResponse(**_serializable(value))
|
||||
for value in values
|
||||
]
|
||||
)
|
||||
|
||||
@router.post("/recoveries", response_model=RecoveryResponse)
|
||||
def request_recovery(
|
||||
payload: RecoveryRequestPayload,
|
||||
|
||||
@@ -84,6 +84,30 @@ class VaultResponse(BaseModel):
|
||||
contract_version: str = "1"
|
||||
|
||||
|
||||
class VaultOperationalSummaryResponse(BaseModel):
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
name: str
|
||||
provider_id: str
|
||||
purpose: str
|
||||
profile_kind: str
|
||||
scope_type: str
|
||||
scope_id: str | None = None
|
||||
policy_ref: str
|
||||
recovery_quorum: int
|
||||
state: str
|
||||
revision: int
|
||||
current_key_version: int | None = None
|
||||
current_key_state: str | None = None
|
||||
algorithm_suite: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class VaultOperationalListResponse(BaseModel):
|
||||
items: list[VaultOperationalSummaryResponse]
|
||||
|
||||
|
||||
class EnvelopePayload(BaseModel):
|
||||
envelope_id: str = Field(min_length=1, max_length=255)
|
||||
owner_module: str = Field(min_length=1, max_length=120)
|
||||
@@ -126,6 +150,27 @@ class EnvelopeResponse(EnvelopePayload):
|
||||
contract_version: str = "1"
|
||||
|
||||
|
||||
class EnvelopeOperationalSummaryResponse(BaseModel):
|
||||
envelope_id: str
|
||||
tenant_id: str
|
||||
owner_module: str
|
||||
resource_type: str
|
||||
resource_id: str
|
||||
profile_kind: str
|
||||
provider_id: str
|
||||
vault_id: str
|
||||
key_version: int
|
||||
algorithm_suite: str
|
||||
state: str
|
||||
migration_id: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class EnvelopeOperationalListResponse(BaseModel):
|
||||
items: list[EnvelopeOperationalSummaryResponse]
|
||||
|
||||
|
||||
class MigrationRequestPayload(BaseModel):
|
||||
envelope_id: str = Field(min_length=1, max_length=255)
|
||||
target_provider_id: str = Field(min_length=1, max_length=120)
|
||||
@@ -156,6 +201,31 @@ class MigrationResponse(BaseModel):
|
||||
contract_version: str = "1"
|
||||
|
||||
|
||||
class MigrationOperationalSummaryResponse(BaseModel):
|
||||
migration_id: str
|
||||
tenant_id: str
|
||||
source_envelope_id: str
|
||||
target_envelope_id: str | None = None
|
||||
target_provider_id: str
|
||||
target_vault_id: str
|
||||
target_key_version: int
|
||||
target_algorithm_suite: str
|
||||
mode: str
|
||||
state: str
|
||||
policy_decision_ref: str
|
||||
assurance_evidence_ref: str
|
||||
evidence_refs: list[str] = Field(default_factory=list)
|
||||
error_code: str | None = None
|
||||
requested_by: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
completed_at: datetime | None = None
|
||||
|
||||
|
||||
class MigrationOperationalListResponse(BaseModel):
|
||||
items: list[MigrationOperationalSummaryResponse]
|
||||
|
||||
|
||||
class RecoveryRequestPayload(BaseModel):
|
||||
vault_id: str = Field(min_length=1, max_length=255)
|
||||
reason: str = Field(min_length=1, max_length=2000)
|
||||
@@ -191,6 +261,30 @@ class RecoveryResponse(BaseModel):
|
||||
contract_version: str = "1"
|
||||
|
||||
|
||||
class RecoveryOperationalSummaryResponse(BaseModel):
|
||||
recovery_id: str
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
state: str
|
||||
requested_scope: str
|
||||
reason: str
|
||||
quorum: int
|
||||
approvals: int
|
||||
rejections: int
|
||||
revision: int
|
||||
policy_decision_ref: str
|
||||
requester_account_id: str
|
||||
requester_assurance_ref: str
|
||||
expires_at: datetime
|
||||
execution_ref: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class RecoveryOperationalListResponse(BaseModel):
|
||||
items: list[RecoveryOperationalSummaryResponse]
|
||||
|
||||
|
||||
class DisablePreflightResponse(BaseModel):
|
||||
allowed: bool
|
||||
protected_count: int
|
||||
@@ -206,15 +300,23 @@ __all__ = [
|
||||
"DisablePreflightResponse",
|
||||
"EnvelopePayload",
|
||||
"EnvelopeRegistrationPayload",
|
||||
"EnvelopeOperationalListResponse",
|
||||
"EnvelopeOperationalSummaryResponse",
|
||||
"EnvelopeResponse",
|
||||
"KeyLifecyclePayload",
|
||||
"KeyRotationPayload",
|
||||
"MigrationOutcomePayload",
|
||||
"MigrationOperationalListResponse",
|
||||
"MigrationOperationalSummaryResponse",
|
||||
"MigrationRequestPayload",
|
||||
"MigrationResponse",
|
||||
"RecoveryDecisionPayload",
|
||||
"RecoveryOperationalListResponse",
|
||||
"RecoveryOperationalSummaryResponse",
|
||||
"RecoveryRequestPayload",
|
||||
"RecoveryResponse",
|
||||
"VaultCreatePayload",
|
||||
"VaultOperationalListResponse",
|
||||
"VaultOperationalSummaryResponse",
|
||||
"VaultResponse",
|
||||
]
|
||||
|
||||
@@ -2,13 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.core.encryption import (
|
||||
@@ -61,6 +62,88 @@ class EncryptionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VaultOperationalSummary:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
name: str
|
||||
provider_id: str
|
||||
purpose: str
|
||||
profile_kind: str
|
||||
scope_type: str
|
||||
scope_id: str | None
|
||||
policy_ref: str
|
||||
recovery_quorum: int
|
||||
state: str
|
||||
revision: int
|
||||
current_key_version: int | None
|
||||
current_key_state: str | None
|
||||
algorithm_suite: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EnvelopeOperationalSummary:
|
||||
envelope_id: str
|
||||
tenant_id: str
|
||||
owner_module: str
|
||||
resource_type: str
|
||||
resource_id: str
|
||||
profile_kind: str
|
||||
provider_id: str
|
||||
vault_id: str
|
||||
key_version: int
|
||||
algorithm_suite: str
|
||||
state: str
|
||||
migration_id: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MigrationOperationalSummary:
|
||||
migration_id: str
|
||||
tenant_id: str
|
||||
source_envelope_id: str
|
||||
target_envelope_id: str | None
|
||||
target_provider_id: str
|
||||
target_vault_id: str
|
||||
target_key_version: int
|
||||
target_algorithm_suite: str
|
||||
mode: str
|
||||
state: str
|
||||
policy_decision_ref: str
|
||||
assurance_evidence_ref: str
|
||||
evidence_refs: tuple[str, ...]
|
||||
error_code: str | None
|
||||
requested_by: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
completed_at: datetime | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecoveryOperationalSummary:
|
||||
recovery_id: str
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
state: str
|
||||
requested_scope: str
|
||||
reason: str
|
||||
quorum: int
|
||||
approvals: int
|
||||
rejections: int
|
||||
revision: int
|
||||
policy_decision_ref: str
|
||||
requester_account_id: str
|
||||
requester_assurance_ref: str
|
||||
expires_at: datetime
|
||||
execution_ref: str | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class SqlEncryptionService:
|
||||
"""Provider-neutral encryption metadata and recovery orchestration.
|
||||
|
||||
@@ -282,6 +365,57 @@ class SqlEncryptionService:
|
||||
)
|
||||
return self._vault_ref(vault, db) if vault is not None else None
|
||||
|
||||
def list_vaults(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
state: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> tuple[VaultOperationalSummary, ...]:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, tenant_id)
|
||||
statement = (
|
||||
select(EncryptionVault, EncryptionKeyVersion)
|
||||
.outerjoin(
|
||||
EncryptionKeyVersion,
|
||||
and_(
|
||||
EncryptionKeyVersion.tenant_id == EncryptionVault.tenant_id,
|
||||
EncryptionKeyVersion.vault_id == EncryptionVault.vault_id,
|
||||
EncryptionKeyVersion.version
|
||||
== EncryptionVault.current_key_version,
|
||||
),
|
||||
)
|
||||
.where(EncryptionVault.tenant_id == tenant_id)
|
||||
.order_by(EncryptionVault.updated_at.desc(), EncryptionVault.vault_id)
|
||||
.limit(_bounded_limit(limit))
|
||||
)
|
||||
if state:
|
||||
statement = statement.where(EncryptionVault.state == state)
|
||||
return tuple(
|
||||
VaultOperationalSummary(
|
||||
tenant_id=vault.tenant_id,
|
||||
vault_id=vault.vault_id,
|
||||
name=vault.name,
|
||||
provider_id=vault.provider_id,
|
||||
purpose=vault.purpose,
|
||||
profile_kind=vault.profile_kind,
|
||||
scope_type=vault.scope_type,
|
||||
scope_id=vault.scope_id,
|
||||
policy_ref=vault.policy_ref,
|
||||
recovery_quorum=vault.recovery_quorum,
|
||||
state=vault.state,
|
||||
revision=vault.revision,
|
||||
current_key_version=vault.current_key_version,
|
||||
current_key_state=key.state if key is not None else None,
|
||||
algorithm_suite=key.algorithm_suite if key is not None else None,
|
||||
created_at=vault.created_at,
|
||||
updated_at=vault.updated_at,
|
||||
)
|
||||
for vault, key in db.execute(statement)
|
||||
)
|
||||
|
||||
def reconcile_vault(
|
||||
self,
|
||||
session: object,
|
||||
@@ -848,6 +982,54 @@ class SqlEncryptionService:
|
||||
)
|
||||
return _envelope_ref(item) if item is not None else None
|
||||
|
||||
def list_envelopes(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
vault_id: str | None = None,
|
||||
owner_module: str | None = None,
|
||||
state: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> tuple[EnvelopeOperationalSummary, ...]:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, tenant_id)
|
||||
statement = select(ContentProtectionRecord).where(
|
||||
ContentProtectionRecord.tenant_id == tenant_id
|
||||
)
|
||||
if vault_id:
|
||||
statement = statement.where(ContentProtectionRecord.vault_id == vault_id)
|
||||
if owner_module:
|
||||
statement = statement.where(
|
||||
ContentProtectionRecord.owner_module == owner_module
|
||||
)
|
||||
if state:
|
||||
statement = statement.where(ContentProtectionRecord.state == state)
|
||||
statement = statement.order_by(
|
||||
ContentProtectionRecord.updated_at.desc(),
|
||||
ContentProtectionRecord.envelope_id,
|
||||
).limit(_bounded_limit(limit))
|
||||
return tuple(
|
||||
EnvelopeOperationalSummary(
|
||||
envelope_id=item.envelope_id,
|
||||
tenant_id=item.tenant_id,
|
||||
owner_module=item.owner_module,
|
||||
resource_type=item.resource_type,
|
||||
resource_id=item.resource_id,
|
||||
profile_kind=item.profile_kind,
|
||||
provider_id=item.provider_id,
|
||||
vault_id=item.vault_id,
|
||||
key_version=item.key_version,
|
||||
algorithm_suite=item.algorithm_suite,
|
||||
state=item.state,
|
||||
migration_id=item.migration_id,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
)
|
||||
for item in db.scalars(statement)
|
||||
)
|
||||
|
||||
def request_migration(
|
||||
self,
|
||||
session: object,
|
||||
@@ -924,6 +1106,49 @@ class SqlEncryptionService:
|
||||
db.flush()
|
||||
return self._migration_ref(db, item)
|
||||
|
||||
def list_migrations(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
state: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> tuple[MigrationOperationalSummary, ...]:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, tenant_id)
|
||||
statement = select(ProtectionMigration).where(
|
||||
ProtectionMigration.tenant_id == tenant_id
|
||||
)
|
||||
if state:
|
||||
statement = statement.where(ProtectionMigration.state == state)
|
||||
statement = statement.order_by(
|
||||
ProtectionMigration.updated_at.desc(), ProtectionMigration.id
|
||||
).limit(_bounded_limit(limit))
|
||||
return tuple(
|
||||
MigrationOperationalSummary(
|
||||
migration_id=item.id,
|
||||
tenant_id=item.tenant_id,
|
||||
source_envelope_id=item.source_envelope_id,
|
||||
target_envelope_id=item.target_envelope_id,
|
||||
target_provider_id=item.target_provider_id,
|
||||
target_vault_id=item.target_vault_id,
|
||||
target_key_version=item.target_key_version,
|
||||
target_algorithm_suite=item.target_algorithm_suite,
|
||||
mode=item.mode,
|
||||
state=item.state,
|
||||
policy_decision_ref=item.policy_decision_ref,
|
||||
assurance_evidence_ref=item.assurance_evidence_ref,
|
||||
evidence_refs=tuple(item.evidence_refs),
|
||||
error_code=item.error_code,
|
||||
requested_by=item.requested_by,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
completed_at=item.completed_at,
|
||||
)
|
||||
for item in db.scalars(statement)
|
||||
)
|
||||
|
||||
def record_migration_outcome(
|
||||
self,
|
||||
session: object,
|
||||
@@ -1092,6 +1317,70 @@ class SqlEncryptionService:
|
||||
db.flush()
|
||||
return self._recovery_ref(db, item)
|
||||
|
||||
def list_recoveries(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
state: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> tuple[RecoveryOperationalSummary, ...]:
|
||||
db = _session(session)
|
||||
_require_tenant(principal, tenant_id)
|
||||
statement = select(RecoveryCeremony).where(
|
||||
RecoveryCeremony.tenant_id == tenant_id
|
||||
)
|
||||
if state:
|
||||
statement = statement.where(RecoveryCeremony.state == state)
|
||||
statement = statement.order_by(
|
||||
RecoveryCeremony.updated_at.desc(), RecoveryCeremony.id
|
||||
).limit(_bounded_limit(limit))
|
||||
ceremonies = tuple(db.scalars(statement))
|
||||
if not ceremonies:
|
||||
return ()
|
||||
counts: dict[str, Counter[str]] = {
|
||||
ceremony.id: Counter() for ceremony in ceremonies
|
||||
}
|
||||
count_rows = db.execute(
|
||||
select(
|
||||
RecoveryApproval.recovery_id,
|
||||
RecoveryApproval.decision,
|
||||
func.count(RecoveryApproval.id),
|
||||
)
|
||||
.where(RecoveryApproval.recovery_id.in_(tuple(counts)))
|
||||
.group_by(RecoveryApproval.recovery_id, RecoveryApproval.decision)
|
||||
)
|
||||
for recovery_id, decision, count in count_rows:
|
||||
counts[str(recovery_id)][str(decision)] = int(count)
|
||||
now = _as_utc(utcnow())
|
||||
return tuple(
|
||||
RecoveryOperationalSummary(
|
||||
recovery_id=item.id,
|
||||
tenant_id=item.tenant_id,
|
||||
vault_id=item.vault_id,
|
||||
state=(
|
||||
"expired"
|
||||
if item.state == "pending" and _as_utc(item.expires_at) <= now
|
||||
else item.state
|
||||
),
|
||||
requested_scope=item.requested_scope,
|
||||
reason=item.reason,
|
||||
quorum=item.quorum,
|
||||
approvals=counts[item.id]["approve"],
|
||||
rejections=counts[item.id]["reject"],
|
||||
revision=item.revision,
|
||||
policy_decision_ref=item.policy_decision_ref,
|
||||
requester_account_id=item.requester_account_id,
|
||||
requester_assurance_ref=item.requester_assurance_ref,
|
||||
expires_at=item.expires_at,
|
||||
execution_ref=item.execution_ref,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
)
|
||||
for item in ceremonies
|
||||
)
|
||||
|
||||
def decide_recovery(
|
||||
self,
|
||||
session: object,
|
||||
@@ -2007,6 +2296,10 @@ def _as_utc(value: datetime) -> datetime:
|
||||
return value.astimezone(timezone.utc)
|
||||
|
||||
|
||||
def _bounded_limit(value: int) -> int:
|
||||
return max(1, min(int(value), 500))
|
||||
|
||||
|
||||
def _exception_code(exc: Exception) -> str:
|
||||
return f"provider_{type(exc).__name__.lower()}"[:255]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user