fix(keycloak): preserve profile fields
This commit is contained in:
parent
deb3813c2e
commit
fbed11aeed
@ -55,6 +55,36 @@ class KeycloakAdminClient:
|
||||
self._expires_at: float = 0.0
|
||||
self._group_id_cache: dict[str, str] = {}
|
||||
|
||||
@staticmethod
|
||||
def _safe_update_payload(full: dict[str, Any]) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {}
|
||||
username = full.get("username")
|
||||
if isinstance(username, str):
|
||||
payload["username"] = username
|
||||
enabled = full.get("enabled")
|
||||
if isinstance(enabled, bool):
|
||||
payload["enabled"] = enabled
|
||||
email = full.get("email")
|
||||
if isinstance(email, str):
|
||||
payload["email"] = email
|
||||
email_verified = full.get("emailVerified")
|
||||
if isinstance(email_verified, bool):
|
||||
payload["emailVerified"] = email_verified
|
||||
first_name = full.get("firstName")
|
||||
if isinstance(first_name, str):
|
||||
payload["firstName"] = first_name
|
||||
last_name = full.get("lastName")
|
||||
if isinstance(last_name, str):
|
||||
payload["lastName"] = last_name
|
||||
|
||||
actions = full.get("requiredActions")
|
||||
if isinstance(actions, list):
|
||||
payload["requiredActions"] = [a for a in actions if isinstance(a, str)]
|
||||
|
||||
attrs = full.get("attributes")
|
||||
payload["attributes"] = attrs if isinstance(attrs, dict) else {}
|
||||
return payload
|
||||
|
||||
def ready(self) -> bool:
|
||||
return bool(settings.KEYCLOAK_ADMIN_CLIENT_ID and settings.KEYCLOAK_ADMIN_CLIENT_SECRET)
|
||||
|
||||
@ -146,6 +176,21 @@ class KeycloakAdminClient:
|
||||
resp = client.put(url, headers={**self._headers(), "Content-Type": "application/json"}, json=payload)
|
||||
resp.raise_for_status()
|
||||
|
||||
def update_user_safe(self, user_id: str, payload: dict[str, Any]) -> None:
|
||||
full = self.get_user(user_id)
|
||||
merged = self._safe_update_payload(full)
|
||||
for key, value in payload.items():
|
||||
if key == "attributes":
|
||||
attrs = merged.get("attributes")
|
||||
if not isinstance(attrs, dict):
|
||||
attrs = {}
|
||||
if isinstance(value, dict):
|
||||
attrs.update(value)
|
||||
merged["attributes"] = attrs
|
||||
continue
|
||||
merged[key] = value
|
||||
self.update_user(user_id, merged)
|
||||
|
||||
def create_user(self, payload: dict[str, Any]) -> str:
|
||||
url = f"{settings.KEYCLOAK_ADMIN_URL}/admin/realms/{settings.KEYCLOAK_REALM}/users"
|
||||
with httpx.Client(timeout=settings.HTTP_CHECK_TIMEOUT_SEC) as client:
|
||||
@ -175,13 +220,14 @@ class KeycloakAdminClient:
|
||||
raise RuntimeError("user id missing")
|
||||
|
||||
full = self.get_user(user_id)
|
||||
attrs = full.get("attributes") or {}
|
||||
payload = self._safe_update_payload(full)
|
||||
attrs = payload.get("attributes")
|
||||
if not isinstance(attrs, dict):
|
||||
attrs = {}
|
||||
attrs[key] = [value]
|
||||
# Keycloak rejects PUTs that include read-only fields from the GET payload (400 Bad Request).
|
||||
# Update only the attributes we intend to change.
|
||||
self.update_user(user_id, {"attributes": attrs})
|
||||
payload["attributes"] = attrs
|
||||
# Keep profile fields intact so required actions don't re-trigger unexpectedly.
|
||||
self.update_user(user_id, payload)
|
||||
|
||||
def get_group_id(self, group_name: str) -> str | None:
|
||||
cached = self._group_id_cache.get(group_name)
|
||||
|
||||
@ -245,7 +245,7 @@ def provision_access_request(request_code: str) -> ProvisionResult:
|
||||
if isinstance(actions, list) and "CONFIGURE_TOTP" in actions:
|
||||
# Backfill earlier accounts created when we forced MFA enrollment.
|
||||
new_actions = [a for a in actions if a != "CONFIGURE_TOTP"]
|
||||
admin_client().update_user(user_id, {"requiredActions": new_actions})
|
||||
admin_client().update_user_safe(user_id, {"requiredActions": new_actions})
|
||||
mailu_from_attr: str | None = None
|
||||
if isinstance(attrs, dict):
|
||||
raw_mailu = attrs.get(MAILU_EMAIL_ATTR)
|
||||
|
||||
@ -177,7 +177,7 @@ def _auto_completed_keycloak_steps(conn, request_code: str, username: str) -> se
|
||||
# remove it if present.
|
||||
if "CONFIGURE_TOTP" in required_actions:
|
||||
try:
|
||||
admin_client().update_user(
|
||||
admin_client().update_user_safe(
|
||||
user_id,
|
||||
{"requiredActions": [a for a in actions_list if a != "CONFIGURE_TOTP"]},
|
||||
)
|
||||
@ -824,7 +824,7 @@ def register(app) -> None:
|
||||
actions_list = [a for a in actions if isinstance(a, str)]
|
||||
if "UPDATE_PASSWORD" not in actions_list:
|
||||
actions_list.append("UPDATE_PASSWORD")
|
||||
admin_client().update_user(user_id, {"requiredActions": actions_list})
|
||||
admin_client().update_user_safe(user_id, {"requiredActions": actions_list})
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user