feat: synchronize governance templates in bulk

This commit is contained in:
2026-08-20 19:47:02 +02:00
parent 935cee8ccd
commit 550c14b92e
9 changed files with 550 additions and 41 deletions
+74 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections import Counter
from collections.abc import Mapping
import os
from pathlib import Path
@@ -8,7 +9,12 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy.orm import Session
from govoplan_admin.backend.governance import create_template, delete_template, update_template
from govoplan_admin.backend.governance import (
create_template,
delete_template,
synchronize_templates,
update_template,
)
from govoplan_core.admin.settings import get_system_settings
from govoplan_core.auth import ApiPrincipal, has_scope, require_any_scope, require_scope
from govoplan_core.audit.logging import audit_from_principal, audit_operation_context
@@ -119,6 +125,8 @@ from .schemas import (
GovernanceTemplateListDeltaResponse,
GovernanceTemplateListResponse,
GovernanceTemplateUpdateRequest,
GovernanceSynchronizationRequest,
GovernanceSynchronizationResponse,
MaintenanceModeItem,
ModuleCatalogItem,
ModuleCatalogResponse,
@@ -2105,6 +2113,71 @@ def create_governance_template(
return _governance_template_item(session, item)
@router.post(
"/system/governance-templates/synchronize",
response_model=GovernanceSynchronizationResponse,
)
def synchronize_governance_templates(
payload: GovernanceSynchronizationRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("system:governance:write")),
):
try:
result = synchronize_templates(
session,
template_ids=payload.template_ids,
dry_run=payload.dry_run,
)
except (AdminConflictError, AdminValidationError) as exc:
raise _http_admin_error(exc) from exc
counts = Counter(item.status for item in result.outcomes)
audit_from_principal(
session,
principal,
action=(
"governance_template.synchronization_previewed"
if payload.dry_run
else "governance_template.synchronized"
),
scope="system",
object_type="governance_template_batch",
object_id=result.operation_id,
details={
"version": result.version,
"dry_run": result.dry_run,
"template_ids": list(dict.fromkeys(payload.template_ids)),
"counts": dict(sorted(counts.items())),
"blocked_assignment_ids": [
item.assignment_id
for item in result.outcomes
if item.status in {"blocked", "failed"}
],
},
)
session.commit()
return GovernanceSynchronizationResponse(
version=result.version,
operation_id=result.operation_id,
dry_run=result.dry_run,
outcomes=[
{
"assignment_id": item.assignment_id,
"template_id": item.template_id,
"tenant_id": item.tenant_id,
"kind": item.kind,
"operation": item.operation,
"status": item.status,
"resource_id": item.resource_id,
"blocker_codes": list(item.blocker_codes),
"message": item.message,
"provenance": dict(item.provenance),
}
for item in result.outcomes
],
counts=dict(sorted(counts.items())),
)
@router.patch("/system/governance-templates/{template_id}", response_model=GovernanceTemplateItem)
def update_governance_template(
template_id: str,
@@ -590,3 +590,31 @@ class GovernanceTemplateUpdateRequest(BaseModel):
is_active: bool = True
assignments: list[GovernanceAssignment] = Field(default_factory=list)
change_request_id: str | None = None
class GovernanceSynchronizationRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
template_ids: list[str] = Field(min_length=1, max_length=100)
dry_run: bool = False
class GovernanceSynchronizationOutcome(BaseModel):
assignment_id: str
template_id: str
tenant_id: str
kind: Literal["group", "role"]
operation: Literal["upsert", "remove"]
status: Literal["created", "updated", "unchanged", "removed", "absent", "blocked", "failed"]
resource_id: str | None = None
blocker_codes: list[str] = Field(default_factory=list)
message: str | None = None
provenance: dict[str, str] = Field(default_factory=dict)
class GovernanceSynchronizationResponse(BaseModel):
version: Literal["1"] = "1"
operation_id: str
dry_run: bool
outcomes: list[GovernanceSynchronizationOutcome]
counts: dict[str, int] = Field(default_factory=dict)