Implement identity trust module

This commit is contained in:
2026-08-01 20:57:27 +02:00
parent 765fcd5a2d
commit 0ffc8b6b0f
20 changed files with 2058 additions and 5 deletions
@@ -0,0 +1,256 @@
from __future__ import annotations
from dataclasses import asdict
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from govoplan_core.audit.logging import audit_event
from govoplan_core.auth import ApiPrincipal, get_api_principal, has_scope
from govoplan_core.core.identity_trust import (
AssuranceCheckRequest,
DeviceKeyRegistration,
KeyAccessRequest,
KeyEpochRotationRequest,
)
from govoplan_core.db.session import get_session
from govoplan_identity_trust.backend.manifest import (
ADMIN_SCOPE,
ASSURANCE_SCOPE,
DEVICE_READ_SCOPE,
DEVICE_WRITE_SCOPE,
KEY_ACCESS_SCOPE,
)
from govoplan_identity_trust.backend.schemas import (
AssuranceCheckPayload,
AssuranceEvidencePayload,
AssuranceResponse,
DeviceKeyListResponse,
DeviceKeyRegisterPayload,
DeviceKeyResponse,
DeviceKeyRevokePayload,
EpochResponse,
EpochRotatePayload,
KeyAccessPayload,
KeyAccessResponse,
)
from govoplan_identity_trust.backend.service import (
IdentityTrustError,
SqlIdentityTrustService,
record_assurance_evidence,
)
router = APIRouter(prefix="/identity-trust", tags=["identity-trust"])
service = SqlIdentityTrustService()
def _require(principal: ApiPrincipal, *scopes: str) -> None:
if any(has_scope(principal, scope) for scope in scopes):
return
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing one of: {', '.join(scopes)}",
)
def _error(exc: IdentityTrustError) -> HTTPException:
return HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc))
def _device_response(value) -> DeviceKeyResponse:
return DeviceKeyResponse(**asdict(value))
def _epoch_response(value) -> EpochResponse:
return EpochResponse(**asdict(value))
@router.post("/device-keys", response_model=DeviceKeyResponse)
def api_register_device_key(
payload: DeviceKeyRegisterPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> DeviceKeyResponse:
_require(principal, DEVICE_WRITE_SCOPE, ADMIN_SCOPE)
try:
value = service.register_device_key(
session,
principal,
request=DeviceKeyRegistration(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
except (IdentityTrustError, ValueError) as exc:
raise _error(IdentityTrustError(str(exc))) from exc
audit_event(
session,
tenant_id=principal.tenant_id,
user_id=getattr(principal.user, "id", None),
api_key_id=principal.api_key_id,
action="identity_trust.device_key.registered",
object_type="device_public_key",
object_id=value.key_id,
details={"algorithm": value.algorithm, "private_material": False},
)
session.commit()
return _device_response(value)
@router.get("/device-keys", response_model=DeviceKeyListResponse)
def api_list_device_keys(
account_id: str,
active_only: bool = Query(default=True),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> DeviceKeyListResponse:
_require(principal, DEVICE_READ_SCOPE, ADMIN_SCOPE)
try:
values = service.list_device_keys(
session,
principal,
tenant_id=principal.tenant_id,
account_id=account_id,
active_only=active_only,
)
except IdentityTrustError as exc:
raise _error(exc) from exc
return DeviceKeyListResponse(keys=[_device_response(value) for value in values])
@router.post("/device-keys/{key_id}/revoke", response_model=DeviceKeyResponse)
def api_revoke_device_key(
key_id: str,
payload: DeviceKeyRevokePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> DeviceKeyResponse:
_require(principal, DEVICE_WRITE_SCOPE, ADMIN_SCOPE)
try:
value = service.revoke_device_key(
session,
principal,
tenant_id=principal.tenant_id,
key_id=key_id,
expected_epoch=payload.expected_epoch,
reason=payload.reason,
)
except IdentityTrustError as exc:
raise _error(exc) from exc
audit_event(
session,
tenant_id=principal.tenant_id,
user_id=getattr(principal.user, "id", None),
api_key_id=principal.api_key_id,
action="identity_trust.device_key.revoked",
object_type="device_public_key",
object_id=key_id,
details={"reason": payload.reason, "epoch": value.epoch},
)
session.commit()
return _device_response(value)
@router.post("/epochs/rotate", response_model=EpochResponse)
def api_rotate_epoch(
payload: EpochRotatePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> EpochResponse:
_require(principal, ADMIN_SCOPE)
try:
value = service.rotate_epoch(
session,
principal,
request=KeyEpochRotationRequest(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
except (IdentityTrustError, ValueError) as exc:
raise _error(IdentityTrustError(str(exc))) from exc
audit_event(
session,
tenant_id=principal.tenant_id,
user_id=getattr(principal.user, "id", None),
api_key_id=principal.api_key_id,
action="identity_trust.key_epoch.rotated",
object_type=payload.subject_kind,
object_id=payload.subject_id,
details={"epoch": value.epoch, "history_policy": value.history_policy},
)
session.commit()
return _epoch_response(value)
@router.post("/key-access/decide", response_model=KeyAccessResponse)
def api_decide_key_access(
payload: KeyAccessPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> KeyAccessResponse:
_require(principal, KEY_ACCESS_SCOPE, ADMIN_SCOPE)
try:
value = service.decide_key_access(
session,
principal,
request=KeyAccessRequest(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
except (IdentityTrustError, ValueError) as exc:
raise _error(IdentityTrustError(str(exc))) from exc
session.commit()
data = asdict(value)
data["requirements"] = list(value.requirements)
return KeyAccessResponse(**data)
@router.post("/assurance/evidence", response_model=dict[str, object])
def api_record_assurance(
payload: AssuranceEvidencePayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> dict[str, object]:
_require(principal, ASSURANCE_SCOPE, ADMIN_SCOPE)
try:
value = record_assurance_evidence(
session,
principal,
tenant_id=principal.tenant_id,
**payload.model_dump(),
)
except IdentityTrustError as exc:
raise _error(exc) from exc
audit_event(
session,
tenant_id=principal.tenant_id,
user_id=getattr(principal.user, "id", None),
api_key_id=principal.api_key_id,
action="identity_trust.assurance.recorded",
object_type="assurance_evidence",
object_id=value.id,
details={"provider_id": value.provider_id, "level": value.assurance_level},
)
session.commit()
return {"id": value.id, "evidence_ref": value.evidence_ref}
@router.post("/assurance/check", response_model=AssuranceResponse)
def api_check_assurance(
payload: AssuranceCheckPayload,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(get_api_principal),
) -> AssuranceResponse:
_require(principal, KEY_ACCESS_SCOPE, ADMIN_SCOPE)
value = service.verify_assurance(
session,
principal,
request=AssuranceCheckRequest(
tenant_id=principal.tenant_id,
**payload.model_dump(),
),
)
return AssuranceResponse(**asdict(value))