Prefer canonical identity and organization directories
This commit is contained in:
@@ -43,6 +43,7 @@ from govoplan_access.backend.db.models import (
|
|||||||
UserGroupMembership,
|
UserGroupMembership,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import OrganizationDirectory
|
||||||
from govoplan_access.backend.permissions.catalog import effective_permission_count, expand_scopes
|
from govoplan_access.backend.permissions.catalog import effective_permission_count, expand_scopes
|
||||||
|
|
||||||
|
|
||||||
@@ -143,13 +144,22 @@ def _user_item(
|
|||||||
*,
|
*,
|
||||||
owner_ids: set[str] | None = None,
|
owner_ids: set[str] | None = None,
|
||||||
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> UserAdminItem:
|
) -> UserAdminItem:
|
||||||
account = session.get(Account, user.account_id)
|
account = session.get(Account, user.account_id)
|
||||||
if account is None:
|
if account is None:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="User account is missing")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="User account is missing")
|
||||||
groups = collect_user_groups(session, user)
|
groups = collect_user_groups(session, user)
|
||||||
roles = collect_direct_user_roles(session, user)
|
roles = collect_direct_user_roles(session, user)
|
||||||
external_roles = collect_external_function_roles(session, user, idm_assignments) if idm_assignments else []
|
external_roles = (
|
||||||
|
collect_external_function_roles(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_assignments,
|
||||||
|
organization_directory=organization_directory,
|
||||||
|
)
|
||||||
|
if idm_assignments else []
|
||||||
|
)
|
||||||
effective_scopes = set(collect_user_scopes(session, user, include_system=False))
|
effective_scopes = set(collect_user_scopes(session, user, include_system=False))
|
||||||
for role in external_roles:
|
for role in external_roles:
|
||||||
effective_scopes.update(role.permissions or [])
|
effective_scopes.update(role.permissions or [])
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from govoplan_core.api.v1.schemas import (
|
|||||||
UserUiPreferences,
|
UserUiPreferences,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.access import AuthMethod, PrincipalRef
|
from govoplan_core.core.access import AuthMethod, PrincipalRef
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal
|
from govoplan_access.backend.auth.dependencies import ApiPrincipal, get_api_principal
|
||||||
from govoplan_core.admin.settings import get_system_settings
|
from govoplan_core.admin.settings import get_system_settings
|
||||||
from govoplan_core.audit.logging import audit_from_principal
|
from govoplan_core.audit.logging import audit_from_principal
|
||||||
@@ -176,6 +178,16 @@ def _language_context(session: Session, *, tenant: Tenant, user: User) -> dict[s
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _identity_directory_from_request(request: Request) -> IdentityDirectory | None:
|
||||||
|
registry = getattr(request.app.state, "govoplan_registry", None)
|
||||||
|
if not isinstance(registry, PlatformRegistry) or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
return None
|
||||||
|
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 _resolve_login_user(session: Session, payload: LoginRequest) -> tuple[Account, User, Tenant]:
|
def _resolve_login_user(session: Session, payload: LoginRequest) -> tuple[Account, User, Tenant]:
|
||||||
account = (
|
account = (
|
||||||
session.query(Account)
|
session.query(Account)
|
||||||
@@ -215,6 +227,8 @@ def _me_response(
|
|||||||
service_account_id: str | None = None,
|
service_account_id: str | None = None,
|
||||||
include_system: bool = True,
|
include_system: bool = True,
|
||||||
include_all_memberships: bool = True,
|
include_all_memberships: bool = True,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
identity_id: str | None = None,
|
||||||
) -> MeResponse:
|
) -> MeResponse:
|
||||||
tenant_roles = collect_user_roles(session, user)
|
tenant_roles = collect_user_roles(session, user)
|
||||||
system_roles = collect_system_roles(session, account) if include_system else []
|
system_roles = collect_system_roles(session, account) if include_system else []
|
||||||
@@ -248,7 +262,7 @@ def _me_response(
|
|||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
membership_id=user.id,
|
membership_id=user.id,
|
||||||
tenant_id=tenant.id,
|
tenant_id=tenant.id,
|
||||||
identity_id=identity_id_for_account(session, account.id),
|
identity_id=identity_id or identity_id_for_account(session, account.id, identity_directory=identity_directory),
|
||||||
scopes=frozenset(scopes),
|
scopes=frozenset(scopes),
|
||||||
group_ids=frozenset(group.id for group in groups),
|
group_ids=frozenset(group.id for group in groups),
|
||||||
role_ids=frozenset(role.id for role in tenant_roles + system_roles),
|
role_ids=frozenset(role.id for role in tenant_roles + system_roles),
|
||||||
@@ -271,7 +285,8 @@ def _me_response(
|
|||||||
@router.post("/login", response_model=LoginResponse)
|
@router.post("/login", response_model=LoginResponse)
|
||||||
def login(payload: LoginRequest, request: Request, response: Response, session: Session = Depends(get_session)):
|
def login(payload: LoginRequest, request: Request, response: Response, session: Session = Depends(get_session)):
|
||||||
account, user, tenant = _resolve_login_user(session, payload)
|
account, user, tenant = _resolve_login_user(session, payload)
|
||||||
me_payload = _me_response(session, account=account, user=user, tenant=tenant)
|
identity_directory = _identity_directory_from_request(request)
|
||||||
|
me_payload = _me_response(session, account=account, user=user, tenant=tenant, identity_directory=identity_directory)
|
||||||
maintenance_mode = saved_maintenance_mode(session)
|
maintenance_mode = saved_maintenance_mode(session)
|
||||||
if maintenance_mode.enabled and not scopes_grant(me_payload.scopes, MAINTENANCE_ACCESS_SCOPE):
|
if maintenance_mode.enabled and not scopes_grant(me_payload.scopes, MAINTENANCE_ACCESS_SCOPE):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -300,6 +315,7 @@ def login(payload: LoginRequest, request: Request, response: Response, session:
|
|||||||
effective_scopes=me_payload.scopes,
|
effective_scopes=me_payload.scopes,
|
||||||
auth_method="session",
|
auth_method="session",
|
||||||
session_id=created.model.id,
|
session_id=created.model.id,
|
||||||
|
identity_directory=identity_directory,
|
||||||
).model_dump(),
|
).model_dump(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -321,6 +337,7 @@ def me(principal: ApiPrincipal = Depends(get_api_principal), session: Session =
|
|||||||
service_account_id=principal.principal.service_account_id,
|
service_account_id=principal.principal.service_account_id,
|
||||||
include_system=principal.auth_session is not None,
|
include_system=principal.auth_session is not None,
|
||||||
include_all_memberships=principal.auth_session is not None,
|
include_all_memberships=principal.auth_session is not None,
|
||||||
|
identity_id=principal.principal.identity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -401,6 +418,7 @@ def update_profile(
|
|||||||
session_id=principal.session_id,
|
session_id=principal.session_id,
|
||||||
include_system=True,
|
include_system=True,
|
||||||
include_all_memberships=True,
|
include_all_memberships=True,
|
||||||
|
identity_id=principal.principal.identity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -430,6 +448,7 @@ def switch_tenant(
|
|||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
auth_method="session",
|
auth_method="session",
|
||||||
session_id=principal.session_id,
|
session_id=principal.session_id,
|
||||||
|
identity_id=principal.principal.identity_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ from govoplan_core.core.configuration_control import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.configuration_safety import configuration_safety_catalog, plan_configuration_change
|
from govoplan_core.core.configuration_safety import configuration_safety_catalog, plan_configuration_change
|
||||||
from govoplan_core.core.access import CAPABILITY_ACCESS_EXPLANATION, AccessExplanationService, AccessDecisionProvenance, PrincipalRef
|
from govoplan_core.core.access import CAPABILITY_ACCESS_EXPLANATION, AccessExplanationService, AccessDecisionProvenance, PrincipalRef
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory, OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory, OrganizationFunctionAssignmentRef
|
||||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||||
@@ -507,6 +508,16 @@ def _optional_organization_directory() -> OrganizationDirectory | None:
|
|||||||
return capability
|
return capability
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_identity_directory() -> IdentityDirectory | None:
|
||||||
|
registry = get_registry()
|
||||||
|
if registry is None or not registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
return None
|
||||||
|
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 _optional_idm_directory() -> IdmDirectory | None:
|
def _optional_idm_directory() -> IdmDirectory | None:
|
||||||
registry = get_registry()
|
registry = get_registry()
|
||||||
if registry is None or not registry.has_capability(CAPABILITY_IDM_DIRECTORY):
|
if registry is None or not registry.has_capability(CAPABILITY_IDM_DIRECTORY):
|
||||||
@@ -527,6 +538,7 @@ def _access_explanation_service_or_error() -> AccessExplanationService:
|
|||||||
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
||||||
|
|
||||||
return SqlAccessExplanationService(
|
return SqlAccessExplanationService(
|
||||||
|
identity_directory=_optional_identity_directory(),
|
||||||
idm_directory=_optional_idm_directory(),
|
idm_directory=_optional_idm_directory(),
|
||||||
organization_directory=_optional_organization_directory(),
|
organization_directory=_optional_organization_directory(),
|
||||||
)
|
)
|
||||||
@@ -541,12 +553,19 @@ def _idm_assignments_for_user(
|
|||||||
return tuple(idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id))
|
return tuple(idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id))
|
||||||
|
|
||||||
|
|
||||||
def _principal_ref_for_user(session: Session, user: User, *, idm_directory: IdmDirectory | None = None) -> PrincipalRef:
|
def _principal_ref_for_user(
|
||||||
|
session: Session,
|
||||||
|
user: User,
|
||||||
|
*,
|
||||||
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
|
) -> PrincipalRef:
|
||||||
account = session.get(Account, user.account_id)
|
account = session.get(Account, user.account_id)
|
||||||
if account is None:
|
if account is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||||
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
||||||
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments))
|
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments, organization_directory=organization_directory))
|
||||||
scopes = set(collect_user_scopes(session, user, include_system=True))
|
scopes = set(collect_user_scopes(session, user, include_system=True))
|
||||||
for role in idm_roles:
|
for role in idm_roles:
|
||||||
scopes.update(role.permissions or [])
|
scopes.update(role.permissions or [])
|
||||||
@@ -558,7 +577,7 @@ def _principal_ref_for_user(session: Session, user: User, *, idm_directory: IdmD
|
|||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
membership_id=user.id,
|
membership_id=user.id,
|
||||||
tenant_id=user.tenant_id,
|
tenant_id=user.tenant_id,
|
||||||
identity_id=identity_id_for_account(session, account.id),
|
identity_id=identity_id_for_account(session, account.id, identity_directory=identity_directory),
|
||||||
scopes=frozenset(sorted(scopes)),
|
scopes=frozenset(sorted(scopes)),
|
||||||
group_ids=frozenset(group.id for group in collect_user_groups(session, user)),
|
group_ids=frozenset(group.id for group in collect_user_groups(session, user)),
|
||||||
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
||||||
@@ -1937,8 +1956,9 @@ def _full_users_delta_response(session: Session, tenant: Tenant) -> UserListDelt
|
|||||||
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
||||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
return UserListDeltaResponse(
|
return UserListDeltaResponse(
|
||||||
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory) for user in users],
|
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory, organization_directory=organization_directory) for user in users],
|
||||||
deleted=[],
|
deleted=[],
|
||||||
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_USERS_COLLECTION,)),
|
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_USERS_COLLECTION,)),
|
||||||
has_more=False,
|
has_more=False,
|
||||||
@@ -1960,13 +1980,14 @@ def _users_delta_response(session: Session, tenant: Tenant, *, since: str, limit
|
|||||||
}
|
}
|
||||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
deleted = [
|
deleted = [
|
||||||
_delta_deleted_item(entry)
|
_delta_deleted_item(entry)
|
||||||
for entry in entries
|
for entry in entries
|
||||||
if entry.resource_type == "access_user" and entry.resource_id not in visible
|
if entry.resource_type == "access_user" and entry.resource_id not in visible
|
||||||
]
|
]
|
||||||
return UserListDeltaResponse(
|
return UserListDeltaResponse(
|
||||||
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory) for user in visible.values()],
|
users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory, organization_directory=organization_directory) for user in visible.values()],
|
||||||
deleted=deleted,
|
deleted=deleted,
|
||||||
watermark=_access_delta_response_watermark(session, tenant_id=tenant.id, collections=(ACCESS_USERS_COLLECTION,), entries=entries, has_more=has_more),
|
watermark=_access_delta_response_watermark(session, tenant_id=tenant.id, collections=(ACCESS_USERS_COLLECTION,), entries=entries, has_more=has_more),
|
||||||
has_more=has_more,
|
has_more=has_more,
|
||||||
@@ -1980,9 +2001,16 @@ def _user_item_for_response(
|
|||||||
*,
|
*,
|
||||||
owner_ids: set[str] | None = None,
|
owner_ids: set[str] | None = None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> UserAdminItem:
|
) -> UserAdminItem:
|
||||||
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
idm_assignments = _idm_assignments_for_user(idm_directory, user)
|
||||||
return _user_item(session, user, owner_ids=owner_ids, idm_assignments=idm_assignments)
|
return _user_item(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
owner_ids=owner_ids,
|
||||||
|
idm_assignments=idm_assignments,
|
||||||
|
organization_directory=organization_directory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/delta", response_model=UserListDeltaResponse)
|
@router.get("/users/delta", response_model=UserListDeltaResponse)
|
||||||
@@ -2009,7 +2037,8 @@ def list_users(
|
|||||||
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
users = session.query(User).filter(User.tenant_id == tenant.id).order_by(User.display_name.asc(), User.email.asc()).all()
|
||||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
return UserListResponse(users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory) for user in users])
|
organization_directory = _optional_organization_directory()
|
||||||
|
return UserListResponse(users=[_user_item_for_response(session, user, owner_ids=owner_ids, idm_directory=idm_directory, organization_directory=organization_directory) for user in users])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/users/{user_id}/access-explanation", response_model=UserAccessExplanationResponse)
|
@router.get("/users/{user_id}/access-explanation", response_model=UserAccessExplanationResponse)
|
||||||
@@ -2024,15 +2053,16 @@ def get_user_access_explanation(
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
explanation = build_user_access_explanation(
|
explanation = build_user_access_explanation(
|
||||||
session,
|
session,
|
||||||
user,
|
user,
|
||||||
idm_directory=idm_directory,
|
idm_directory=idm_directory,
|
||||||
organization_directory=_optional_organization_directory(),
|
organization_directory=organization_directory,
|
||||||
include_system=False,
|
include_system=False,
|
||||||
)
|
)
|
||||||
return UserAccessExplanationResponse(
|
return UserAccessExplanationResponse(
|
||||||
user=_user_item_for_response(session, user, idm_directory=idm_directory),
|
user=_user_item_for_response(session, user, idm_directory=idm_directory, organization_directory=organization_directory),
|
||||||
role_sources=[source.to_dict() for source in explanation.role_sources],
|
role_sources=[source.to_dict() for source in explanation.role_sources],
|
||||||
scopes=[scope.to_dict() for scope in explanation.scopes],
|
scopes=[scope.to_dict() for scope in explanation.scopes],
|
||||||
function_facts=[fact.to_dict() for fact in explanation.function_facts],
|
function_facts=[fact.to_dict() for fact in explanation.function_facts],
|
||||||
@@ -2054,7 +2084,15 @@ def get_resource_access_explanation(
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
target_principal = _principal_ref_for_user(session, user, idm_directory=idm_directory)
|
identity_directory = _optional_identity_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
|
target_principal = _principal_ref_for_user(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_directory=idm_directory,
|
||||||
|
identity_directory=identity_directory,
|
||||||
|
organization_directory=organization_directory,
|
||||||
|
)
|
||||||
provenance = _access_explanation_service_or_error().explain_resource_provenance(
|
provenance = _access_explanation_service_or_error().explain_resource_provenance(
|
||||||
target_principal,
|
target_principal,
|
||||||
resource_type=resource_type,
|
resource_type=resource_type,
|
||||||
@@ -2062,7 +2100,7 @@ def get_resource_access_explanation(
|
|||||||
action=action,
|
action=action,
|
||||||
)
|
)
|
||||||
return ResourceAccessExplanationResponse(
|
return ResourceAccessExplanationResponse(
|
||||||
user=_user_item_for_response(session, user, idm_directory=idm_directory),
|
user=_user_item_for_response(session, user, idm_directory=idm_directory, organization_directory=organization_directory),
|
||||||
resource_type=resource_type,
|
resource_type=resource_type,
|
||||||
resource_id=resource_id,
|
resource_id=resource_id,
|
||||||
action=action,
|
action=action,
|
||||||
@@ -2152,8 +2190,9 @@ def create_user(
|
|||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
idm_directory = _optional_idm_directory()
|
idm_directory = _optional_idm_directory()
|
||||||
|
organization_directory = _optional_organization_directory()
|
||||||
return UserCreateResponse(
|
return UserCreateResponse(
|
||||||
user=_user_item_for_response(session, result.user, idm_directory=idm_directory),
|
user=_user_item_for_response(session, result.user, idm_directory=idm_directory, organization_directory=organization_directory),
|
||||||
account_created=result.account_created,
|
account_created=result.account_created,
|
||||||
temporary_password=result.temporary_password,
|
temporary_password=result.temporary_password,
|
||||||
)
|
)
|
||||||
@@ -2234,7 +2273,12 @@ def update_user(
|
|||||||
details=payload.model_dump(exclude_none=True),
|
details=payload.model_dump(exclude_none=True),
|
||||||
)
|
)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _user_item_for_response(session, user, idm_directory=_optional_idm_directory())
|
return _user_item_for_response(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_directory=_optional_idm_directory(),
|
||||||
|
organization_directory=_optional_organization_directory(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _full_groups_delta_response(session: Session, tenant: Tenant) -> GroupListDeltaResponse:
|
def _full_groups_delta_response(session: Session, tenant: Tenant) -> GroupListDeltaResponse:
|
||||||
@@ -3311,7 +3355,12 @@ def create_tenant_api_key(
|
|||||||
user = session.query(User).filter(User.id == user_id, User.tenant_id == tenant.id, User.is_active.is_(True)).one_or_none()
|
user = session.query(User).filter(User.id == user_id, User.tenant_id == tenant.id, User.is_active.is_(True)).one_or_none()
|
||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Active user not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Active user not found")
|
||||||
user_scopes = _user_item_for_response(session, user, idm_directory=_optional_idm_directory()).effective_scopes
|
user_scopes = _user_item_for_response(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
idm_directory=_optional_idm_directory(),
|
||||||
|
organization_directory=_optional_organization_directory(),
|
||||||
|
).effective_scopes
|
||||||
requested = payload.scopes or ["campaign:read"]
|
requested = payload.scopes or ["campaign:read"]
|
||||||
invalid = [scope for scope in requested if scope.startswith("system:") or not scopes_grant(user_scopes, scope)]
|
invalid = [scope for scope in requested if scope.startswith("system:") or not scopes_grant(user_scopes, scope)]
|
||||||
if invalid:
|
if invalid:
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ from govoplan_core.core.access import (
|
|||||||
PrincipalRef,
|
PrincipalRef,
|
||||||
PrincipalResolver,
|
PrincipalResolver,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory
|
||||||
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import OrganizationDirectory
|
||||||
from govoplan_core.core.modules import AccessDecision
|
from govoplan_core.core.modules import AccessDecision
|
||||||
from govoplan_core.core.registry import PlatformRegistry
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
|
from govoplan_core.core.maintenance import MAINTENANCE_ACCESS_SCOPE, maintenance_response_detail, saved_maintenance_mode
|
||||||
@@ -63,6 +65,7 @@ def _build_principal_ref(
|
|||||||
api_key: ApiKey | None = None,
|
api_key: ApiKey | None = None,
|
||||||
auth_session: AuthSession | None = None,
|
auth_session: AuthSession | None = None,
|
||||||
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
idm_assignments: tuple[OrganizationFunctionAssignmentRef, ...] = (),
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
extra_roles: tuple[Role, ...] = (),
|
extra_roles: tuple[Role, ...] = (),
|
||||||
) -> PrincipalRef:
|
) -> PrincipalRef:
|
||||||
function_assignment_ids = list(collect_function_assignment_ids(session, user))
|
function_assignment_ids = list(collect_function_assignment_ids(session, user))
|
||||||
@@ -74,7 +77,7 @@ def _build_principal_ref(
|
|||||||
account_id=account.id,
|
account_id=account.id,
|
||||||
membership_id=user.id,
|
membership_id=user.id,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
identity_id=identity_id_for_account(session, account.id),
|
identity_id=identity_id_for_account(session, account.id, identity_directory=identity_directory),
|
||||||
scopes=frozenset(scopes),
|
scopes=frozenset(scopes),
|
||||||
group_ids=_principal_group_ids(session, user),
|
group_ids=_principal_group_ids(session, user),
|
||||||
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
role_ids=frozenset(sorted(dict.fromkeys(role_ids))),
|
||||||
@@ -128,6 +131,8 @@ def _resolve_legacy_principal_ref(
|
|||||||
authorization: str | None,
|
authorization: str | None,
|
||||||
x_api_key: str | None,
|
x_api_key: str | None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> PrincipalRef:
|
) -> PrincipalRef:
|
||||||
token, source = _extract_token(request, authorization, x_api_key)
|
token, source = _extract_token(request, authorization, x_api_key)
|
||||||
if not token:
|
if not token:
|
||||||
@@ -147,7 +152,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
):
|
):
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive or inconsistent API-key principal")
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive or inconsistent API-key principal")
|
||||||
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=api_key.tenant_id)
|
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=api_key.tenant_id)
|
||||||
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments))
|
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments, organization_directory=organization_directory))
|
||||||
user_scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=False), idm_roles)
|
user_scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=False), idm_roles)
|
||||||
effective_scopes = intersect_api_key_scopes(user_scopes, api_key.scopes or [])
|
effective_scopes = intersect_api_key_scopes(user_scopes, api_key.scopes or [])
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -160,6 +165,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
scopes=effective_scopes,
|
scopes=effective_scopes,
|
||||||
auth_method="api_key",
|
auth_method="api_key",
|
||||||
idm_assignments=idm_assignments,
|
idm_assignments=idm_assignments,
|
||||||
|
identity_directory=identity_directory,
|
||||||
extra_roles=idm_roles,
|
extra_roles=idm_roles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,7 +187,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
if not header_token or not cookie_token or header_token != cookie_token or not verify_auth_session_csrf(auth_session, header_token):
|
if not header_token or not cookie_token or header_token != cookie_token or not verify_auth_session_csrf(auth_session, header_token):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid or missing CSRF token")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Invalid or missing CSRF token")
|
||||||
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=user.tenant_id)
|
idm_assignments = _idm_assignments_for_account(idm_directory, account.id, tenant_id=user.tenant_id)
|
||||||
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments))
|
idm_roles = tuple(collect_external_function_roles(session, user, idm_assignments, organization_directory=organization_directory))
|
||||||
scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=True), idm_roles)
|
scopes = _scopes_with_extra_roles(collect_user_scopes(session, user, include_system=True), idm_roles)
|
||||||
session.commit()
|
session.commit()
|
||||||
return _build_principal_ref(
|
return _build_principal_ref(
|
||||||
@@ -193,6 +199,7 @@ def _resolve_legacy_principal_ref(
|
|||||||
scopes=scopes,
|
scopes=scopes,
|
||||||
auth_method="session",
|
auth_method="session",
|
||||||
idm_assignments=idm_assignments,
|
idm_assignments=idm_assignments,
|
||||||
|
identity_directory=identity_directory,
|
||||||
extra_roles=idm_roles,
|
extra_roles=idm_roles,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -257,8 +264,15 @@ def _permission_evaluator_from_request(request: Request) -> PermissionEvaluator
|
|||||||
|
|
||||||
|
|
||||||
class LegacyPrincipalResolver:
|
class LegacyPrincipalResolver:
|
||||||
def __init__(self, idm_directory: IdmDirectory | None = None) -> None:
|
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._idm_directory = idm_directory
|
||||||
|
self._identity_directory = identity_directory
|
||||||
|
self._organization_directory = organization_directory
|
||||||
|
|
||||||
def resolve_request(self, request: object, *, session: object | None = None) -> PrincipalRef:
|
def resolve_request(self, request: object, *, session: object | None = None) -> PrincipalRef:
|
||||||
if not isinstance(request, Request):
|
if not isinstance(request, Request):
|
||||||
@@ -272,6 +286,8 @@ class LegacyPrincipalResolver:
|
|||||||
authorization=authorization,
|
authorization=authorization,
|
||||||
x_api_key=x_api_key,
|
x_api_key=x_api_key,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
with get_database().session() as managed_session:
|
with get_database().session() as managed_session:
|
||||||
return _resolve_legacy_principal_ref(
|
return _resolve_legacy_principal_ref(
|
||||||
@@ -280,6 +296,8 @@ class LegacyPrincipalResolver:
|
|||||||
authorization=authorization,
|
authorization=authorization,
|
||||||
x_api_key=x_api_key,
|
x_api_key=x_api_key,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ from govoplan_core.core.access import (
|
|||||||
OrganizationUnitRef,
|
OrganizationUnitRef,
|
||||||
UserRef,
|
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.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 (
|
from govoplan_access.backend.db.models import (
|
||||||
Account,
|
Account,
|
||||||
Function,
|
Function,
|
||||||
@@ -73,6 +80,16 @@ def _identity_ref(identity: Identity, account_links: list[IdentityAccountLink])
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
|
||||||
return OrganizationUnitRef(
|
return OrganizationUnitRef(
|
||||||
id=item.id,
|
id=item.id,
|
||||||
@@ -83,6 +100,16 @@ def _organization_unit_ref(item: OrganizationUnit) -> OrganizationUnitRef:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def _function_ref(function: Function, role_ids: Iterable[str]) -> FunctionRef:
|
||||||
return FunctionRef(
|
return FunctionRef(
|
||||||
id=function.id,
|
id=function.id,
|
||||||
@@ -97,6 +124,20 @@ def _function_ref(function: Function, role_ids: Iterable[str]) -> FunctionRef:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def _function_assignment_ref(item: FunctionAssignment) -> FunctionAssignmentRef:
|
||||||
return FunctionAssignmentRef(
|
return FunctionAssignmentRef(
|
||||||
id=item.id,
|
id=item.id,
|
||||||
@@ -134,8 +175,15 @@ def _idm_function_assignment_ref(item: IdmFunctionAssignmentRef, *, account_id:
|
|||||||
|
|
||||||
|
|
||||||
class SqlAccessDirectory(AccessSemanticDirectory):
|
class SqlAccessDirectory(AccessSemanticDirectory):
|
||||||
def __init__(self, idm_directory: IdmDirectory | None = None) -> None:
|
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._idm_directory = idm_directory
|
||||||
|
self._identity_directory = identity_directory
|
||||||
|
self._organization_directory = organization_directory
|
||||||
|
|
||||||
def get_account(self, account_id: str) -> AccountRef | None:
|
def get_account(self, account_id: str) -> AccountRef | None:
|
||||||
with get_database().session() as session:
|
with get_database().session() as session:
|
||||||
@@ -230,6 +278,10 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return subject.label or subject.id
|
return subject.label or subject.id
|
||||||
|
|
||||||
def get_identity(self, identity_id: str) -> IdentityRef | None:
|
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:
|
with get_database().session() as session:
|
||||||
identity = session.get(Identity, identity_id)
|
identity = session.get(Identity, identity_id)
|
||||||
if identity is None:
|
if identity is None:
|
||||||
@@ -243,6 +295,16 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return _identity_ref(identity, links)
|
return _identity_ref(identity, links)
|
||||||
|
|
||||||
def accounts_for_identity(self, identity_id: str) -> tuple[AccountRef, ...]:
|
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:
|
with get_database().session() as session:
|
||||||
accounts = (
|
accounts = (
|
||||||
session.query(Account)
|
session.query(Account)
|
||||||
@@ -254,11 +316,19 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return tuple(_account_ref(account) for account in accounts)
|
return tuple(_account_ref(account) for account in accounts)
|
||||||
|
|
||||||
def get_organization_unit(self, organization_unit_id: str) -> OrganizationUnitRef | None:
|
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:
|
with get_database().session() as session:
|
||||||
item = session.get(OrganizationUnit, organization_unit_id)
|
item = session.get(OrganizationUnit, organization_unit_id)
|
||||||
return _organization_unit_ref(item) if item is not None else None
|
return _organization_unit_ref(item) if item is not None else None
|
||||||
|
|
||||||
def organization_units_for_tenant(self, tenant_id: str) -> tuple[OrganizationUnitRef, ...]:
|
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:
|
with get_database().session() as session:
|
||||||
items = (
|
items = (
|
||||||
session.query(OrganizationUnit)
|
session.query(OrganizationUnit)
|
||||||
@@ -269,6 +339,10 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
return tuple(_organization_unit_ref(item) for item in items)
|
return tuple(_organization_unit_ref(item) for item in items)
|
||||||
|
|
||||||
def get_function(self, function_id: str) -> FunctionRef | None:
|
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:
|
with get_database().session() as session:
|
||||||
function = session.get(Function, function_id)
|
function = session.get(Function, function_id)
|
||||||
if function is None:
|
if function is None:
|
||||||
@@ -288,6 +362,22 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
*,
|
*,
|
||||||
include_subunits: bool = False,
|
include_subunits: bool = False,
|
||||||
) -> tuple[FunctionRef, ...]:
|
) -> 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:
|
with get_database().session() as session:
|
||||||
unit_ids = {organization_unit_id}
|
unit_ids = {organization_unit_id}
|
||||||
if include_subunits:
|
if include_subunits:
|
||||||
@@ -345,3 +435,33 @@ class SqlAccessDirectory(AccessSemanticDirectory):
|
|||||||
)
|
)
|
||||||
deduped = {item.id: item for item in assignments}
|
deduped = {item.id: item for item in assignments}
|
||||||
return tuple(deduped.values())
|
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()}
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ from govoplan_access.backend.semantic import (
|
|||||||
identity_id_for_account,
|
identity_id_for_account,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.access import AccessDecisionProvenance, AccessExplanationService, PrincipalRef, ResourceAccessExplanationProvider
|
from govoplan_core.core.access import AccessDecisionProvenance, AccessExplanationService, PrincipalRef, ResourceAccessExplanationProvider
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory
|
||||||
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import IdmDirectory, OrganizationFunctionAssignmentRef
|
||||||
from govoplan_core.core.organizations import OrganizationDirectory
|
from govoplan_core.core.organizations import ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||||
from govoplan_core.db.session import get_database
|
from govoplan_core.db.session import get_database
|
||||||
from govoplan_access.backend.permissions.catalog import expand_scopes, scopes_grant
|
from govoplan_access.backend.permissions.catalog import expand_scopes, scopes_grant
|
||||||
|
|
||||||
@@ -159,10 +160,12 @@ class SqlAccessExplanationService(AccessExplanationService):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
organization_directory: OrganizationDirectory | None = None,
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
resource_explanation_providers: Iterable[ResourceAccessExplanationProvider] = (),
|
resource_explanation_providers: Iterable[ResourceAccessExplanationProvider] = (),
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._identity_directory = identity_directory
|
||||||
self._idm_directory = idm_directory
|
self._idm_directory = idm_directory
|
||||||
self._organization_directory = organization_directory
|
self._organization_directory = organization_directory
|
||||||
self._resource_explanation_providers = tuple(resource_explanation_providers)
|
self._resource_explanation_providers = tuple(resource_explanation_providers)
|
||||||
@@ -178,6 +181,7 @@ class SqlAccessExplanationService(AccessExplanationService):
|
|||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
required_scope,
|
required_scope,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
organization_directory=self._organization_directory,
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
@@ -196,6 +200,7 @@ class SqlAccessExplanationService(AccessExplanationService):
|
|||||||
session,
|
session,
|
||||||
principal,
|
principal,
|
||||||
action,
|
action,
|
||||||
|
identity_directory=self._identity_directory,
|
||||||
idm_directory=self._idm_directory,
|
idm_directory=self._idm_directory,
|
||||||
organization_directory=self._organization_directory,
|
organization_directory=self._organization_directory,
|
||||||
)
|
)
|
||||||
@@ -250,12 +255,17 @@ def _scope_provenance(
|
|||||||
principal: PrincipalRef,
|
principal: PrincipalRef,
|
||||||
required_scope: str,
|
required_scope: str,
|
||||||
*,
|
*,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
idm_directory: IdmDirectory | None = None,
|
idm_directory: IdmDirectory | None = None,
|
||||||
organization_directory: OrganizationDirectory | None = None,
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> list[AccessDecisionProvenance]:
|
) -> list[AccessDecisionProvenance]:
|
||||||
items: list[AccessDecisionProvenance] = []
|
items: list[AccessDecisionProvenance] = []
|
||||||
account = session.get(Account, principal.account_id)
|
account = session.get(Account, principal.account_id)
|
||||||
identity_id = principal.identity_id or identity_id_for_account(session, principal.account_id)
|
identity_id = principal.identity_id or identity_id_for_account(
|
||||||
|
session,
|
||||||
|
principal.account_id,
|
||||||
|
identity_directory=identity_directory,
|
||||||
|
)
|
||||||
if identity_id:
|
if identity_id:
|
||||||
items.append(AccessDecisionProvenance(kind="identity", id=identity_id, source="identity_account_link"))
|
items.append(AccessDecisionProvenance(kind="identity", id=identity_id, source="identity_account_link"))
|
||||||
items.append(
|
items.append(
|
||||||
@@ -421,6 +431,12 @@ def _idm_function_role_sources(
|
|||||||
for assignment in idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id)
|
for assignment in idm_directory.organization_function_assignments_for_account(user.account_id, tenant_id=user.tenant_id)
|
||||||
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
||||||
]
|
]
|
||||||
|
if organization_directory is not None:
|
||||||
|
assignments = [
|
||||||
|
assignment
|
||||||
|
for assignment in assignments
|
||||||
|
if _organization_function_active(organization_directory, assignment)
|
||||||
|
]
|
||||||
if not assignments:
|
if not assignments:
|
||||||
return [], []
|
return [], []
|
||||||
function_ids = sorted({assignment.function_id for assignment in assignments})
|
function_ids = sorted({assignment.function_id for assignment in assignments})
|
||||||
@@ -429,6 +445,7 @@ def _idm_function_role_sources(
|
|||||||
.join(Role, ExternalFunctionRoleAssignment.role_id == Role.id)
|
.join(Role, ExternalFunctionRoleAssignment.role_id == Role.id)
|
||||||
.filter(
|
.filter(
|
||||||
ExternalFunctionRoleAssignment.tenant_id == user.tenant_id,
|
ExternalFunctionRoleAssignment.tenant_id == user.tenant_id,
|
||||||
|
ExternalFunctionRoleAssignment.source_module == ORGANIZATIONS_MODULE_ID,
|
||||||
ExternalFunctionRoleAssignment.function_id.in_(function_ids),
|
ExternalFunctionRoleAssignment.function_id.in_(function_ids),
|
||||||
Role.tenant_id == user.tenant_id,
|
Role.tenant_id == user.tenant_id,
|
||||||
)
|
)
|
||||||
@@ -446,7 +463,7 @@ def _idm_function_role_sources(
|
|||||||
mappings = mappings_by_function.get(assignment.function_id, [])
|
mappings = mappings_by_function.get(assignment.function_id, [])
|
||||||
function_facts.append(
|
function_facts.append(
|
||||||
FunctionFactExplanation(
|
FunctionFactExplanation(
|
||||||
source_module="organizations",
|
source_module=ORGANIZATIONS_MODULE_ID,
|
||||||
assignment_id=assignment.id,
|
assignment_id=assignment.id,
|
||||||
tenant_id=assignment.tenant_id,
|
tenant_id=assignment.tenant_id,
|
||||||
identity_id=assignment.identity_id,
|
identity_id=assignment.identity_id,
|
||||||
@@ -501,6 +518,14 @@ def _organization_labels(
|
|||||||
return (function.name if function is not None else None, unit.name if unit is not None else None)
|
return (function.name if function is not None else None, unit.name if unit is not None else None)
|
||||||
|
|
||||||
|
|
||||||
|
def _organization_function_active(
|
||||||
|
organization_directory: OrganizationDirectory,
|
||||||
|
assignment: OrganizationFunctionAssignmentRef,
|
||||||
|
) -> bool:
|
||||||
|
function = organization_directory.get_function(assignment.function_id)
|
||||||
|
return function is not None and function.tenant_id == assignment.tenant_id and function.status == "active"
|
||||||
|
|
||||||
|
|
||||||
def _system_role_sources(session: Session, account: Account) -> list[AccessRoleSourceExplanation]:
|
def _system_role_sources(session: Session, account: Account) -> list[AccessRoleSourceExplanation]:
|
||||||
roles = (
|
roles = (
|
||||||
session.query(Role)
|
session.query(Role)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from govoplan_core.core.access import (
|
|||||||
)
|
)
|
||||||
from govoplan_core.core.campaigns import CAPABILITY_CAMPAIGNS_ACCESS
|
from govoplan_core.core.campaigns import CAPABILITY_CAMPAIGNS_ACCESS
|
||||||
from govoplan_core.core.files import CAPABILITY_FILES_ACCESS
|
from govoplan_core.core.files import CAPABILITY_FILES_ACCESS
|
||||||
|
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY, IdentityDirectory
|
||||||
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory
|
from govoplan_core.core.idm import CAPABILITY_IDM_DIRECTORY, IdmDirectory
|
||||||
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory
|
from govoplan_core.core.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, OrganizationDirectory
|
||||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||||
@@ -455,8 +456,11 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = (
|
|||||||
def _legacy_principal_resolver(context: ModuleContext) -> object:
|
def _legacy_principal_resolver(context: ModuleContext) -> object:
|
||||||
from govoplan_access.backend.auth.dependencies import LegacyPrincipalResolver
|
from govoplan_access.backend.auth.dependencies import LegacyPrincipalResolver
|
||||||
|
|
||||||
idm_directory = _optional_idm_directory(context)
|
return LegacyPrincipalResolver(
|
||||||
return LegacyPrincipalResolver(idm_directory=idm_directory)
|
idm_directory=_optional_idm_directory(context),
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
|
organization_directory=_optional_organization_directory(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _legacy_permission_evaluator(context: ModuleContext) -> object:
|
def _legacy_permission_evaluator(context: ModuleContext) -> object:
|
||||||
@@ -483,13 +487,30 @@ def _tenant_context_switcher(context: ModuleContext) -> object:
|
|||||||
def _access_directory(context: ModuleContext) -> object:
|
def _access_directory(context: ModuleContext) -> object:
|
||||||
from govoplan_access.backend.directory import SqlAccessDirectory
|
from govoplan_access.backend.directory import SqlAccessDirectory
|
||||||
|
|
||||||
return SqlAccessDirectory(idm_directory=_optional_idm_directory(context))
|
return SqlAccessDirectory(
|
||||||
|
idm_directory=_optional_idm_directory(context),
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
|
organization_directory=_optional_organization_directory(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _access_semantic_directory(context: ModuleContext) -> object:
|
def _access_semantic_directory(context: ModuleContext) -> object:
|
||||||
from govoplan_access.backend.directory import SqlAccessDirectory
|
from govoplan_access.backend.directory import SqlAccessDirectory
|
||||||
|
|
||||||
return SqlAccessDirectory(idm_directory=_optional_idm_directory(context))
|
return SqlAccessDirectory(
|
||||||
|
idm_directory=_optional_idm_directory(context),
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
|
organization_directory=_optional_organization_directory(context),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_identity_directory(context: ModuleContext) -> IdentityDirectory | None:
|
||||||
|
if not context.registry.has_capability(CAPABILITY_IDENTITY_DIRECTORY):
|
||||||
|
return None
|
||||||
|
capability = context.registry.require_capability(CAPABILITY_IDENTITY_DIRECTORY)
|
||||||
|
if not isinstance(capability, IdentityDirectory):
|
||||||
|
raise RuntimeError(f"Invalid capability: {CAPABILITY_IDENTITY_DIRECTORY}")
|
||||||
|
return capability
|
||||||
|
|
||||||
|
|
||||||
def _optional_idm_directory(context: ModuleContext) -> IdmDirectory | None:
|
def _optional_idm_directory(context: ModuleContext) -> IdmDirectory | None:
|
||||||
@@ -525,6 +546,7 @@ def _access_explanation_service(context: ModuleContext) -> object:
|
|||||||
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
from govoplan_access.backend.explanation import SqlAccessExplanationService
|
||||||
|
|
||||||
return SqlAccessExplanationService(
|
return SqlAccessExplanationService(
|
||||||
|
identity_directory=_optional_identity_directory(context),
|
||||||
idm_directory=_optional_idm_directory(context),
|
idm_directory=_optional_idm_directory(context),
|
||||||
organization_directory=_optional_organization_directory(context),
|
organization_directory=_optional_organization_directory(context),
|
||||||
resource_explanation_providers=_resource_explanation_providers(context),
|
resource_explanation_providers=_resource_explanation_providers(context),
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ from govoplan_access.backend.db.models import (
|
|||||||
Role,
|
Role,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.identity import IdentityDirectory
|
||||||
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
from govoplan_core.core.idm import OrganizationFunctionAssignmentRef
|
||||||
|
from govoplan_core.core.organizations import ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||||
from govoplan_core.security.time import utc_now
|
from govoplan_core.security.time import utc_now
|
||||||
|
|
||||||
|
|
||||||
@@ -35,7 +37,16 @@ def primary_identity_for_account(session: Session, account_id: str) -> Identity
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def identity_id_for_account(session: Session, account_id: str) -> str | None:
|
def identity_id_for_account(
|
||||||
|
session: Session,
|
||||||
|
account_id: str,
|
||||||
|
*,
|
||||||
|
identity_directory: IdentityDirectory | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
if identity_directory is not None:
|
||||||
|
identity = identity_directory.identity_for_account(account_id)
|
||||||
|
if identity is not None and identity.status == "active":
|
||||||
|
return identity.id
|
||||||
identity = primary_identity_for_account(session, account_id)
|
identity = primary_identity_for_account(session, account_id)
|
||||||
return identity.id if identity is not None else None
|
return identity.id if identity is not None else None
|
||||||
|
|
||||||
@@ -163,13 +174,20 @@ def collect_external_function_roles(
|
|||||||
user: User,
|
user: User,
|
||||||
assignments: Iterable[OrganizationFunctionAssignmentRef],
|
assignments: Iterable[OrganizationFunctionAssignmentRef],
|
||||||
*,
|
*,
|
||||||
source_module: str = "organizations",
|
source_module: str = ORGANIZATIONS_MODULE_ID,
|
||||||
|
organization_directory: OrganizationDirectory | None = None,
|
||||||
) -> list[Role]:
|
) -> list[Role]:
|
||||||
function_ids = sorted({
|
function_ids = sorted({
|
||||||
assignment.function_id
|
assignment.function_id
|
||||||
for assignment in assignments
|
for assignment in assignments
|
||||||
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
if assignment.tenant_id == user.tenant_id and assignment.status == "active"
|
||||||
})
|
})
|
||||||
|
if organization_directory is not None and source_module == ORGANIZATIONS_MODULE_ID:
|
||||||
|
function_ids = [
|
||||||
|
function_id
|
||||||
|
for function_id in function_ids
|
||||||
|
if _organization_function_active(organization_directory, function_id, tenant_id=user.tenant_id)
|
||||||
|
]
|
||||||
if not function_ids:
|
if not function_ids:
|
||||||
return []
|
return []
|
||||||
return (
|
return (
|
||||||
@@ -184,3 +202,13 @@ def collect_external_function_roles(
|
|||||||
.order_by(Role.name.asc())
|
.order_by(Role.name.asc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _organization_function_active(
|
||||||
|
organization_directory: OrganizationDirectory,
|
||||||
|
function_id: str,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
) -> bool:
|
||||||
|
function = organization_directory.get_function(function_id)
|
||||||
|
return function is not None and function.tenant_id == tenant_id and function.status == "active"
|
||||||
|
|||||||
Reference in New Issue
Block a user