from __future__ import annotations from collections.abc import Iterable, Mapping from govoplan_core.core.access import ( AccessSemanticDirectory, AccessSubjectRef, AccountRef, FunctionAssignmentRef, FunctionRef, GroupRef, IdentityRef, OrganizationUnitRef, UserRef, ) from govoplan_core.core.identity import IdentityDirectory, IdentityRef as DirectoryIdentityRef from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef as IdmFunctionAssignmentRef from govoplan_core.core.organizations import ( ORGANIZATIONS_MODULE_ID, OrganizationDirectory, OrganizationFunctionRef as DirectoryFunctionRef, OrganizationUnitRef as DirectoryOrganizationUnitRef, ) from govoplan_access.backend.db.models import ( Account, Function, FunctionAssignment, FunctionRoleAssignment, Group, Identity, IdentityAccountLink, OrganizationUnit, User, ) from govoplan_core.db.session import get_database from govoplan_access.backend.semantic import active_function_assignments_for_account def _status(active: bool) -> str: return "active" if active else "inactive" def _account_ref(account: Account) -> AccountRef: return AccountRef( id=account.id, email=account.email, display_name=account.display_name, status=_status(account.is_active), # type: ignore[arg-type] ) def _user_ref(user: User, account: Account | None = None) -> UserRef: return UserRef( id=user.id, account_id=user.account_id, tenant_id=user.tenant_id, email=account.email if account else user.email, display_name=user.display_name or (account.display_name if account else None), status=_status(user.is_active), # type: ignore[arg-type] ) def _group_ref(group: Group) -> GroupRef: return GroupRef( id=group.id, tenant_id=group.tenant_id, name=group.name, status=_status(group.is_active), # type: ignore[arg-type] ) def _identity_ref(identity: Identity, account_links: list[IdentityAccountLink]) -> IdentityRef: primary_account_id = next((link.account_id for link in account_links if link.is_primary), None) return IdentityRef( id=identity.id, display_name=identity.display_name, primary_account_id=primary_account_id, account_ids=tuple(link.account_id for link in account_links), status=_status(identity.is_active), # type: ignore[arg-type] ) def _directory_identity_ref(identity: DirectoryIdentityRef) -> IdentityRef: return IdentityRef( id=identity.id, display_name=identity.display_name, primary_account_id=identity.primary_account_id, account_ids=tuple(identity.account_ids), status=identity.status, # type: ignore[arg-type] ) def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef: return OrganizationUnitRef( id=item.id, tenant_id=item.tenant_id, name=item.name, parent_id=item.parent_id, status=_status(item.is_active), # type: ignore[arg-type] ) def _directory_organization_unit_ref(item: DirectoryOrganizationUnitRef) -> OrganizationUnitRef: return OrganizationUnitRef( id=item.id, tenant_id=item.tenant_id, name=item.name, parent_id=item.parent_id, status=item.status, # type: ignore[arg-type] ) def _function_ref(function: Function, role_ids: Iterable[str]) -> FunctionRef: return FunctionRef( id=function.id, tenant_id=function.tenant_id, organization_unit_id=function.organization_unit_id, slug=function.slug, name=function.name, role_ids=tuple(role_ids), delegable=function.delegable, act_in_place_allowed=function.act_in_place_allowed, status=_status(function.is_active), # type: ignore[arg-type] ) def _directory_function_ref(function: DirectoryFunctionRef, role_ids: Iterable[str]) -> FunctionRef: return FunctionRef( id=function.id, tenant_id=function.tenant_id, organization_unit_id=function.organization_unit_id, slug=function.slug, name=function.name, role_ids=tuple(role_ids), delegable=function.delegable, act_in_place_allowed=function.act_in_place_allowed, status=function.status, # type: ignore[arg-type] ) def _function_assignment_ref(item: FunctionAssignment) -> FunctionAssignmentRef: return FunctionAssignmentRef( id=item.id, tenant_id=item.tenant_id, account_id=item.account_id, identity_id=item.identity_id, function_id=item.function_id, organization_unit_id=item.organization_unit_id, applies_to_subunits=item.applies_to_subunits, source=item.source, # type: ignore[arg-type] delegated_from_assignment_id=item.delegated_from_assignment_id, acting_for_account_id=item.acting_for_account_id, valid_from=item.valid_from, valid_until=item.valid_until, status=_status(item.is_active), # type: ignore[arg-type] ) def _idm_function_assignment_ref(item: IdmFunctionAssignmentRef, *, account_id: str) -> FunctionAssignmentRef: return FunctionAssignmentRef( id=item.id, tenant_id=item.tenant_id, account_id=item.account_id or account_id, identity_id=item.identity_id, function_id=item.function_id, organization_unit_id=item.organization_unit_id, applies_to_subunits=item.applies_to_subunits, source=item.source, # type: ignore[arg-type] delegated_from_assignment_id=item.delegated_from_assignment_id, acting_for_account_id=item.acting_for_account_id, valid_from=item.valid_from, valid_until=item.valid_until, status=item.status, # type: ignore[arg-type] ) class SqlAccessDirectory(AccessSemanticDirectory): def __init__( self, idm_directory: IdmDirectory | None = None, identity_directory: IdentityDirectory | None = None, organization_directory: OrganizationDirectory | None = None, ) -> None: self._idm_directory = idm_directory self._identity_directory = identity_directory self._organization_directory = organization_directory def get_account(self, account_id: str) -> AccountRef | None: with get_database().session() as session: account = session.get(Account, account_id) return _account_ref(account) if account is not None else None def get_user(self, user_id: str) -> UserRef | None: with get_database().session() as session: user = session.get(User, user_id) if user is None: return None account = session.get(Account, user.account_id) return _user_ref(user, account) def get_users(self, user_ids: Iterable[str]) -> Mapping[str, UserRef]: ids = {str(user_id) for user_id in user_ids if user_id} if not ids: return {} with get_database().session() as session: users = session.query(User).filter(User.id.in_(ids)).all() account_ids = {user.account_id for user in users} accounts = { account.id: account for account in session.query(Account).filter(Account.id.in_(account_ids)).all() } if account_ids else {} return {user.id: _user_ref(user, accounts.get(user.account_id)) for user in users} def users_for_tenant(self, tenant_id: str) -> tuple[UserRef, ...]: with get_database().session() as session: users = session.query(User).filter(User.tenant_id == tenant_id).order_by(User.display_name.asc(), User.email.asc()).all() account_ids = {user.account_id for user in users} accounts = { account.id: account for account in session.query(Account).filter(Account.id.in_(account_ids)).all() } if account_ids else {} return tuple(_user_ref(user, accounts.get(user.account_id)) for user in users) def get_group(self, group_id: str) -> GroupRef | None: with get_database().session() as session: group = session.get(Group, group_id) return _group_ref(group) if group is not None else None def get_groups(self, group_ids: Iterable[str]) -> Mapping[str, GroupRef]: ids = {str(group_id) for group_id in group_ids if group_id} if not ids: return {} with get_database().session() as session: groups = session.query(Group).filter(Group.id.in_(ids)).all() return {group.id: _group_ref(group) for group in groups} def groups_for_tenant(self, tenant_id: str) -> tuple[GroupRef, ...]: with get_database().session() as session: groups = session.query(Group).filter(Group.tenant_id == tenant_id).order_by(Group.name.asc()).all() return tuple(_group_ref(group) for group in groups) def groups_for_user(self, user_id: str, *, tenant_id: str) -> tuple[GroupRef, ...]: with get_database().session() as session: from govoplan_access.backend.db.models import UserGroupMembership groups = ( session.query(Group) .join(UserGroupMembership, UserGroupMembership.group_id == Group.id) .filter( UserGroupMembership.user_id == user_id, UserGroupMembership.tenant_id == tenant_id, Group.tenant_id == tenant_id, ) .order_by(Group.name.asc()) .all() ) return tuple(_group_ref(group) for group in groups) def display_label(self, subject: AccessSubjectRef) -> str | None: if subject.kind == "identity": identity = self.get_identity(subject.id) return identity.display_name if identity else subject.label if subject.kind == "account": account = self.get_account(subject.id) return account.display_name or account.email if account else subject.label if subject.kind == "user": user = self.get_user(subject.id) return user.display_name or user.email if user else subject.label if subject.kind == "group": group = self.get_group(subject.id) return group.name if group else subject.label if subject.kind == "organization_unit": item = self.get_organization_unit(subject.id) return item.name if item else subject.label if subject.kind == "function": item = self.get_function(subject.id) return item.name if item else subject.label return subject.label or subject.id def get_identity(self, identity_id: str) -> IdentityRef | None: if self._identity_directory is not None: identity = self._identity_directory.get_identity(identity_id) if identity is not None: return _directory_identity_ref(identity) with get_database().session() as session: identity = session.get(Identity, identity_id) if identity is None: return None links = ( session.query(IdentityAccountLink) .filter(IdentityAccountLink.identity_id == identity.id) .order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc()) .all() ) return _identity_ref(identity, links) def accounts_for_identity(self, identity_id: str) -> tuple[AccountRef, ...]: if self._identity_directory is not None: links = tuple(self._identity_directory.accounts_for_identity(identity_id)) if links: account_ids = [link.account_id for link in links] with get_database().session() as session: accounts = { account.id: account for account in session.query(Account).filter(Account.id.in_(account_ids)).all() } return tuple(_account_ref(accounts[account_id]) for account_id in account_ids if account_id in accounts) with get_database().session() as session: accounts = ( session.query(Account) .join(IdentityAccountLink, IdentityAccountLink.account_id == Account.id) .filter(IdentityAccountLink.identity_id == identity_id) .order_by(IdentityAccountLink.is_primary.desc(), Account.email.asc()) .all() ) return tuple(_account_ref(account) for account in accounts) def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None: if self._organization_directory is not None: item = self._organization_directory.get_organization_unit(organization_unit_id) if item is not None: return _directory_organization_unit_ref(item) with get_database().session() as session: item = session.get(OrganizationUnit, organization_unit_id) return _organization_unit_ref(item) if item is not None else None def organization_units_for_tenant(self, tenant_id: str) -> tuple[OrganizationUnitRef, ...]: if self._organization_directory is not None: items = tuple(self._organization_directory.organization_units_for_tenant(tenant_id)) if items: return tuple(_directory_organization_unit_ref(item) for item in items) with get_database().session() as session: items = ( session.query(OrganizationUnit) .filter(OrganizationUnit.tenant_id == tenant_id) .order_by(OrganizationUnit.name.asc()) .all() ) return tuple(_organization_unit_ref(item) for item in items) def get_function(self, function_id: str) -> FunctionRef | None: if self._organization_directory is not None: function = self._organization_directory.get_function(function_id) if function is not None: return _directory_function_ref(function, self._external_function_role_ids(function.id, tenant_id=function.tenant_id)) with get_database().session() as session: function = session.get(Function, function_id) if function is None: return None role_ids = [ row[0] for row in session.query(FunctionRoleAssignment.role_id) .filter(FunctionRoleAssignment.function_id == function.id) .order_by(FunctionRoleAssignment.created_at.asc()) .all() ] return _function_ref(function, role_ids) def functions_for_organization_unit( self, organization_unit_id: str, *, include_subunits: bool = False, ) -> tuple[FunctionRef, ...]: if self._organization_directory is not None: functions = tuple( self._organization_directory.functions_for_organization_unit( organization_unit_id, include_subunits=include_subunits, ) ) if functions: role_ids_by_function = self._external_function_role_ids_by_function( [item.id for item in functions], tenant_id=functions[0].tenant_id, ) return tuple( _directory_function_ref(item, role_ids_by_function.get(item.id, ())) for item in functions ) with get_database().session() as session: unit_ids = {organization_unit_id} if include_subunits: pending = [organization_unit_id] while pending: parent_id = pending.pop() children = [ row[0] for row in session.query(OrganizationUnit.id) .filter(OrganizationUnit.parent_id == parent_id) .all() ] for child_id in children: if child_id not in unit_ids: unit_ids.add(child_id) pending.append(child_id) functions = ( session.query(Function) .filter(Function.organization_unit_id.in_(unit_ids)) .order_by(Function.name.asc()) .all() ) role_rows = ( session.query(FunctionRoleAssignment.function_id, FunctionRoleAssignment.role_id) .filter(FunctionRoleAssignment.function_id.in_([item.id for item in functions])) .all() if functions else [] ) role_ids_by_function: dict[str, list[str]] = {} for function_id, role_id in role_rows: role_ids_by_function.setdefault(function_id, []).append(role_id) return tuple(_function_ref(item, role_ids_by_function.get(item.id, [])) for item in functions) def get_function_assignment(self, assignment_id: str) -> FunctionAssignmentRef | None: with get_database().session() as session: item = session.get(FunctionAssignment, assignment_id) return _function_assignment_ref(item) if item is not None else None def function_assignments_for_account( self, account_id: str, *, tenant_id: str | None = None, ) -> tuple[FunctionAssignmentRef, ...]: with get_database().session() as session: items = active_function_assignments_for_account(session, account_id, tenant_id=tenant_id) assignments = [_function_assignment_ref(item) for item in items] if self._idm_directory is not None: assignments.extend( _idm_function_assignment_ref(item, account_id=account_id) for item in self._idm_directory.organization_function_assignments_for_account( account_id, tenant_id=tenant_id, ) ) deduped = {item.id: item for item in assignments} return tuple(deduped.values()) def _external_function_role_ids(self, function_id: str, *, tenant_id: str) -> tuple[str, ...]: return self._external_function_role_ids_by_function([function_id], tenant_id=tenant_id).get(function_id, ()) def _external_function_role_ids_by_function( self, function_ids: Iterable[str], *, tenant_id: str, ) -> dict[str, tuple[str, ...]]: ids = sorted({str(function_id) for function_id in function_ids if function_id}) if not ids: return {} from govoplan_access.backend.db.models import ExternalFunctionRoleAssignment with get_database().session() as session: rows = ( session.query(ExternalFunctionRoleAssignment.function_id, ExternalFunctionRoleAssignment.role_id) .filter( ExternalFunctionRoleAssignment.tenant_id == tenant_id, ExternalFunctionRoleAssignment.source_module == ORGANIZATIONS_MODULE_ID, ExternalFunctionRoleAssignment.function_id.in_(ids), ) .order_by(ExternalFunctionRoleAssignment.created_at.asc()) .all() ) result: dict[str, list[str]] = {} for function_id, role_id in rows: result.setdefault(function_id, []).append(role_id) return {function_id: tuple(role_ids) for function_id, role_ids in result.items()}