feat(auth): sync Keycloak users for authorization
Generic Container CI/CD / test-build-publish (push) Failing after 33s
Server CI/CD v2 / build-test-publish-and-deploy (push) Failing after 34s

This commit is contained in:
2026-11-04 13:35:29 +08:00
parent bd8c619bfb
commit 9a9add1531
11 changed files with 716 additions and 8 deletions
+99
View File
@@ -12,11 +12,17 @@ from app.auth.metadata_dependencies import (
get_current_metadata_admin,
get_metadata_repository,
)
from app.auth.keycloak_dependencies import (
get_current_keycloak_payload,
oauth2_optional,
)
from app.core.audit import AuditAction, log_audit_event
from app.domain.schemas.admin_metadata import (
AdminProjectCreateRequest,
AdminProjectResponse,
AdminProjectUpdateRequest,
KeycloakUserSyncIssue,
KeycloakUserSyncResponse,
MetadataUsersBatchSyncRequest,
MetadataUserResponse,
MetadataUserSyncRequest,
@@ -36,6 +42,11 @@ from app.infra.db.metadb.repositories.metadata_repository import (
MetadataRepository,
ProjectDbRouting,
)
from app.services.keycloak_user_sync import (
KeycloakUserSyncConfigurationError,
KeycloakUserSyncRequestError,
fetch_keycloak_users,
)
router = APIRouter()
@@ -229,6 +240,94 @@ async def sync_metadata_users_batch(
return results
@router.post(
"/admin/keycloak-user-syncs",
response_model=KeycloakUserSyncResponse,
)
async def sync_keycloak_user_directory(
current_user=Depends(get_current_metadata_admin),
keycloak_payload: dict = Depends(get_current_keycloak_payload),
keycloak_access_token: str | None = Depends(oauth2_optional),
metadata_repo: MetadataRepository = Depends(get_metadata_repository),
) -> KeycloakUserSyncResponse:
try:
keycloak_users = await fetch_keycloak_users(
issuer=str(keycloak_payload.get("iss") or ""),
bearer_token=keycloak_access_token or "",
)
except KeycloakUserSyncConfigurationError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=str(exc),
) from exc
except KeycloakUserSyncRequestError as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=str(exc),
) from exc
created = 0
updated = 0
issues: list[KeycloakUserSyncIssue] = []
for keycloak_user in keycloak_users:
if keycloak_user.service_account_client_id:
issues.append(
KeycloakUserSyncIssue(
username=keycloak_user.username,
reason="service_account",
)
)
continue
if not keycloak_user.email:
issues.append(
KeycloakUserSyncIssue(
username=keycloak_user.username,
reason="missing_email",
)
)
continue
try:
_, was_created = await metadata_repo.sync_user_snapshot_from_keycloak(
keycloak_id=keycloak_user.keycloak_id,
username=keycloak_user.username,
email=keycloak_user.email,
)
except IntegrityError:
await metadata_repo.session.rollback()
issues.append(
KeycloakUserSyncIssue(
username=keycloak_user.username,
reason="identity_conflict",
)
)
except SQLAlchemyError as exc:
await metadata_repo.session.rollback()
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Metadata database error during Keycloak user synchronization",
) from exc
else:
created += int(was_created)
updated += int(not was_created)
result = KeycloakUserSyncResponse(
scanned=len(keycloak_users),
created=created,
updated=updated,
skipped=len(issues),
issues=issues,
)
await log_audit_event(
action=AuditAction.UPDATE,
user_id=current_user.id,
resource_type="keycloak_user_directory",
request_data=result.model_dump(mode="json", exclude={"issues"}),
response_status=status.HTTP_200_OK,
session=metadata_repo.session,
)
return result
@router.get("/admin/users", response_model=List[MetadataUserResponse])
async def list_metadata_users(
skip: int = Query(0, ge=0),