fix: sync role CRUD with provider client roles

This commit is contained in:
Chris
2026-04-03 01:17:13 +08:00
parent 6adca8c229
commit f351fe6454
3 changed files with 180 additions and 3 deletions

View File

@@ -35,6 +35,12 @@ class ProviderGroupSyncResult:
action: str
@dataclass
class ProviderRoleSyncResult:
role_name: str
action: str
class ProviderAdminService:
def __init__(self, settings: Settings) -> None:
self.base_url = settings.keycloak_base_url.rstrip("/")
@@ -384,3 +390,119 @@ class ProviderAdminService:
raise HTTPException(status_code=502, detail="idp_lookup_failed")
payload = resp.json() if resp.content else []
return payload if isinstance(payload, list) else []
def _resolve_client_uuid(self, client: httpx.Client, provider_client_id: str) -> str:
if not provider_client_id:
raise HTTPException(status_code=400, detail="provider_client_id_required")
# provider_client_id stores Keycloak clientId (not internal UUID).
resp = client.get(
f"/admin/realms/{self.realm}/clients",
params={"clientId": provider_client_id},
)
if resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_lookup_failed")
payload = resp.json() if resp.content else []
rows = payload if isinstance(payload, list) else []
for row in rows:
if not isinstance(row, dict):
continue
if str(row.get("clientId", "")).strip() != provider_client_id:
continue
client_uuid = str(row.get("id", "")).strip()
if client_uuid:
return client_uuid
raise HTTPException(status_code=404, detail="provider_client_not_found")
def ensure_client_role(
self,
*,
provider_client_id: str,
provider_role_name: str,
description: str | None = None,
) -> ProviderRoleSyncResult:
role_name = provider_role_name.strip()
if not role_name:
raise HTTPException(status_code=400, detail="provider_role_name_required")
with self._client() as client:
client_uuid = self._resolve_client_uuid(client, provider_client_id)
get_resp = client.get(f"/admin/realms/{self.realm}/clients/{client_uuid}/roles/{role_name}")
if get_resp.status_code == 404:
create_resp = client.post(
f"/admin/realms/{self.realm}/clients/{client_uuid}/roles",
json={"name": role_name, "description": description or ""},
)
if create_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_role_create_failed")
return ProviderRoleSyncResult(role_name=role_name, action="created")
if get_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_lookup_failed")
payload = get_resp.json() if get_resp.content else {}
update_payload = {
"name": role_name,
"description": description if description is not None else payload.get("description", ""),
"attributes": payload.get("attributes") if isinstance(payload, dict) else {},
}
put_resp = client.put(
f"/admin/realms/{self.realm}/clients/{client_uuid}/roles/{role_name}",
json=update_payload,
)
if put_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_role_update_failed")
return ProviderRoleSyncResult(role_name=role_name, action="updated")
def update_client_role(
self,
*,
provider_client_id: str,
old_provider_role_name: str,
new_provider_role_name: str,
description: str | None = None,
) -> ProviderRoleSyncResult:
old_name = old_provider_role_name.strip()
new_name = new_provider_role_name.strip()
if not old_name or not new_name:
raise HTTPException(status_code=400, detail="provider_role_name_required")
with self._client() as client:
client_uuid = self._resolve_client_uuid(client, provider_client_id)
get_resp = client.get(f"/admin/realms/{self.realm}/clients/{client_uuid}/roles/{old_name}")
if get_resp.status_code == 404:
# If old role missing, create the new one to self-heal drift.
create_resp = client.post(
f"/admin/realms/{self.realm}/clients/{client_uuid}/roles",
json={"name": new_name, "description": description or ""},
)
if create_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_role_create_failed")
return ProviderRoleSyncResult(role_name=new_name, action="created")
if get_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_lookup_failed")
payload = get_resp.json() if get_resp.content else {}
update_payload = {
"name": new_name,
"description": description if description is not None else payload.get("description", ""),
"attributes": payload.get("attributes") if isinstance(payload, dict) else {},
}
put_resp = client.put(
f"/admin/realms/{self.realm}/clients/{client_uuid}/roles/{old_name}",
json=update_payload,
)
if put_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_role_update_failed")
return ProviderRoleSyncResult(role_name=new_name, action="updated")
def delete_client_role(self, *, provider_client_id: str, provider_role_name: str) -> ProviderDeleteResult:
role_name = provider_role_name.strip()
if not role_name:
return ProviderDeleteResult(action="not_found")
with self._client() as client:
client_uuid = self._resolve_client_uuid(client, provider_client_id)
resp = client.delete(f"/admin/realms/{self.realm}/clients/{client_uuid}/roles/{role_name}")
if resp.status_code in {204, 404}:
return ProviderDeleteResult(action="deleted" if resp.status_code == 204 else "not_found")
if resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_role_delete_failed")
return ProviderDeleteResult(action="deleted")