refactor(idm): consume identity and organization contracts
This commit is contained in:
@@ -5,7 +5,6 @@ from typing import Any, TypeVar
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy import func, or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -18,11 +17,21 @@ from govoplan_core.core.configuration_control import (
|
||||
ensure_configuration_change_allowed,
|
||||
record_configuration_change_applied,
|
||||
)
|
||||
from govoplan_core.core.identity import (
|
||||
CAPABILITY_IDENTITY_DIRECTORY,
|
||||
CAPABILITY_IDENTITY_SEARCH,
|
||||
IdentityDirectory,
|
||||
IdentityRef,
|
||||
IdentitySearchProvider,
|
||||
)
|
||||
from govoplan_core.core.organizations import (
|
||||
CAPABILITY_ORGANIZATION_DIRECTORY,
|
||||
OrganizationDirectory,
|
||||
OrganizationFunctionRef,
|
||||
)
|
||||
from govoplan_core.core.runtime import get_registry
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_identity.backend.db.models import Identity, IdentityAccountLink
|
||||
from govoplan_idm.backend.db.models import IdmOrganizationFunctionAssignment, IdmTenantSettings
|
||||
from govoplan_organizations.backend.db.models import OrganizationFunction
|
||||
|
||||
from .schemas import (
|
||||
IdmSettingsItem,
|
||||
@@ -126,31 +135,76 @@ def _default_settings(tenant_id: str) -> IdmSettingsItem:
|
||||
)
|
||||
|
||||
|
||||
def _ensure_identity(session: Session, identity_id: str) -> None:
|
||||
identity = session.get(Identity, identity_id)
|
||||
if identity is None:
|
||||
def _identity_directory() -> IdentityDirectory:
|
||||
registry = get_registry()
|
||||
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Identity directory is unavailable",
|
||||
)
|
||||
capability = registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||
if not isinstance(capability, IdentityDirectory):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}",
|
||||
)
|
||||
return capability
|
||||
|
||||
|
||||
def _identity_search() -> IdentitySearchProvider:
|
||||
registry = get_registry()
|
||||
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_SEARCH):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Identity search is unavailable",
|
||||
)
|
||||
capability = registry.require_capability(CAPABILITY_IDENTITY_SEARCH)
|
||||
if not isinstance(capability, IdentitySearchProvider):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Invalid capability: {CAPABILITY_IDENTITY_SEARCH}",
|
||||
)
|
||||
return capability
|
||||
|
||||
|
||||
def _organization_directory() -> OrganizationDirectory:
|
||||
registry = get_registry()
|
||||
if registry is None or not registry.has_capability(CAPABILITY_ORGANIZATION_DIRECTORY):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Organization directory is unavailable",
|
||||
)
|
||||
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 _organization_function(function_id: str, tenant_id: str) -> OrganizationFunctionRef:
|
||||
function = _organization_directory().get_function(function_id)
|
||||
if function is None or function.tenant_id != tenant_id:
|
||||
raise _not_found("Organization function")
|
||||
return function
|
||||
|
||||
|
||||
def _ensure_identity(identity_id: str) -> None:
|
||||
if _identity_directory().get_identity(identity_id) is None:
|
||||
raise _not_found("Identity")
|
||||
|
||||
|
||||
def _ensure_account_link(session: Session, identity_id: str, account_id: str | None) -> None:
|
||||
def _ensure_account_link(identity_id: str, account_id: str | None) -> None:
|
||||
if account_id is None:
|
||||
return
|
||||
exists = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id == identity_id, IdentityAccountLink.account_id == account_id)
|
||||
.first()
|
||||
)
|
||||
if exists is None:
|
||||
links = _identity_directory().accounts_for_identity(identity_id)
|
||||
if not any(link.account_id == account_id for link in links):
|
||||
raise _invalid("Account is not linked to the selected identity.")
|
||||
|
||||
|
||||
def _account_linked_to_identity(session: Session, identity_id: str, account_id: str) -> bool:
|
||||
return (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id == identity_id, IdentityAccountLink.account_id == account_id)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
def _account_linked_to_identity(identity_id: str, account_id: str) -> bool:
|
||||
return any(link.account_id == account_id for link in _identity_directory().accounts_for_identity(identity_id))
|
||||
|
||||
|
||||
def _ensure_assignment_workflow(
|
||||
@@ -160,13 +214,13 @@ def _ensure_assignment_workflow(
|
||||
tenant_id: str,
|
||||
) -> None:
|
||||
_ensure_assignment_shape(item)
|
||||
function = _get_tenant_row(session, OrganizationFunction, item.function_id, tenant_id, "Organization function")
|
||||
function = _organization_function(item.function_id, tenant_id)
|
||||
base = _assignment_source_assignment(session, item, tenant_id=tenant_id)
|
||||
_ensure_assignment_source_rules(
|
||||
item,
|
||||
function=function,
|
||||
base=base,
|
||||
account_linked_to_identity=lambda identity_id, account_id: _account_linked_to_identity(session, identity_id, account_id),
|
||||
account_linked_to_identity=_account_linked_to_identity,
|
||||
)
|
||||
|
||||
|
||||
@@ -198,7 +252,7 @@ def _assignment_source_assignment(
|
||||
def _ensure_assignment_source_rules(
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
function: OrganizationFunction,
|
||||
function: OrganizationFunctionRef,
|
||||
base: IdmOrganizationFunctionAssignment | None,
|
||||
account_linked_to_identity: Callable[[str, str], bool],
|
||||
) -> None:
|
||||
@@ -214,7 +268,7 @@ def _ensure_assignment_source_rules(
|
||||
def _ensure_delegated_assignment(
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
function: OrganizationFunction,
|
||||
function: OrganizationFunctionRef,
|
||||
base: IdmOrganizationFunctionAssignment | None,
|
||||
) -> None:
|
||||
if base is None:
|
||||
@@ -230,7 +284,7 @@ def _ensure_delegated_assignment(
|
||||
def _ensure_acting_for_assignment(
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
function: OrganizationFunction,
|
||||
function: OrganizationFunctionRef,
|
||||
base: IdmOrganizationFunctionAssignment | None,
|
||||
account_linked_to_identity: Callable[[str, str], bool],
|
||||
) -> None:
|
||||
@@ -434,9 +488,9 @@ def create_organization_function_assignment(
|
||||
) -> OrganizationFunctionAssignmentItem:
|
||||
tenant_id = _tenant_id(principal)
|
||||
approval, target, _value = _ensure_assignment_change_allowed(session, principal, tenant_id=tenant_id, operation="created", payload=payload)
|
||||
_ensure_identity(session, payload.identity_id)
|
||||
_ensure_account_link(session, payload.identity_id, payload.account_id)
|
||||
function = _get_tenant_row(session, OrganizationFunction, payload.function_id, tenant_id, "Organization function")
|
||||
_ensure_identity(payload.identity_id)
|
||||
_ensure_account_link(payload.identity_id, payload.account_id)
|
||||
function = _organization_function(payload.function_id, tenant_id)
|
||||
item = IdmOrganizationFunctionAssignment(
|
||||
tenant_id=tenant_id,
|
||||
identity_id=payload.identity_id,
|
||||
@@ -486,7 +540,7 @@ def update_organization_function_assignment(
|
||||
if "function_id" in payload.model_fields_set:
|
||||
if payload.function_id is None:
|
||||
raise _invalid("Function is required.")
|
||||
function = _get_tenant_row(session, OrganizationFunction, payload.function_id, tenant_id, "Organization function")
|
||||
function = _organization_function(payload.function_id, tenant_id)
|
||||
item.function_id = function.id
|
||||
item.organization_unit_id = function.organization_unit_id
|
||||
identity_id = payload.identity_id if "identity_id" in payload.model_fields_set else item.identity_id
|
||||
@@ -494,9 +548,9 @@ def update_organization_function_assignment(
|
||||
if "identity_id" in payload.model_fields_set:
|
||||
if payload.identity_id is None:
|
||||
raise _invalid("Identity is required.")
|
||||
_ensure_identity(session, payload.identity_id)
|
||||
_ensure_identity(payload.identity_id)
|
||||
item.identity_id = payload.identity_id
|
||||
_ensure_account_link(session, identity_id, account_id)
|
||||
_ensure_account_link(identity_id, account_id)
|
||||
if "source" in payload.model_fields_set and payload.source is None:
|
||||
raise _invalid("Assignment source is required.")
|
||||
if "applies_to_subunits" in payload.model_fields_set and payload.applies_to_subunits is None:
|
||||
@@ -543,53 +597,25 @@ def list_organization_identity_candidates(
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(require_any_scope(*ORGANIZATION_IDENTITY_READ_SCOPES)),
|
||||
) -> OrganizationIdentityCandidateList:
|
||||
del principal
|
||||
identity_query = session.query(Identity)
|
||||
if not include_inactive:
|
||||
identity_query = identity_query.filter(Identity.is_active.is_(True))
|
||||
if query:
|
||||
pattern = f"%{query.strip().casefold()}%"
|
||||
matching_account_links = session.query(IdentityAccountLink.identity_id).filter(
|
||||
func.lower(IdentityAccountLink.account_id).like(pattern)
|
||||
)
|
||||
identity_query = identity_query.filter(
|
||||
or_(
|
||||
func.lower(Identity.id).like(pattern),
|
||||
func.lower(Identity.display_name).like(pattern),
|
||||
func.lower(Identity.external_subject).like(pattern),
|
||||
Identity.id.in_(matching_account_links),
|
||||
)
|
||||
)
|
||||
|
||||
identities = identity_query.order_by(Identity.display_name.asc(), Identity.id.asc()).limit(limit).all()
|
||||
identity_ids = [identity.id for identity in identities]
|
||||
links_by_identity: dict[str, list[IdentityAccountLink]] = {identity_id: [] for identity_id in identity_ids}
|
||||
if identity_ids:
|
||||
links = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id.in_(identity_ids))
|
||||
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.account_id.asc())
|
||||
.all()
|
||||
)
|
||||
for link in links:
|
||||
links_by_identity.setdefault(link.identity_id, []).append(link)
|
||||
del session, principal
|
||||
identities = _identity_search().search_identities(
|
||||
query,
|
||||
include_inactive=include_inactive,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return OrganizationIdentityCandidateList(
|
||||
identities=[
|
||||
_identity_candidate(identity, links_by_identity.get(identity.id, []))
|
||||
for identity in identities
|
||||
]
|
||||
identities=[_identity_candidate(identity) for identity in identities]
|
||||
)
|
||||
|
||||
|
||||
def _identity_candidate(identity: Identity, links: list[IdentityAccountLink]) -> OrganizationIdentityCandidate:
|
||||
primary_link = next((link for link in links if link.is_primary), None)
|
||||
def _identity_candidate(identity: IdentityRef) -> OrganizationIdentityCandidate:
|
||||
return OrganizationIdentityCandidate(
|
||||
id=identity.id,
|
||||
display_name=identity.display_name,
|
||||
external_subject=identity.external_subject,
|
||||
source=identity.source,
|
||||
primary_account_id=primary_link.account_id if primary_link is not None else None,
|
||||
account_ids=[link.account_id for link in links],
|
||||
status="active" if identity.is_active else "inactive",
|
||||
primary_account_id=identity.primary_account_id,
|
||||
account_ids=list(identity.account_ids),
|
||||
status=identity.status,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user