Support tenant-scoped directory lifecycle with isolation tests
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
0071dba99e
commit
a0cc947bf6
4 changed files with 146 additions and 0 deletions
|
|
@ -70,6 +70,42 @@ mutation CreateUser($id: String!, $email: String!, $display: String!) {
|
|||
raise
|
||||
return Result("netkingdom-lldap", _oidc_subject(username), "password_setup_required", resumed)
|
||||
|
||||
def tenant_access(self, payload: dict[str, Any]) -> Result:
|
||||
"""Change only the two role groups owned by the specified tenant."""
|
||||
_required(payload, "external_subject", "tenant", "idempotency_key", "correlation_id")
|
||||
tenant = str(payload["tenant"])
|
||||
if not re.fullmatch(r"tenant:[a-z0-9][a-z0-9._-]*(?::[a-z0-9][a-z0-9._-]*)*", tenant):
|
||||
raise ValueError("invalid tenant identifier")
|
||||
if tenant == "tenant:platform:root":
|
||||
raise ValueError("platform root is not a tenant access target")
|
||||
if not isinstance(payload.get("enabled"), bool):
|
||||
raise ValueError("enabled must be boolean")
|
||||
roles = payload.get("roles", [])
|
||||
if not isinstance(roles, (list, tuple)) or any(r not in {"user", "tenant-admin"} for r in roles):
|
||||
raise ValueError("unsupported tenant role")
|
||||
subject = _directory_username(str(payload["external_subject"]))
|
||||
if not re.fullmatch(r"[A-Za-z0-9._-]+", subject):
|
||||
raise ValueError("invalid directory subject")
|
||||
token = self._login()
|
||||
user = self._user(token, subject)
|
||||
if user is None:
|
||||
raise ValueError("login identity not found; create it before changing access")
|
||||
managed = {f"{tenant}:users", f"{tenant}:admins"}
|
||||
desired = {f"{tenant}:users"} if payload["enabled"] else set()
|
||||
if payload["enabled"] and "tenant-admin" in roles:
|
||||
desired.add(f"{tenant}:admins")
|
||||
current = {str(g["displayName"]): int(g["id"]) for g in user.get("groups", ())}
|
||||
groups = list(self._directory(token)[1])
|
||||
for name in sorted(desired - current.keys()):
|
||||
self._add_group(token, subject, self._ensure_group(token, groups, name))
|
||||
for name in sorted((managed & current.keys()) - desired):
|
||||
self._remove_group(token, subject, current[name])
|
||||
checked = self._user(token, subject)
|
||||
if checked is None or ({g["displayName"] for g in checked.get("groups", ())} & managed) != desired:
|
||||
raise RuntimeError("tenant access readback did not confirm the requested state")
|
||||
return Result("netkingdom-lldap", _oidc_subject(subject),
|
||||
"tenant_active" if payload["enabled"] else "tenant_disabled", False)
|
||||
|
||||
def suspend(self, subject: str) -> Result:
|
||||
subject = _directory_username(subject)
|
||||
token = self._login()
|
||||
|
|
@ -245,6 +281,8 @@ mutation Remove($userId: String!, $groupId: Int!) {
|
|||
def dispatch(
|
||||
provisioner: LLDAPProvisioner, path: str, payload: dict[str, Any]
|
||||
) -> Result | DriftResult:
|
||||
if path == "/v1/identities/tenant-access":
|
||||
return provisioner.tenant_access(payload)
|
||||
if path == "/v1/identities/provision":
|
||||
return provisioner.provision(payload)
|
||||
if path == "/v1/identities/drift":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue