feat(auth): sync Keycloak users for authorization
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user