Implement role-based account journeys with database and browser acceptance suites
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
75750c0036
commit
1127f852dd
24 changed files with 1554 additions and 148 deletions
|
|
@ -1837,7 +1837,69 @@ class UserEngineService:
|
|||
)
|
||||
return updated
|
||||
|
||||
def set_tenant_account_status(
|
||||
def authorize_tenant_member_action(
|
||||
self, actor: Actor, user_id: str, *, tenant: str, correlation_id: str,
|
||||
) -> User:
|
||||
"""Authorize before any credential-provider side effect or target readout."""
|
||||
self.resolve_tenant_context(actor, tenant)
|
||||
if not {"tenant-admin", PLATFORM_OPERATOR_ROLE}.intersection(actor.roles):
|
||||
raise AuthorizationDenied("tenant administrator role required")
|
||||
memberships = self.store.memberships_for_user(user_id, tenant=tenant)
|
||||
if not any(m.scope_type == "tenant" and m.scope_id == tenant for m in memberships):
|
||||
raise NotFoundError("account is not a member of this tenant")
|
||||
self._authorize(actor, action="tenant.account.update",
|
||||
resource_type="user-engine:tenant-account",
|
||||
resource_id=f"{tenant}:{user_id}", tenant=tenant,
|
||||
correlation_id=correlation_id, target_user_id=user_id)
|
||||
return self._require_user(user_id)
|
||||
|
||||
def validate_tenant_role_change(self, actor: Actor, user_id: str, role: str, *, tenant: str, correlation_id: str) -> None:
|
||||
self.authorize_tenant_member_action(actor, user_id, tenant=tenant, correlation_id=correlation_id)
|
||||
if role not in {"user", "tenant-admin"}:
|
||||
raise ValidationError("Choose User or Tenant administrator.")
|
||||
self._authorize(actor, action="membership.write", resource_type="user-engine:membership",
|
||||
resource_id=f"{tenant}:{user_id}:tenant:{tenant}", tenant=tenant,
|
||||
correlation_id=correlation_id, target_user_id=user_id,
|
||||
context={"scope_type":"tenant", "scope_id":tenant, "kind":role})
|
||||
if role != "tenant-admin":
|
||||
self.require_admin_successor(user_id, tenant=tenant)
|
||||
|
||||
def set_tenant_role(self, actor: Actor, user_id: str, role: str, *, tenant: str, correlation_id: str) -> None:
|
||||
with self.store.tenant_lifecycle_guard(tenant):
|
||||
self._set_tenant_role(actor, user_id, role, tenant=tenant, correlation_id=correlation_id)
|
||||
|
||||
def _set_tenant_role(self, actor: Actor, user_id: str, role: str, *, tenant: str, correlation_id: str) -> None:
|
||||
self.validate_tenant_role_change(actor, user_id, role, tenant=tenant, correlation_id=correlation_id)
|
||||
with self.store.transaction():
|
||||
for membership in self.store.memberships_for_user(user_id, tenant=tenant):
|
||||
if membership.scope_type == "tenant" and membership.scope_id == tenant:
|
||||
self.store.save_membership(replace(membership, kind=role, freshness_version=correlation_id))
|
||||
self._record_mutation(actor, action="membership.write", subject=user_id, tenant=tenant,
|
||||
correlation_id=correlation_id, decision_id=None,
|
||||
event_type="membership.role_changed", aggregate_id=user_id,
|
||||
payload={"user_id":user_id, "tenant":tenant, "role":role})
|
||||
|
||||
def require_admin_successor(self, user_id: str, *, tenant: str) -> None:
|
||||
"""Do not deactivate the last active tenant administrator."""
|
||||
members = self.store.memberships_for_tenant(tenant)
|
||||
admins = {m.user_id for m in members if m.scope_type == "tenant"
|
||||
and m.scope_id == tenant and m.kind == "tenant-admin"}
|
||||
if user_id not in admins:
|
||||
return
|
||||
account = self.store.tenant_account(tenant, user_id)
|
||||
if account is not None and account.status != AccountStatus.ACTIVE:
|
||||
return
|
||||
for other in admins - {user_id}:
|
||||
state = self.store.tenant_account(tenant, other)
|
||||
if state is not None and state.status == AccountStatus.ACTIVE:
|
||||
return
|
||||
raise ConflictError("Assign another active tenant administrator before disabling this account.")
|
||||
|
||||
def set_tenant_account_status(self, actor: Actor, user_id: str, status: AccountStatus, *, tenant: str, correlation_id: str | None = None) -> TenantAccount:
|
||||
with self.store.tenant_lifecycle_guard(tenant):
|
||||
return self._set_tenant_account_status(actor, user_id, status, tenant=tenant, correlation_id=correlation_id)
|
||||
|
||||
def _set_tenant_account_status(
|
||||
self,
|
||||
actor: Actor,
|
||||
user_id: str,
|
||||
|
|
@ -1863,6 +1925,8 @@ class UserEngineService:
|
|||
)
|
||||
updated = replace(account, status=status)
|
||||
with self.store.transaction():
|
||||
if status in {AccountStatus.SUSPENDED, AccountStatus.DISABLED}:
|
||||
self.require_admin_successor(user_id, tenant=tenant_context.tenant)
|
||||
self.store.save_tenant_account(updated)
|
||||
self._record_mutation(
|
||||
actor,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue