Sync GovOPlaN module state
This commit is contained in:
@@ -63,6 +63,11 @@ from govoplan_access.backend.api.v1.admin_schemas import (
|
||||
GroupListResponse,
|
||||
GroupSummary,
|
||||
GroupUpdateRequest,
|
||||
ExternalFunctionRoleMappingCreateRequest,
|
||||
ExternalFunctionRoleMappingItem,
|
||||
ExternalFunctionRoleMappingListDeltaResponse,
|
||||
ExternalFunctionRoleMappingListResponse,
|
||||
ExternalFunctionRoleMappingUpdateRequest,
|
||||
FunctionAdminItem,
|
||||
FunctionAssignmentAdminItem,
|
||||
FunctionAssignmentCreateRequest,
|
||||
@@ -148,6 +153,7 @@ from govoplan_core.core.configuration_control import (
|
||||
record_configuration_change_applied,
|
||||
)
|
||||
from govoplan_core.core.configuration_safety import configuration_safety_catalog, plan_configuration_change
|
||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
from govoplan_core.core.change_sequence import (
|
||||
decode_sequence_watermark,
|
||||
@@ -160,6 +166,7 @@ from govoplan_core.core.change_sequence import (
|
||||
from govoplan_access.backend.db.models import (
|
||||
Account,
|
||||
ApiKey,
|
||||
ExternalFunctionRoleAssignment,
|
||||
Function,
|
||||
FunctionAssignment,
|
||||
FunctionDelegation,
|
||||
@@ -190,6 +197,7 @@ ACCESS_ROLES_COLLECTION = "access.admin.roles"
|
||||
ACCESS_IDENTITIES_COLLECTION = "access.admin.identities"
|
||||
ACCESS_ORGANIZATION_UNITS_COLLECTION = "access.admin.organization_units"
|
||||
ACCESS_FUNCTIONS_COLLECTION = "access.admin.functions"
|
||||
ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION = "access.admin.external_function_role_mappings"
|
||||
ACCESS_FUNCTION_ASSIGNMENTS_COLLECTION = "access.admin.function_assignments"
|
||||
ACCESS_FUNCTION_DELEGATIONS_COLLECTION = "access.admin.function_delegations"
|
||||
ACCESS_SYSTEM_ROLES_COLLECTION = "access.admin.system_roles"
|
||||
@@ -439,6 +447,60 @@ def _set_function_roles(session: Session, *, function: Function, role_ids: list[
|
||||
session.flush()
|
||||
|
||||
|
||||
def _external_function_role_mapping_item(item: ExternalFunctionRoleAssignment) -> ExternalFunctionRoleMappingItem:
|
||||
return ExternalFunctionRoleMappingItem(
|
||||
id=item.id,
|
||||
tenant_id=item.tenant_id,
|
||||
source_module=item.source_module,
|
||||
function_id=item.function_id,
|
||||
role_id=item.role_id,
|
||||
settings=item.settings or {},
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _validate_external_function_role(
|
||||
session: Session,
|
||||
*,
|
||||
tenant_id: str,
|
||||
role_id: str,
|
||||
) -> Role:
|
||||
role = session.query(Role).filter(Role.tenant_id == tenant_id, Role.id == role_id, Role.is_assignable.is_(True)).one_or_none()
|
||||
if role is None:
|
||||
raise AdminValidationError("Selected role is invalid or not assignable.")
|
||||
return role
|
||||
|
||||
|
||||
def _organization_directory_or_error() -> OrganizationDirectory:
|
||||
registry = get_registry()
|
||||
if registry is None or not registry.has_capability(CAPABILITY_ORGANIZATION_DIRECTORY):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail={
|
||||
"code": "organizations_required",
|
||||
"message": "External function role mappings require the Organizations module.",
|
||||
},
|
||||
)
|
||||
capability = registry.require_capability(CAPABILITY_ORGANIZATION_DIRECTORY)
|
||||
if not isinstance(capability, OrganizationDirectory):
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Invalid capability: {CAPABILITY_ORGANIZATION_DIRECTORY}")
|
||||
return capability
|
||||
|
||||
|
||||
def _validate_external_function_source(*, tenant_id: str, source_module: str, function_id: str) -> None:
|
||||
if source_module != ORGANIZATIONS_MODULE_ID:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=f"Unsupported function source module: {source_module}",
|
||||
)
|
||||
function = _organization_directory_or_error().get_function(function_id)
|
||||
if function is None or function.tenant_id != tenant_id:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Organization function not found.")
|
||||
if function.status != "active":
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Organization function is not active.")
|
||||
|
||||
|
||||
def _function_assignment_item(item: FunctionAssignment) -> FunctionAssignmentAdminItem:
|
||||
return FunctionAssignmentAdminItem(
|
||||
id=item.id,
|
||||
@@ -1292,6 +1354,206 @@ def deactivate_function(
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/external-function-role-mappings", response_model=ExternalFunctionRoleMappingListResponse)
|
||||
def list_external_function_role_mappings(
|
||||
tenant_id: str | None = None,
|
||||
source_module: str | None = None,
|
||||
function_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:role:read")),
|
||||
):
|
||||
tenant = _resolve_tenant(session, principal, tenant_id)
|
||||
query = session.query(ExternalFunctionRoleAssignment).filter(ExternalFunctionRoleAssignment.tenant_id == tenant.id)
|
||||
if source_module:
|
||||
query = query.filter(ExternalFunctionRoleAssignment.source_module == source_module.strip())
|
||||
if function_id:
|
||||
query = query.filter(ExternalFunctionRoleAssignment.function_id == function_id.strip())
|
||||
items = query.order_by(ExternalFunctionRoleAssignment.source_module.asc(), ExternalFunctionRoleAssignment.function_id.asc()).all()
|
||||
return ExternalFunctionRoleMappingListResponse(mappings=[_external_function_role_mapping_item(item) for item in items])
|
||||
|
||||
|
||||
def _full_external_function_role_mappings_delta_response(session: Session, tenant: Tenant) -> ExternalFunctionRoleMappingListDeltaResponse:
|
||||
items = (
|
||||
session.query(ExternalFunctionRoleAssignment)
|
||||
.filter(ExternalFunctionRoleAssignment.tenant_id == tenant.id)
|
||||
.order_by(ExternalFunctionRoleAssignment.source_module.asc(), ExternalFunctionRoleAssignment.function_id.asc())
|
||||
.all()
|
||||
)
|
||||
return ExternalFunctionRoleMappingListDeltaResponse(
|
||||
mappings=[_external_function_role_mapping_item(item) for item in items],
|
||||
deleted=[],
|
||||
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,)),
|
||||
has_more=False,
|
||||
full=True,
|
||||
)
|
||||
|
||||
|
||||
def _external_function_role_mappings_delta_response(
|
||||
session: Session,
|
||||
tenant: Tenant,
|
||||
*,
|
||||
since: str,
|
||||
limit: int,
|
||||
) -> ExternalFunctionRoleMappingListDeltaResponse:
|
||||
entries, has_more = _access_delta_entries(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
collections=(ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,),
|
||||
since=since,
|
||||
limit=limit,
|
||||
)
|
||||
if entries is None:
|
||||
return _full_external_function_role_mappings_delta_response(session, tenant)
|
||||
changed_ids = _changed_ids(entries, "access_external_function_role_mapping")
|
||||
visible = {
|
||||
item.id: item
|
||||
for item in (
|
||||
session.query(ExternalFunctionRoleAssignment)
|
||||
.filter(ExternalFunctionRoleAssignment.tenant_id == tenant.id, ExternalFunctionRoleAssignment.id.in_(changed_ids))
|
||||
.all()
|
||||
if changed_ids
|
||||
else []
|
||||
)
|
||||
}
|
||||
deleted = [
|
||||
_delta_deleted_item(entry)
|
||||
for entry in entries
|
||||
if entry.resource_type == "access_external_function_role_mapping" and entry.resource_id not in visible
|
||||
]
|
||||
return ExternalFunctionRoleMappingListDeltaResponse(
|
||||
mappings=[_external_function_role_mapping_item(item) for item in visible.values()],
|
||||
deleted=deleted,
|
||||
watermark=_access_delta_response_watermark(
|
||||
session,
|
||||
tenant_id=tenant.id,
|
||||
collections=(ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,),
|
||||
entries=entries,
|
||||
has_more=has_more,
|
||||
),
|
||||
has_more=has_more,
|
||||
full=False,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/external-function-role-mappings/delta", response_model=ExternalFunctionRoleMappingListDeltaResponse)
|
||||
def list_external_function_role_mappings_delta(
|
||||
tenant_id: str | None = Query(default=None),
|
||||
since: str | None = None,
|
||||
limit: int = Query(default=500, ge=1, le=1000),
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:read", "access:function:read", "access:role:read")),
|
||||
):
|
||||
tenant = _resolve_tenant(session, principal, tenant_id)
|
||||
if since is None:
|
||||
return _full_external_function_role_mappings_delta_response(session, tenant)
|
||||
return _external_function_role_mappings_delta_response(session, tenant, since=since, limit=limit)
|
||||
|
||||
|
||||
@router.post("/external-function-role-mappings", response_model=ExternalFunctionRoleMappingItem, status_code=status.HTTP_201_CREATED)
|
||||
def create_external_function_role_mapping(
|
||||
payload: ExternalFunctionRoleMappingCreateRequest,
|
||||
tenant_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:write", "access:function:write", "access:role:assign")),
|
||||
):
|
||||
tenant = _resolve_tenant(session, principal, tenant_id)
|
||||
source_module = payload.source_module.strip() or ORGANIZATIONS_MODULE_ID
|
||||
function_id = payload.function_id.strip()
|
||||
if not function_id:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Function ID is required.")
|
||||
_validate_external_function_source(tenant_id=tenant.id, source_module=source_module, function_id=function_id)
|
||||
_validate_external_function_role(session, tenant_id=tenant.id, role_id=payload.role_id)
|
||||
item = ExternalFunctionRoleAssignment(
|
||||
tenant_id=tenant.id,
|
||||
source_module=source_module,
|
||||
function_id=function_id,
|
||||
role_id=payload.role_id,
|
||||
settings=payload.settings,
|
||||
)
|
||||
session.add(item)
|
||||
try:
|
||||
session.flush()
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="This external function role mapping already exists.") from exc
|
||||
_record_access_change(
|
||||
session,
|
||||
collection=ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,
|
||||
resource_type="access_external_function_role_mapping",
|
||||
resource_id=item.id,
|
||||
operation="created",
|
||||
tenant_id=tenant.id,
|
||||
principal=principal,
|
||||
payload={"source_module": source_module, "function_id": function_id, "role_id": payload.role_id},
|
||||
)
|
||||
session.commit()
|
||||
return _external_function_role_mapping_item(item)
|
||||
|
||||
|
||||
@router.patch("/external-function-role-mappings/{mapping_id}", response_model=ExternalFunctionRoleMappingItem)
|
||||
def update_external_function_role_mapping(
|
||||
mapping_id: str,
|
||||
payload: ExternalFunctionRoleMappingUpdateRequest,
|
||||
tenant_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:write", "access:function:write", "access:role:assign")),
|
||||
):
|
||||
tenant = _resolve_tenant(session, principal, tenant_id)
|
||||
item = session.query(ExternalFunctionRoleAssignment).filter(ExternalFunctionRoleAssignment.id == mapping_id, ExternalFunctionRoleAssignment.tenant_id == tenant.id).one_or_none()
|
||||
if item is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="External function role mapping not found")
|
||||
_validate_external_function_source(tenant_id=tenant.id, source_module=item.source_module, function_id=item.function_id)
|
||||
if payload.role_id is not None:
|
||||
_validate_external_function_role(session, tenant_id=tenant.id, role_id=payload.role_id)
|
||||
item.role_id = payload.role_id
|
||||
if payload.settings is not None:
|
||||
item.settings = payload.settings
|
||||
session.add(item)
|
||||
try:
|
||||
session.flush()
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="This external function role mapping already exists.") from exc
|
||||
_record_access_change(
|
||||
session,
|
||||
collection=ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,
|
||||
resource_type="access_external_function_role_mapping",
|
||||
resource_id=item.id,
|
||||
operation="updated",
|
||||
tenant_id=tenant.id,
|
||||
principal=principal,
|
||||
payload={"source_module": item.source_module, "function_id": item.function_id, "role_id": item.role_id},
|
||||
)
|
||||
session.commit()
|
||||
return _external_function_role_mapping_item(item)
|
||||
|
||||
|
||||
@router.delete("/external-function-role-mappings/{mapping_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_external_function_role_mapping(
|
||||
mapping_id: str,
|
||||
tenant_id: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope("admin:roles:write", "access:function:write", "access:role:assign")),
|
||||
):
|
||||
tenant = _resolve_tenant(session, principal, tenant_id)
|
||||
item = session.query(ExternalFunctionRoleAssignment).filter(ExternalFunctionRoleAssignment.id == mapping_id, ExternalFunctionRoleAssignment.tenant_id == tenant.id).one_or_none()
|
||||
if item is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="External function role mapping not found")
|
||||
session.delete(item)
|
||||
_record_access_change(
|
||||
session,
|
||||
collection=ACCESS_EXTERNAL_FUNCTION_ROLE_MAPPINGS_COLLECTION,
|
||||
resource_type="access_external_function_role_mapping",
|
||||
resource_id=mapping_id,
|
||||
operation="deleted",
|
||||
tenant_id=tenant.id,
|
||||
principal=principal,
|
||||
payload={"source_module": item.source_module, "function_id": item.function_id, "role_id": item.role_id},
|
||||
)
|
||||
session.commit()
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/function-assignments", response_model=FunctionAssignmentListResponse)
|
||||
def list_function_assignments(
|
||||
tenant_id: str | None = None,
|
||||
|
||||
Reference in New Issue
Block a user