Sync site-role assignments to Keycloak group role mappings
This commit is contained in:
@@ -129,6 +129,31 @@ def _site_group_name(display_name: str, site_key: str) -> str:
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_site_client_roles(
|
||||||
|
*,
|
||||||
|
idp: ProviderAdminService,
|
||||||
|
site,
|
||||||
|
site_role_rows,
|
||||||
|
provider_client_ids: set[str],
|
||||||
|
) -> None:
|
||||||
|
if not site.provider_group_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"site_provider_group_missing:{site.site_key}")
|
||||||
|
|
||||||
|
role_names_by_client: dict[str, list[str]] = {}
|
||||||
|
for _, role, system in site_role_rows:
|
||||||
|
provider_client_id = str(system.name or "").strip()
|
||||||
|
if not provider_client_id:
|
||||||
|
continue
|
||||||
|
role_names_by_client.setdefault(provider_client_id, []).append(role.name)
|
||||||
|
|
||||||
|
for provider_client_id in sorted(provider_client_ids):
|
||||||
|
idp.set_group_client_roles(
|
||||||
|
group_id=site.provider_group_id,
|
||||||
|
provider_client_id=provider_client_id,
|
||||||
|
role_names=role_names_by_client.get(provider_client_id, []),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/companies", response_model=ListResponse)
|
@router.get("/companies", response_model=ListResponse)
|
||||||
def list_companies(
|
def list_companies(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -597,11 +622,15 @@ def assign_site_roles(site_key: str, payload: SiteRoleAssignRequest, db: Session
|
|||||||
sites_repo = SitesRepository(db)
|
sites_repo = SitesRepository(db)
|
||||||
roles_repo = RolesRepository(db)
|
roles_repo = RolesRepository(db)
|
||||||
site_roles_repo = SiteRolesRepository(db)
|
site_roles_repo = SiteRolesRepository(db)
|
||||||
|
idp = ProviderAdminService(get_settings())
|
||||||
|
|
||||||
site = sites_repo.get_by_key(site_key)
|
site = sites_repo.get_by_key(site_key)
|
||||||
if not site:
|
if not site:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="site_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="site_not_found")
|
||||||
|
|
||||||
|
current_rows = site_roles_repo.list_site_role_rows(site.id)
|
||||||
|
current_client_ids = {str(system.name or "").strip() for _, _, system in current_rows if str(system.name or "").strip()}
|
||||||
|
|
||||||
role_ids: list[str] = []
|
role_ids: list[str] = []
|
||||||
for role_key in list(dict.fromkeys(payload.role_keys)):
|
for role_key in list(dict.fromkeys(payload.role_keys)):
|
||||||
role = roles_repo.get_by_key(role_key)
|
role = roles_repo.get_by_key(role_key)
|
||||||
@@ -609,7 +638,23 @@ def assign_site_roles(site_key: str, payload: SiteRoleAssignRequest, db: Session
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"role_not_found:{role_key}")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"role_not_found:{role_key}")
|
||||||
role_ids.append(role.id)
|
role_ids.append(role.id)
|
||||||
|
|
||||||
site_roles_repo.set_site_roles(site_id=site.id, role_ids=role_ids)
|
site_roles_repo.set_site_roles(site_id=site.id, role_ids=role_ids, commit=False)
|
||||||
|
updated_rows = site_roles_repo.list_site_role_rows(site.id)
|
||||||
|
updated_client_ids = {str(system.name or "").strip() for _, _, system in updated_rows if str(system.name or "").strip()}
|
||||||
|
clients_to_sync = current_client_ids | updated_client_ids
|
||||||
|
|
||||||
|
try:
|
||||||
|
_sync_site_client_roles(
|
||||||
|
idp=idp,
|
||||||
|
site=site,
|
||||||
|
site_role_rows=updated_rows,
|
||||||
|
provider_client_ids=clients_to_sync,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
return list_site_roles(site_key=site_key, db=db)
|
return list_site_roles(site_key=site_key, db=db)
|
||||||
|
|
||||||
|
|
||||||
@@ -650,12 +695,24 @@ def list_role_sites(role_key: str, db: Session = Depends(get_db)) -> RoleSitesRe
|
|||||||
def assign_role_sites(role_key: str, payload: UserSiteAssignRequest, db: Session = Depends(get_db)) -> RoleSitesResponse:
|
def assign_role_sites(role_key: str, payload: UserSiteAssignRequest, db: Session = Depends(get_db)) -> RoleSitesResponse:
|
||||||
roles_repo = RolesRepository(db)
|
roles_repo = RolesRepository(db)
|
||||||
sites_repo = SitesRepository(db)
|
sites_repo = SitesRepository(db)
|
||||||
|
systems_repo = SystemsRepository(db)
|
||||||
site_roles_repo = SiteRolesRepository(db)
|
site_roles_repo = SiteRolesRepository(db)
|
||||||
|
idp = ProviderAdminService(get_settings())
|
||||||
|
|
||||||
role = roles_repo.get_by_key(role_key)
|
role = roles_repo.get_by_key(role_key)
|
||||||
if not role:
|
if not role:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="role_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="role_not_found")
|
||||||
|
|
||||||
|
system = systems_repo.get_by_id(role.system_id)
|
||||||
|
if not system:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||||
|
provider_client_id = str(system.name or "").strip()
|
||||||
|
if not provider_client_id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=f"provider_client_id_missing:{system.system_key}")
|
||||||
|
|
||||||
|
previous_rows = site_roles_repo.list_role_site_rows(role.id)
|
||||||
|
previous_site_ids = {site.id for _, site in previous_rows}
|
||||||
|
|
||||||
site_ids: list[str] = []
|
site_ids: list[str] = []
|
||||||
for site_key in list(dict.fromkeys(payload.site_keys)):
|
for site_key in list(dict.fromkeys(payload.site_keys)):
|
||||||
site = sites_repo.get_by_key(site_key)
|
site = sites_repo.get_by_key(site_key)
|
||||||
@@ -663,7 +720,26 @@ def assign_role_sites(role_key: str, payload: UserSiteAssignRequest, db: Session
|
|||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"site_not_found:{site_key}")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"site_not_found:{site_key}")
|
||||||
site_ids.append(site.id)
|
site_ids.append(site.id)
|
||||||
|
|
||||||
site_roles_repo.set_role_sites(role_id=role.id, site_ids=site_ids)
|
site_roles_repo.set_role_sites(role_id=role.id, site_ids=site_ids, commit=False)
|
||||||
|
|
||||||
|
affected_site_ids = previous_site_ids | set(site_ids)
|
||||||
|
try:
|
||||||
|
for site_id in affected_site_ids:
|
||||||
|
site = sites_repo.get_by_id(site_id)
|
||||||
|
if not site:
|
||||||
|
continue
|
||||||
|
site_rows = site_roles_repo.list_site_role_rows(site.id)
|
||||||
|
_sync_site_client_roles(
|
||||||
|
idp=idp,
|
||||||
|
site=site,
|
||||||
|
site_role_rows=site_rows,
|
||||||
|
provider_client_ids={provider_client_id},
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
return list_role_sites(role_key=role_key, db=db)
|
return list_role_sites(role_key=role_key, db=db)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,16 @@ class SiteRolesRepository:
|
|||||||
)
|
)
|
||||||
return list(self.db.execute(stmt).all())
|
return list(self.db.execute(stmt).all())
|
||||||
|
|
||||||
def set_site_roles(self, *, site_id: str, role_ids: list[str]) -> None:
|
def set_site_roles(self, *, site_id: str, role_ids: list[str], commit: bool = True) -> None:
|
||||||
self.db.execute(delete(SiteRole).where(SiteRole.site_id == site_id))
|
self.db.execute(delete(SiteRole).where(SiteRole.site_id == site_id))
|
||||||
for role_id in role_ids:
|
for role_id in role_ids:
|
||||||
self.db.add(SiteRole(site_id=site_id, role_id=role_id))
|
self.db.add(SiteRole(site_id=site_id, role_id=role_id))
|
||||||
self.db.commit()
|
if commit:
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def set_role_sites(self, *, role_id: str, site_ids: list[str]) -> None:
|
def set_role_sites(self, *, role_id: str, site_ids: list[str], commit: bool = True) -> None:
|
||||||
self.db.execute(delete(SiteRole).where(SiteRole.role_id == role_id))
|
self.db.execute(delete(SiteRole).where(SiteRole.role_id == role_id))
|
||||||
for site_id in site_ids:
|
for site_id in site_ids:
|
||||||
self.db.add(SiteRole(site_id=site_id, role_id=role_id))
|
self.db.add(SiteRole(site_id=site_id, role_id=role_id))
|
||||||
self.db.commit()
|
if commit:
|
||||||
|
self.db.commit()
|
||||||
|
|||||||
@@ -413,6 +413,65 @@ class ProviderAdminService:
|
|||||||
return client_uuid
|
return client_uuid
|
||||||
raise HTTPException(status_code=404, detail="provider_client_not_found")
|
raise HTTPException(status_code=404, detail="provider_client_not_found")
|
||||||
|
|
||||||
|
def _get_client_role_representation(self, client: httpx.Client, *, client_uuid: str, role_name: str) -> dict:
|
||||||
|
resp = client.get(f"/admin/realms/{self.realm}/clients/{client_uuid}/roles/{role_name}")
|
||||||
|
if resp.status_code == 404:
|
||||||
|
raise HTTPException(status_code=404, detail=f"provider_role_not_found:{role_name}")
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
raise HTTPException(status_code=502, detail="idp_lookup_failed")
|
||||||
|
payload = resp.json() if resp.content else {}
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
raise HTTPException(status_code=502, detail="idp_lookup_failed")
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def set_group_client_roles(self, *, group_id: str, provider_client_id: str, role_names: list[str]) -> None:
|
||||||
|
if not group_id:
|
||||||
|
raise HTTPException(status_code=400, detail="provider_group_id_required")
|
||||||
|
|
||||||
|
desired_names = [name.strip() for name in role_names if isinstance(name, str) and name.strip()]
|
||||||
|
desired_name_set = set(desired_names)
|
||||||
|
|
||||||
|
with self._client() as client:
|
||||||
|
client_uuid = self._resolve_client_uuid(client, provider_client_id)
|
||||||
|
|
||||||
|
current_resp = client.get(f"/admin/realms/{self.realm}/groups/{group_id}/role-mappings/clients/{client_uuid}")
|
||||||
|
if current_resp.status_code >= 400:
|
||||||
|
raise HTTPException(status_code=502, detail="idp_group_role_mapping_lookup_failed")
|
||||||
|
current_payload = current_resp.json() if current_resp.content else []
|
||||||
|
current_rows = current_payload if isinstance(current_payload, list) else []
|
||||||
|
current_map: dict[str, dict] = {}
|
||||||
|
for row in current_rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
name = str(row.get("name", "")).strip()
|
||||||
|
if name:
|
||||||
|
current_map[name] = row
|
||||||
|
|
||||||
|
to_add_names = sorted(desired_name_set - set(current_map.keys()))
|
||||||
|
to_remove_names = sorted(set(current_map.keys()) - desired_name_set)
|
||||||
|
|
||||||
|
if to_add_names:
|
||||||
|
add_payload = [
|
||||||
|
self._get_client_role_representation(client, client_uuid=client_uuid, role_name=role_name)
|
||||||
|
for role_name in to_add_names
|
||||||
|
]
|
||||||
|
add_resp = client.post(
|
||||||
|
f"/admin/realms/{self.realm}/groups/{group_id}/role-mappings/clients/{client_uuid}",
|
||||||
|
json=add_payload,
|
||||||
|
)
|
||||||
|
if add_resp.status_code >= 400:
|
||||||
|
raise HTTPException(status_code=502, detail="idp_group_role_mapping_add_failed")
|
||||||
|
|
||||||
|
if to_remove_names:
|
||||||
|
remove_payload = [current_map[name] for name in to_remove_names]
|
||||||
|
remove_resp = client.request(
|
||||||
|
"DELETE",
|
||||||
|
f"/admin/realms/{self.realm}/groups/{group_id}/role-mappings/clients/{client_uuid}",
|
||||||
|
json=remove_payload,
|
||||||
|
)
|
||||||
|
if remove_resp.status_code >= 400:
|
||||||
|
raise HTTPException(status_code=502, detail="idp_group_role_mapping_remove_failed")
|
||||||
|
|
||||||
def ensure_client_role(
|
def ensure_client_role(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
Reference in New Issue
Block a user