Prefer canonical identity and organization directories
This commit is contained in:
@@ -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.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.organizations import CAPABILITY_ORGANIZATION_DIRECTORY, ORGANIZATIONS_MODULE_ID, OrganizationDirectory
|
||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
||||
@@ -507,6 +508,16 @@ def _optional_organization_directory() -> OrganizationDirectory | None:
|
||||
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:
|
||||
registry = get_registry()
|
||||
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
|
||||
|
||||
return SqlAccessExplanationService(
|
||||
identity_directory=_optional_identity_directory(),
|
||||
idm_directory=_optional_idm_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))
|
||||
|
||||
|
||||
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)
|
||||
if account is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Account not found")
|
||||
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))
|
||||
for role in idm_roles:
|
||||
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,
|
||||
membership_id=user.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)),
|
||||
group_ids=frozenset(group.id for group in collect_user_groups(session, user)),
|
||||
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()
|
||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||
idm_directory = _optional_idm_directory()
|
||||
organization_directory = _optional_organization_directory()
|
||||
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=[],
|
||||
watermark=_access_delta_watermark(session, tenant.id, (ACCESS_USERS_COLLECTION,)),
|
||||
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)
|
||||
idm_directory = _optional_idm_directory()
|
||||
organization_directory = _optional_organization_directory()
|
||||
deleted = [
|
||||
_delta_deleted_item(entry)
|
||||
for entry in entries
|
||||
if entry.resource_type == "access_user" and entry.resource_id not in visible
|
||||
]
|
||||
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,
|
||||
watermark=_access_delta_response_watermark(session, tenant_id=tenant.id, collections=(ACCESS_USERS_COLLECTION,), entries=entries, has_more=has_more),
|
||||
has_more=has_more,
|
||||
@@ -1980,9 +2001,16 @@ def _user_item_for_response(
|
||||
*,
|
||||
owner_ids: set[str] | None = None,
|
||||
idm_directory: IdmDirectory | None = None,
|
||||
organization_directory: OrganizationDirectory | None = None,
|
||||
) -> UserAdminItem:
|
||||
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)
|
||||
@@ -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()
|
||||
owner_ids = tenant_owner_user_ids(session, tenant.id)
|
||||
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)
|
||||
@@ -2024,15 +2053,16 @@ def get_user_access_explanation(
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
idm_directory = _optional_idm_directory()
|
||||
organization_directory = _optional_organization_directory()
|
||||
explanation = build_user_access_explanation(
|
||||
session,
|
||||
user,
|
||||
idm_directory=idm_directory,
|
||||
organization_directory=_optional_organization_directory(),
|
||||
organization_directory=organization_directory,
|
||||
include_system=False,
|
||||
)
|
||||
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],
|
||||
scopes=[scope.to_dict() for scope in explanation.scopes],
|
||||
function_facts=[fact.to_dict() for fact in explanation.function_facts],
|
||||
@@ -2054,7 +2084,15 @@ def get_resource_access_explanation(
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
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(
|
||||
target_principal,
|
||||
resource_type=resource_type,
|
||||
@@ -2062,7 +2100,7 @@ def get_resource_access_explanation(
|
||||
action=action,
|
||||
)
|
||||
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_id=resource_id,
|
||||
action=action,
|
||||
@@ -2152,8 +2190,9 @@ def create_user(
|
||||
)
|
||||
session.commit()
|
||||
idm_directory = _optional_idm_directory()
|
||||
organization_directory = _optional_organization_directory()
|
||||
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,
|
||||
temporary_password=result.temporary_password,
|
||||
)
|
||||
@@ -2234,7 +2273,12 @@ def update_user(
|
||||
details=payload.model_dump(exclude_none=True),
|
||||
)
|
||||
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:
|
||||
@@ -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()
|
||||
if user is None:
|
||||
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"]
|
||||
invalid = [scope for scope in requested if scope.startswith("system:") or not scopes_grant(user_scopes, scope)]
|
||||
if invalid:
|
||||
|
||||
Reference in New Issue
Block a user