feat: expose effective function incumbency contracts

This commit is contained in:
2026-07-29 14:16:29 +02:00
parent 5a8138ea03
commit 01c1f7e13a
7 changed files with 501 additions and 71 deletions
+114 -3
View File
@@ -17,6 +17,13 @@ from govoplan_core.core.configuration_control import (
ensure_configuration_change_allowed,
record_configuration_change_applied,
)
from govoplan_core.core.events import (
EventActorRef,
EventObjectRef,
EventTenantRef,
PlatformEvent,
emit_platform_event,
)
from govoplan_core.core.identity import (
CAPABILITY_IDENTITY_DIRECTORY,
CAPABILITY_IDENTITY_SEARCH,
@@ -31,6 +38,7 @@ from govoplan_core.core.organizations import (
)
from govoplan_core.core.runtime import get_registry
from govoplan_core.db.session import get_session
from govoplan_core.security.time import utc_now
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
from .schemas import (
@@ -413,6 +421,55 @@ def _record_assignment_audit(
)
def _publish_assignment_event(
session: Session,
principal: ApiPrincipal,
item: OrganizationFunctionAssignmentItem,
*,
event_type: str,
) -> None:
emit_platform_event(
session,
PlatformEvent(
type=event_type,
module_id="idm",
payload={
"identity_id": item.identity_id,
"account_id": item.account_id,
"function_id": item.function_id,
"organization_unit_id": item.organization_unit_id,
"source": item.source,
"delegated_from_assignment_id": (
item.delegated_from_assignment_id
),
"acting_for_account_id": item.acting_for_account_id,
"valid_from": (
item.valid_from.isoformat()
if item.valid_from is not None
else None
),
"valid_until": (
item.valid_until.isoformat()
if item.valid_until is not None
else None
),
"is_active": item.is_active,
},
actor=EventActorRef(type="account", id=principal.account_id),
tenant=EventTenantRef(id=principal.tenant_id),
subject=EventObjectRef(
type="organization_function",
id=item.function_id,
),
resource=EventObjectRef(
type="organization_function_assignment",
id=item.id,
),
classification="internal",
)
)
@router.get("/settings", response_model=IdmSettingsItem)
def get_idm_settings(
session: Session = Depends(get_session),
@@ -467,17 +524,34 @@ def update_idm_settings(
@router.get("/organization-function-assignments", response_model=OrganizationFunctionAssignmentList)
def list_organization_function_assignments(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=500, ge=1, le=1000),
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_any_scope(*IDM_ASSIGNMENT_READ_SCOPES)),
) -> OrganizationFunctionAssignmentList:
tenant_id = _tenant_id(principal)
assignments = (
query = (
session.query(IdmOrganizationFunctionAssignment)
.filter(IdmOrganizationFunctionAssignment.tenant_id == tenant_id)
.order_by(IdmOrganizationFunctionAssignment.created_at.asc())
)
total = query.order_by(None).count()
pages = max(1, (total + page_size - 1) // page_size)
assignments = (
query.order_by(
IdmOrganizationFunctionAssignment.created_at.asc(),
IdmOrganizationFunctionAssignment.id.asc(),
)
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return OrganizationFunctionAssignmentList(assignments=[_assignment_item(item) for item in assignments])
return OrganizationFunctionAssignmentList(
assignments=[_assignment_item(item) for item in assignments],
total=total,
page=page,
page_size=page_size,
pages=pages,
)
@router.post("/organization-function-assignments", response_model=OrganizationFunctionAssignmentItem, status_code=status.HTTP_201_CREATED)
@@ -523,6 +597,13 @@ def create_organization_function_assignment(
before=None,
after=after,
)
_publish_assignment_event(
session,
principal,
result,
event_type="idm.function_assignment.created.v1",
)
session.commit()
return result
@@ -586,6 +667,36 @@ def update_organization_function_assignment(
before=before,
after=after,
)
_publish_assignment_event(
session,
principal,
result,
event_type="idm.function_assignment.changed.v1",
)
before_values = before if isinstance(before, dict) else {}
if before_values.get("is_active") is True and result.is_active is False:
_publish_assignment_event(
session,
principal,
result,
event_type="idm.function_assignment.revoked.v1",
)
previous_valid_until = before_values.get("valid_until")
if (
result.valid_until is not None
and result.valid_until <= utc_now()
and (
previous_valid_until is None
or str(previous_valid_until) != result.valid_until.isoformat()
)
):
_publish_assignment_event(
session,
principal,
result,
event_type="idm.function_assignment.expired.v1",
)
session.commit()
return result
@@ -45,6 +45,10 @@ class OrganizationFunctionAssignmentItem(BaseModel):
class OrganizationFunctionAssignmentList(BaseModel):
assignments: list[OrganizationFunctionAssignmentItem]
total: int = 0
page: int = 1
page_size: int = 500
pages: int = 1
class OrganizationFunctionAssignmentCreateRequest(BaseModel):