468 lines
19 KiB
Python
468 lines
19 KiB
Python
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()}
|