refactor: rename idp fields to provider naming

This commit is contained in:
Chris
2026-04-03 01:05:01 +08:00
parent ce181ebf67
commit 388a3f461c
26 changed files with 202 additions and 199 deletions

View File

@@ -51,8 +51,8 @@ from app.schemas.catalog import (
) )
from app.security.admin_guard import require_admin_principal from app.security.admin_guard import require_admin_principal
from app.security.api_client_auth import hash_api_key from app.security.api_client_auth import hash_api_key
from app.services.idp_admin_service import KeycloakAdminService from app.services.idp_admin_service import ProviderAdminService
from app.services.idp_catalog_sync import sync_from_keycloak from app.services.idp_catalog_sync import sync_from_provider
from app.core.config import get_settings from app.core.config import get_settings
router = APIRouter( router = APIRouter(
@@ -76,7 +76,7 @@ def _company_item(company) -> CompanyItem:
company_key=company.company_key, company_key=company.company_key,
display_name=company.display_name, display_name=company.display_name,
legal_name=company.legal_name, legal_name=company.legal_name,
idp_group_id=company.idp_group_id, provider_group_id=company.provider_group_id,
status=company.status, status=company.status,
) )
@@ -89,7 +89,7 @@ def _site_item(site, company) -> SiteItem:
company_display_name=company.display_name, company_display_name=company.display_name,
display_name=site.display_name, display_name=site.display_name,
domain=site.domain, domain=site.domain,
idp_group_id=site.idp_group_id, provider_group_id=site.provider_group_id,
status=site.status, status=site.status,
) )
@@ -99,7 +99,7 @@ def _system_item(system) -> SystemItem:
id=system.id, id=system.id,
system_key=system.system_key, system_key=system.system_key,
name=system.name, name=system.name,
idp_client_id=system.idp_client_id, provider_client_id=system.provider_client_id,
status=system.status, status=system.status,
) )
@@ -108,7 +108,7 @@ def _member_item(user) -> MemberItem:
return MemberItem( return MemberItem(
id=user.id, id=user.id,
user_sub=user.user_sub, user_sub=user.user_sub,
idp_user_id=user.idp_user_id, provider_user_id=user.provider_user_id,
username=user.username, username=user.username,
email=user.email, email=user.email,
display_name=user.display_name, display_name=user.display_name,
@@ -138,7 +138,7 @@ def list_companies(
limit: int = Query(default=100, ge=1, le=500), limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
) -> ListResponse: ) -> ListResponse:
sync_from_keycloak(db) sync_from_provider(db)
repo = CompaniesRepository(db) repo = CompaniesRepository(db)
items, total = repo.list(keyword=keyword, limit=limit, offset=offset) items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
return ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset) return ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset)
@@ -147,7 +147,7 @@ def list_companies(
@router.post("/companies", response_model=CompanyItem) @router.post("/companies", response_model=CompanyItem)
def create_company(payload: CompanyCreateRequest, db: Session = Depends(get_db)) -> CompanyItem: def create_company(payload: CompanyCreateRequest, db: Session = Depends(get_db)) -> CompanyItem:
repo = CompaniesRepository(db) repo = CompaniesRepository(db)
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
company_key = _generate_unique_key("CP", lambda key: repo.get_by_key(key) is not None) company_key = _generate_unique_key("CP", lambda key: repo.get_by_key(key) is not None)
group_name = _company_group_name(payload.display_name, company_key) group_name = _company_group_name(payload.display_name, company_key)
group = idp.ensure_group( group = idp.ensure_group(
@@ -163,7 +163,7 @@ def create_company(payload: CompanyCreateRequest, db: Session = Depends(get_db))
company_key=company_key, company_key=company_key,
display_name=payload.display_name, display_name=payload.display_name,
legal_name=payload.legal_name, legal_name=payload.legal_name,
idp_group_id=group.group_id, provider_group_id=group.group_id,
status=payload.status, status=payload.status,
) )
return _company_item(item) return _company_item(item)
@@ -175,10 +175,10 @@ def update_company(company_key: str, payload: CompanyUpdateRequest, db: Session
item = repo.get_by_key(company_key) item = repo.get_by_key(company_key)
if not item: if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
resolved_display_name = payload.display_name if payload.display_name is not None else item.display_name resolved_display_name = payload.display_name if payload.display_name is not None else item.display_name
resolved_status = payload.status if payload.status is not None else item.status resolved_status = payload.status if payload.status is not None else item.status
resolved_group_id = payload.idp_group_id or item.idp_group_id resolved_group_id = payload.provider_group_id or item.provider_group_id
group_name = _company_group_name(resolved_display_name, company_key) group_name = _company_group_name(resolved_display_name, company_key)
group = idp.ensure_group( group = idp.ensure_group(
group_id=resolved_group_id, group_id=resolved_group_id,
@@ -194,7 +194,7 @@ def update_company(company_key: str, payload: CompanyUpdateRequest, db: Session
item, item,
display_name=payload.display_name, display_name=payload.display_name,
legal_name=payload.legal_name, legal_name=payload.legal_name,
idp_group_id=group.group_id, provider_group_id=group.group_id,
status=payload.status, status=payload.status,
) )
return _company_item(item) return _company_item(item)
@@ -203,11 +203,11 @@ def update_company(company_key: str, payload: CompanyUpdateRequest, db: Session
@router.delete("/companies/{company_key}") @router.delete("/companies/{company_key}")
def delete_company(company_key: str, db: Session = Depends(get_db)) -> dict[str, str]: def delete_company(company_key: str, db: Session = Depends(get_db)) -> dict[str, str]:
repo = CompaniesRepository(db) repo = CompaniesRepository(db)
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
item = repo.get_by_key(company_key) item = repo.get_by_key(company_key)
if not item: if not item:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
idp.delete_group(group_id=item.idp_group_id) idp.delete_group(group_id=item.provider_group_id)
repo.delete(item) repo.delete(item)
return {"deleted": company_key} return {"deleted": company_key}
@@ -231,7 +231,7 @@ def list_sites(
limit: int = Query(default=100, ge=1, le=500), limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
) -> ListResponse: ) -> ListResponse:
sync_from_keycloak(db) sync_from_provider(db)
companies_repo = CompaniesRepository(db) companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db) sites_repo = SitesRepository(db)
company_id = None company_id = None
@@ -252,7 +252,7 @@ def list_sites(
def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> SiteItem: def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> SiteItem:
companies_repo = CompaniesRepository(db) companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db) sites_repo = SitesRepository(db)
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
company = companies_repo.get_by_key(payload.company_key) company = companies_repo.get_by_key(payload.company_key)
if not company: if not company:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
@@ -262,7 +262,7 @@ def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> Si
group = idp.ensure_group( group = idp.ensure_group(
group_id=None, group_id=None,
name=group_name, name=group_name,
parent_group_id=company.idp_group_id, parent_group_id=company.provider_group_id,
attributes={ attributes={
"member_entity_type": "site", "member_entity_type": "site",
"site_key": site_key, "site_key": site_key,
@@ -277,7 +277,7 @@ def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> Si
company_id=company.id, company_id=company.id,
display_name=payload.display_name, display_name=payload.display_name,
domain=payload.domain, domain=payload.domain,
idp_group_id=group.group_id, provider_group_id=group.group_id,
status=payload.status, status=payload.status,
) )
return _site_item(item, company) return _site_item(item, company)
@@ -287,7 +287,7 @@ def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> Si
def update_site(site_key: str, payload: SiteUpdateRequest, db: Session = Depends(get_db)) -> SiteItem: def update_site(site_key: str, payload: SiteUpdateRequest, db: Session = Depends(get_db)) -> SiteItem:
companies_repo = CompaniesRepository(db) companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db) sites_repo = SitesRepository(db)
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
item = sites_repo.get_by_key(site_key) item = sites_repo.get_by_key(site_key)
if not item: if not item:
@@ -306,12 +306,12 @@ def update_site(site_key: str, payload: SiteUpdateRequest, db: Session = Depends
resolved_display_name = payload.display_name if payload.display_name is not None else item.display_name resolved_display_name = payload.display_name if payload.display_name is not None else item.display_name
resolved_domain = payload.domain if payload.domain is not None else item.domain resolved_domain = payload.domain if payload.domain is not None else item.domain
resolved_status = payload.status if payload.status is not None else item.status resolved_status = payload.status if payload.status is not None else item.status
resolved_group_id = payload.idp_group_id or item.idp_group_id resolved_group_id = payload.provider_group_id or item.provider_group_id
group_name = _site_group_name(resolved_display_name, site_key) group_name = _site_group_name(resolved_display_name, site_key)
group = idp.ensure_group( group = idp.ensure_group(
group_id=resolved_group_id, group_id=resolved_group_id,
name=group_name, name=group_name,
parent_group_id=target_company.idp_group_id, parent_group_id=target_company.provider_group_id,
attributes={ attributes={
"member_entity_type": "site", "member_entity_type": "site",
"site_key": site_key, "site_key": site_key,
@@ -327,7 +327,7 @@ def update_site(site_key: str, payload: SiteUpdateRequest, db: Session = Depends
company_id=company_id, company_id=company_id,
display_name=payload.display_name, display_name=payload.display_name,
domain=payload.domain, domain=payload.domain,
idp_group_id=group.group_id, provider_group_id=group.group_id,
status=payload.status, status=payload.status,
) )
company = companies_repo.get_by_id(item.company_id) company = companies_repo.get_by_id(item.company_id)
@@ -339,11 +339,11 @@ def update_site(site_key: str, payload: SiteUpdateRequest, db: Session = Depends
@router.delete("/sites/{site_key}") @router.delete("/sites/{site_key}")
def delete_site(site_key: str, db: Session = Depends(get_db)) -> dict[str, str]: def delete_site(site_key: str, db: Session = Depends(get_db)) -> dict[str, str]:
repo = SitesRepository(db) repo = SitesRepository(db)
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
item = repo.get_by_key(site_key) item = repo.get_by_key(site_key)
if not item: if not item:
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")
idp.delete_group(group_id=item.idp_group_id) idp.delete_group(group_id=item.provider_group_id)
repo.delete(item) repo.delete(item)
return {"deleted": site_key} return {"deleted": site_key}
@@ -356,7 +356,7 @@ def list_systems(
limit: int = Query(default=100, ge=1, le=500), limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
) -> ListResponse: ) -> ListResponse:
sync_from_keycloak(db) sync_from_provider(db)
repo = SystemsRepository(db) repo = SystemsRepository(db)
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset) items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
return ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset) return ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset)
@@ -364,17 +364,17 @@ def list_systems(
@router.post("/systems", response_model=SystemItem) @router.post("/systems", response_model=SystemItem)
def create_system(payload: SystemCreateRequest, db: Session = Depends(get_db)) -> SystemItem: def create_system(payload: SystemCreateRequest, db: Session = Depends(get_db)) -> SystemItem:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_manage_in_keycloak_only") raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_manage_in_provider_only")
@router.patch("/systems/{system_key}", response_model=SystemItem) @router.patch("/systems/{system_key}", response_model=SystemItem)
def update_system(system_key: str, payload: SystemUpdateRequest, db: Session = Depends(get_db)) -> SystemItem: def update_system(system_key: str, payload: SystemUpdateRequest, db: Session = Depends(get_db)) -> SystemItem:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_manage_in_keycloak_only") raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_manage_in_provider_only")
@router.delete("/systems/{system_key}") @router.delete("/systems/{system_key}")
def delete_system(system_key: str, db: Session = Depends(get_db)) -> dict[str, str]: def delete_system(system_key: str, db: Session = Depends(get_db)) -> dict[str, str]:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_manage_in_keycloak_only") raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_manage_in_provider_only")
@router.get("/roles", response_model=ListResponse) @router.get("/roles", response_model=ListResponse)
@@ -386,7 +386,7 @@ def list_roles(
limit: int = Query(default=100, ge=1, le=500), limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
) -> ListResponse: ) -> ListResponse:
sync_from_keycloak(db) sync_from_provider(db)
systems_repo = SystemsRepository(db) systems_repo = SystemsRepository(db)
roles_repo = RolesRepository(db) roles_repo = RolesRepository(db)
@@ -410,7 +410,7 @@ def list_roles(
system_key=system_map[row.system_id].system_key, system_key=system_map[row.system_id].system_key,
system_name=system_map[row.system_id].name, system_name=system_map[row.system_id].name,
name=row.name, name=row.name,
idp_role_name=row.idp_role_name, provider_role_name=row.provider_role_name,
description=row.description, description=row.description,
status=row.status, status=row.status,
) )
@@ -436,7 +436,7 @@ def create_role(payload: RoleCreateRequest, db: Session = Depends(get_db)) -> Ro
system_id=system.id, system_id=system.id,
name=payload.name, name=payload.name,
description=payload.description, description=payload.description,
idp_role_name=payload.idp_role_name, provider_role_name=payload.provider_role_name,
status=payload.status, status=payload.status,
) )
except IntegrityError: except IntegrityError:
@@ -449,7 +449,7 @@ def create_role(payload: RoleCreateRequest, db: Session = Depends(get_db)) -> Ro
system_key=system.system_key, system_key=system.system_key,
system_name=system.name, system_name=system.name,
name=row.name, name=row.name,
idp_role_name=row.idp_role_name, provider_role_name=row.provider_role_name,
description=row.description, description=row.description,
status=row.status, status=row.status,
) )
@@ -477,7 +477,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends
system_id=system_id, system_id=system_id,
name=payload.name, name=payload.name,
description=payload.description, description=payload.description,
idp_role_name=payload.idp_role_name, provider_role_name=payload.provider_role_name,
status=payload.status, status=payload.status,
) )
except IntegrityError: except IntegrityError:
@@ -494,7 +494,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends
system_key=system.system_key, system_key=system.system_key,
system_name=system.name, system_name=system.name,
name=role.name, name=role.name,
idp_role_name=role.idp_role_name, provider_role_name=role.provider_role_name,
description=role.description, description=role.description,
status=role.status, status=role.status,
) )
@@ -529,7 +529,7 @@ def list_system_roles(system_key: str, db: Session = Depends(get_db)) -> SystemR
system_key=system.system_key, system_key=system.system_key,
system_name=system.name, system_name=system.name,
name=row.name, name=row.name,
idp_role_name=row.idp_role_name, provider_role_name=row.provider_role_name,
description=row.description, description=row.description,
status=row.status, status=row.status,
) )
@@ -625,7 +625,7 @@ def list_members(
limit: int = Query(default=100, ge=1, le=500), limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
) -> ListResponse: ) -> ListResponse:
sync_from_keycloak(db) sync_from_provider(db)
repo = UsersRepository(db) repo = UsersRepository(db)
rows, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset) rows, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset)
return ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset) return ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset)
@@ -636,11 +636,11 @@ def create_member(payload: MemberUpsertRequest, db: Session = Depends(get_db)) -
users_repo = UsersRepository(db) users_repo = UsersRepository(db)
resolved_sub = payload.user_sub resolved_sub = payload.user_sub
idp_user_id: str | None = None provider_user_id: str | None = None
if payload.sync_to_idp: if payload.sync_to_idp:
if not payload.email: if not payload.email:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email_required_for_idp_sync") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email_required_for_idp_sync")
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
sync_result = idp.ensure_user( sync_result = idp.ensure_user(
sub=payload.user_sub, sub=payload.user_sub,
email=payload.email, email=payload.email,
@@ -648,7 +648,7 @@ def create_member(payload: MemberUpsertRequest, db: Session = Depends(get_db)) -
display_name=payload.display_name, display_name=payload.display_name,
is_active=payload.is_active, is_active=payload.is_active,
) )
idp_user_id = sync_result.user_id provider_user_id = sync_result.user_id
resolved_sub = resolved_sub or sync_result.user_sub resolved_sub = resolved_sub or sync_result.user_sub
if not resolved_sub: if not resolved_sub:
@@ -661,7 +661,7 @@ def create_member(payload: MemberUpsertRequest, db: Session = Depends(get_db)) -
display_name=payload.display_name, display_name=payload.display_name,
is_active=payload.is_active, is_active=payload.is_active,
status=payload.status, status=payload.status,
idp_user_id=idp_user_id, provider_user_id=provider_user_id,
) )
return _member_item(user) return _member_item(user)
@@ -681,16 +681,16 @@ def update_member(user_sub: str, payload: MemberUpdateRequest, db: Session = Dep
if payload.sync_to_idp: if payload.sync_to_idp:
if not next_email: if not next_email:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email_required_for_idp_sync") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="email_required_for_idp_sync")
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
sync_result = idp.ensure_user( sync_result = idp.ensure_user(
sub=user.user_sub, sub=user.user_sub,
email=next_email, email=next_email,
username=next_username, username=next_username,
display_name=next_display_name, display_name=next_display_name,
is_active=next_is_active, is_active=next_is_active,
idp_user_id=user.idp_user_id, provider_user_id=user.provider_user_id,
) )
user.idp_user_id = sync_result.user_id user.provider_user_id = sync_result.user_id
updated = users_repo.update_member( updated = users_repo.update_member(
user, user,
@@ -711,8 +711,8 @@ def delete_member(user_sub: str, db: Session = Depends(get_db), sync_to_idp: boo
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
if sync_to_idp: if sync_to_idp:
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
idp.delete_user(idp_user_id=user.idp_user_id, email=user.email, username=user.username) idp.delete_user(provider_user_id=user.provider_user_id, email=user.email, username=user.username)
users_repo.delete(user) users_repo.delete(user)
return {"deleted": user_sub} return {"deleted": user_sub}
@@ -725,10 +725,10 @@ def reset_member_password(user_sub: str, db: Session = Depends(get_db)) -> Membe
if not user: if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
result = idp.reset_password(idp_user_id=user.idp_user_id, email=user.email, username=user.username) result = idp.reset_password(provider_user_id=user.provider_user_id, email=user.email, username=user.username)
if user.idp_user_id != result.user_id: if user.provider_user_id != result.user_id:
user.idp_user_id = result.user_id user.provider_user_id = result.user_id
db.commit() db.commit()
return MemberPasswordResetResponse(user_sub=user_sub, temporary_password=result.temporary_password) return MemberPasswordResetResponse(user_sub=user_sub, temporary_password=result.temporary_password)
@@ -811,7 +811,7 @@ def list_member_effective_roles(user_sub: str, db: Session = Depends(get_db)) ->
system_name=system.name, system_name=system.name,
role_key=role.role_key, role_key=role.role_key,
role_name=role.name, role_name=role.name,
idp_role_name=role.idp_role_name, provider_role_name=role.provider_role_name,
) )
for site, company, role, system in rows for site, company, role, system in rows
] ]
@@ -836,25 +836,27 @@ def list_api_clients(
) )
@router.post("/sync/from-keycloak") @router.post("/sync/from-provider")
def sync_catalog_from_keycloak(db: Session = Depends(get_db), force: bool = Query(default=True)) -> dict[str, int]: @router.post("/sync/from-keycloak", include_in_schema=False)
return sync_from_keycloak(db, force=force) def sync_catalog_from_provider(db: Session = Depends(get_db), force: bool = Query(default=True)) -> dict[str, int]:
return sync_from_provider(db, force=force)
@router.post("/sync/keycloak-group-names") @router.post("/sync/provider-group-names")
def sync_keycloak_group_names(db: Session = Depends(get_db)) -> dict[str, int]: @router.post("/sync/keycloak-group-names", include_in_schema=False)
def sync_provider_group_names(db: Session = Depends(get_db)) -> dict[str, int]:
companies_repo = CompaniesRepository(db) companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db) sites_repo = SitesRepository(db)
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
companies, _ = companies_repo.list(limit=5000, offset=0) companies, _ = companies_repo.list(limit=5000, offset=0)
company_count = 0 company_count = 0
for company in companies: for company in companies:
if not company.idp_group_id: if not company.provider_group_id:
continue continue
group_name = _company_group_name(company.display_name, company.company_key) group_name = _company_group_name(company.display_name, company.company_key)
idp.ensure_group( idp.ensure_group(
group_id=company.idp_group_id, group_id=company.provider_group_id,
name=group_name, name=group_name,
attributes={ attributes={
"member_entity_type": "company", "member_entity_type": "company",
@@ -869,16 +871,16 @@ def sync_keycloak_group_names(db: Session = Depends(get_db)) -> dict[str, int]:
site_count = 0 site_count = 0
company_map = {company.id: company for company in companies} company_map = {company.id: company for company in companies}
for site in sites: for site in sites:
if not site.idp_group_id: if not site.provider_group_id:
continue continue
company = company_map.get(site.company_id) company = company_map.get(site.company_id)
if not company: if not company:
continue continue
group_name = _site_group_name(site.display_name, site.site_key) group_name = _site_group_name(site.display_name, site.site_key)
idp.ensure_group( idp.ensure_group(
group_id=site.idp_group_id, group_id=site.provider_group_id,
name=group_name, name=group_name,
parent_group_id=company.idp_group_id, parent_group_id=company.provider_group_id,
attributes={ attributes={
"member_entity_type": "site", "member_entity_type": "site",
"site_key": site.site_key, "site_key": site.site_key,

View File

@@ -5,12 +5,12 @@ from app.core.config import get_settings
from app.db.session import get_db from app.db.session import get_db
from app.repositories.users_repo import UsersRepository from app.repositories.users_repo import UsersRepository
from app.repositories.user_sites_repo import UserSitesRepository from app.repositories.user_sites_repo import UserSitesRepository
from app.schemas.idp_admin import KeycloakEnsureUserRequest, KeycloakEnsureUserResponse from app.schemas.idp_admin import ProviderEnsureUserRequest, ProviderEnsureUserResponse
from app.schemas.internal import InternalUpsertUserBySubResponse, InternalUserRoleItem, InternalUserRoleResponse from app.schemas.internal import InternalUpsertUserBySubResponse, InternalUserRoleItem, InternalUserRoleResponse
from app.schemas.permissions import RoleSnapshotResponse from app.schemas.permissions import RoleSnapshotResponse
from app.schemas.users import UserUpsertBySubRequest from app.schemas.users import UserUpsertBySubRequest
from app.security.api_client_auth import require_api_client from app.security.api_client_auth import require_api_client
from app.services.idp_admin_service import KeycloakAdminService from app.services.idp_admin_service import ProviderAdminService
from app.services.permission_service import PermissionService from app.services.permission_service import PermissionService
router = APIRouter(prefix="/internal", tags=["internal"], dependencies=[Depends(require_api_client)]) router = APIRouter(prefix="/internal", tags=["internal"], dependencies=[Depends(require_api_client)])
@@ -33,7 +33,7 @@ def upsert_user_by_sub(
return InternalUpsertUserBySubResponse( return InternalUpsertUserBySubResponse(
id=user.id, id=user.id,
user_sub=user.user_sub, user_sub=user.user_sub,
idp_user_id=user.idp_user_id, provider_user_id=user.provider_user_id,
username=user.username, username=user.username,
email=user.email, email=user.email,
display_name=user.display_name, display_name=user.display_name,
@@ -61,7 +61,7 @@ def _build_user_role_rows(db: Session, user_sub: str) -> list[tuple[str, str, st
system.name, system.name,
role.role_key, role.role_key,
role.name, role.name,
role.idp_role_name, role.provider_role_name,
) )
for site, company, role, system in rows for site, company, role, system in rows
] ]
@@ -82,7 +82,7 @@ def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUser
system_name=system_name, system_name=system_name,
role_key=role_key, role_key=role_key,
role_name=role_name, role_name=role_name,
idp_role_name=idp_role_name, provider_role_name=provider_role_name,
) )
for ( for (
site_key, site_key,
@@ -93,7 +93,7 @@ def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUser
system_name, system_name,
role_key, role_key,
role_name, role_name,
idp_role_name, provider_role_name,
) in rows ) in rows
], ],
) )
@@ -108,14 +108,15 @@ def get_permission_snapshot(
return PermissionService.build_role_snapshot(user_sub=user_sub, rows=rows) return PermissionService.build_role_snapshot(user_sub=user_sub, rows=rows)
@router.post("/idp/users/ensure", response_model=KeycloakEnsureUserResponse) @router.post("/provider/users/ensure", response_model=ProviderEnsureUserResponse)
@router.post("/keycloak/users/ensure", response_model=KeycloakEnsureUserResponse) @router.post("/idp/users/ensure", response_model=ProviderEnsureUserResponse, include_in_schema=False)
@router.post("/keycloak/users/ensure", response_model=ProviderEnsureUserResponse, include_in_schema=False)
def ensure_idp_user( def ensure_idp_user(
payload: KeycloakEnsureUserRequest, payload: ProviderEnsureUserRequest,
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> KeycloakEnsureUserResponse: ) -> ProviderEnsureUserResponse:
settings = get_settings() settings = get_settings()
idp_service = KeycloakAdminService(settings=settings) idp_service = ProviderAdminService(settings=settings)
sync_result = idp_service.ensure_user( sync_result = idp_service.ensure_user(
sub=payload.user_sub, sub=payload.user_sub,
email=payload.email, email=payload.email,
@@ -136,6 +137,6 @@ def ensure_idp_user(
display_name=payload.display_name, display_name=payload.display_name,
is_active=payload.is_active, is_active=payload.is_active,
status="active", status="active",
idp_user_id=sync_result.user_id, provider_user_id=sync_result.user_id,
) )
return KeycloakEnsureUserResponse(idp_user_id=sync_result.user_id, action=sync_result.action) return ProviderEnsureUserResponse(provider_user_id=sync_result.user_id, action=sync_result.action)

View File

@@ -34,7 +34,7 @@ def internal_list_systems(
"id": i.id, "id": i.id,
"system_key": i.system_key, "system_key": i.system_key,
"name": i.name, "name": i.name,
"idp_client_id": i.idp_client_id, "provider_client_id": i.provider_client_id,
"status": i.status, "status": i.status,
} }
for i in items for i in items
@@ -72,7 +72,7 @@ def internal_list_roles(
system_key=system_map[i.system_id].system_key, system_key=system_map[i.system_id].system_key,
system_name=system_map[i.system_id].name, system_name=system_map[i.system_id].name,
name=i.name, name=i.name,
idp_role_name=i.idp_role_name, provider_role_name=i.provider_role_name,
description=i.description, description=i.description,
status=i.status, status=i.status,
) )

View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Session
from app.db.session import get_db from app.db.session import get_db
from app.repositories.users_repo import UsersRepository from app.repositories.users_repo import UsersRepository
from app.repositories.user_sites_repo import UserSitesRepository from app.repositories.user_sites_repo import UserSitesRepository
from app.schemas.auth import KeycloakPrincipal, MeSummaryResponse from app.schemas.auth import ProviderPrincipal, MeSummaryResponse
from app.schemas.permissions import RoleSnapshotResponse from app.schemas.permissions import RoleSnapshotResponse
from app.security.idp_jwt import require_authenticated_principal from app.security.idp_jwt import require_authenticated_principal
from app.services.permission_service import PermissionService from app.services.permission_service import PermissionService
@@ -15,7 +15,7 @@ router = APIRouter(prefix="/me", tags=["me"])
@router.get("", response_model=MeSummaryResponse) @router.get("", response_model=MeSummaryResponse)
def get_me( def get_me(
principal: KeycloakPrincipal = Depends(require_authenticated_principal), principal: ProviderPrincipal = Depends(require_authenticated_principal),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> MeSummaryResponse: ) -> MeSummaryResponse:
try: try:
@@ -39,7 +39,7 @@ def get_me(
@router.get("/permissions/snapshot", response_model=RoleSnapshotResponse) @router.get("/permissions/snapshot", response_model=RoleSnapshotResponse)
def get_my_permission_snapshot( def get_my_permission_snapshot(
principal: KeycloakPrincipal = Depends(require_authenticated_principal), principal: ProviderPrincipal = Depends(require_authenticated_principal),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> RoleSnapshotResponse: ) -> RoleSnapshotResponse:
try: try:
@@ -65,7 +65,7 @@ def get_my_permission_snapshot(
system.name, system.name,
role.role_key, role.role_key,
role.name, role.name,
role.idp_role_name, role.provider_role_name,
) )
for site, company, role, system in rows for site, company, role, system in rows
] ]

View File

@@ -15,7 +15,7 @@ class Company(Base):
company_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True) company_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
display_name: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str] = mapped_column(String(255), nullable=False)
legal_name: Mapped[str | None] = mapped_column(String(255)) legal_name: Mapped[str | None] = mapped_column(String(255))
idp_group_id: Mapped[str | None] = mapped_column(String(128)) provider_group_id: Mapped[str | None] = mapped_column(String(128))
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(

View File

@@ -10,14 +10,14 @@ from app.db.base import Base
class Role(Base): class Role(Base):
__tablename__ = "roles" __tablename__ = "roles"
__table_args__ = (UniqueConstraint("system_id", "idp_role_name", name="uq_roles_system_idp_role_name"),) __table_args__ = (UniqueConstraint("system_id", "provider_role_name", name="uq_roles_system_provider_role_name"),)
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
role_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True) role_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
system_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("systems.id", ondelete="CASCADE"), nullable=False) system_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("systems.id", ondelete="CASCADE"), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(String(1024)) description: Mapped[str | None] = mapped_column(String(1024))
idp_role_name: Mapped[str] = mapped_column(String(255), nullable=False) provider_role_name: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(

View File

@@ -16,7 +16,7 @@ class Site(Base):
company_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False) company_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False)
display_name: Mapped[str] = mapped_column(String(255), nullable=False) display_name: Mapped[str] = mapped_column(String(255), nullable=False)
domain: Mapped[str | None] = mapped_column(String(255)) domain: Mapped[str | None] = mapped_column(String(255))
idp_group_id: Mapped[str | None] = mapped_column(String(128)) provider_group_id: Mapped[str | None] = mapped_column(String(128))
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(

View File

@@ -14,7 +14,7 @@ class System(Base):
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
system_key: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True) system_key: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False) name: Mapped[str] = mapped_column(String(255), nullable=False)
idp_client_id: Mapped[str] = mapped_column(String(128), unique=True, nullable=False) provider_client_id: Mapped[str] = mapped_column(String(128), unique=True, nullable=False)
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active") status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
updated_at: Mapped[datetime] = mapped_column( updated_at: Mapped[datetime] = mapped_column(

View File

@@ -13,7 +13,7 @@ class User(Base):
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4())) id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
user_sub: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) user_sub: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
idp_user_id: Mapped[str | None] = mapped_column(String(128), unique=True) provider_user_id: Mapped[str | None] = mapped_column(String(128), unique=True)
username: Mapped[str | None] = mapped_column(String(255), unique=True) username: Mapped[str | None] = mapped_column(String(255), unique=True)
email: Mapped[str | None] = mapped_column(String(320), unique=True) email: Mapped[str | None] = mapped_column(String(320), unique=True)
display_name: Mapped[str | None] = mapped_column(String(255)) display_name: Mapped[str | None] = mapped_column(String(255))

View File

@@ -36,14 +36,14 @@ class CompaniesRepository:
company_key: str, company_key: str,
display_name: str, display_name: str,
legal_name: str | None, legal_name: str | None,
idp_group_id: str | None = None, provider_group_id: str | None = None,
status: str = "active", status: str = "active",
) -> Company: ) -> Company:
item = Company( item = Company(
company_key=company_key, company_key=company_key,
display_name=display_name, display_name=display_name,
legal_name=legal_name, legal_name=legal_name,
idp_group_id=idp_group_id, provider_group_id=provider_group_id,
status=status, status=status,
) )
self.db.add(item) self.db.add(item)
@@ -57,15 +57,15 @@ class CompaniesRepository:
*, *,
display_name: str | None = None, display_name: str | None = None,
legal_name: str | None = None, legal_name: str | None = None,
idp_group_id: str | None = None, provider_group_id: str | None = None,
status: str | None = None, status: str | None = None,
) -> Company: ) -> Company:
if display_name is not None: if display_name is not None:
item.display_name = display_name item.display_name = display_name
if legal_name is not None: if legal_name is not None:
item.legal_name = legal_name item.legal_name = legal_name
if idp_group_id is not None: if provider_group_id is not None:
item.idp_group_id = idp_group_id item.provider_group_id = provider_group_id
if status is not None: if status is not None:
item.status = status item.status = status
self.db.commit() self.db.commit()

View File

@@ -30,7 +30,7 @@ class RolesRepository:
cond = or_( cond = or_(
Role.role_key.ilike(pattern), Role.role_key.ilike(pattern),
Role.name.ilike(pattern), Role.name.ilike(pattern),
Role.idp_role_name.ilike(pattern), Role.provider_role_name.ilike(pattern),
Role.description.ilike(pattern), Role.description.ilike(pattern),
) )
stmt = stmt.where(cond) stmt = stmt.where(cond)
@@ -52,7 +52,7 @@ class RolesRepository:
system_id: str, system_id: str,
name: str, name: str,
description: str | None, description: str | None,
idp_role_name: str, provider_role_name: str,
status: str = "active", status: str = "active",
) -> Role: ) -> Role:
item = Role( item = Role(
@@ -60,7 +60,7 @@ class RolesRepository:
system_id=system_id, system_id=system_id,
name=name, name=name,
description=description, description=description,
idp_role_name=idp_role_name, provider_role_name=provider_role_name,
status=status, status=status,
) )
self.db.add(item) self.db.add(item)
@@ -75,7 +75,7 @@ class RolesRepository:
system_id: str | None = None, system_id: str | None = None,
name: str | None = None, name: str | None = None,
description: str | None = None, description: str | None = None,
idp_role_name: str | None = None, provider_role_name: str | None = None,
status: str | None = None, status: str | None = None,
) -> Role: ) -> Role:
if system_id is not None: if system_id is not None:
@@ -84,8 +84,8 @@ class RolesRepository:
item.name = name item.name = name
if description is not None: if description is not None:
item.description = description item.description = description
if idp_role_name is not None: if provider_role_name is not None:
item.idp_role_name = idp_role_name item.provider_role_name = provider_role_name
if status is not None: if status is not None:
item.status = status item.status = status
self.db.commit() self.db.commit()

View File

@@ -45,7 +45,7 @@ class SitesRepository:
company_id: str, company_id: str,
display_name: str, display_name: str,
domain: str | None, domain: str | None,
idp_group_id: str | None = None, provider_group_id: str | None = None,
status: str = "active", status: str = "active",
) -> Site: ) -> Site:
item = Site( item = Site(
@@ -53,7 +53,7 @@ class SitesRepository:
company_id=company_id, company_id=company_id,
display_name=display_name, display_name=display_name,
domain=domain, domain=domain,
idp_group_id=idp_group_id, provider_group_id=provider_group_id,
status=status, status=status,
) )
self.db.add(item) self.db.add(item)
@@ -68,7 +68,7 @@ class SitesRepository:
company_id: str | None = None, company_id: str | None = None,
display_name: str | None = None, display_name: str | None = None,
domain: str | None = None, domain: str | None = None,
idp_group_id: str | None = None, provider_group_id: str | None = None,
status: str | None = None, status: str | None = None,
) -> Site: ) -> Site:
if company_id is not None: if company_id is not None:
@@ -77,8 +77,8 @@ class SitesRepository:
item.display_name = display_name item.display_name = display_name
if domain is not None: if domain is not None:
item.domain = domain item.domain = domain
if idp_group_id is not None: if provider_group_id is not None:
item.idp_group_id = idp_group_id item.provider_group_id = provider_group_id
if status is not None: if status is not None:
item.status = status item.status = status
self.db.commit() self.db.commit()

View File

@@ -19,7 +19,7 @@ class SystemsRepository:
count_stmt = select(func.count()).select_from(System) count_stmt = select(func.count()).select_from(System)
if keyword: if keyword:
pattern = f"%{keyword}%" pattern = f"%{keyword}%"
cond = or_(System.system_key.ilike(pattern), System.name.ilike(pattern), System.idp_client_id.ilike(pattern)) cond = or_(System.system_key.ilike(pattern), System.name.ilike(pattern), System.provider_client_id.ilike(pattern))
stmt = stmt.where(cond) stmt = stmt.where(cond)
count_stmt = count_stmt.where(cond) count_stmt = count_stmt.where(cond)
if status: if status:
@@ -29,8 +29,8 @@ class SystemsRepository:
stmt = stmt.order_by(System.created_at.desc()).limit(limit).offset(offset) stmt = stmt.order_by(System.created_at.desc()).limit(limit).offset(offset)
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0) return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
def create(self, *, system_key: str, name: str, idp_client_id: str, status: str = "active") -> System: def create(self, *, system_key: str, name: str, provider_client_id: str, status: str = "active") -> System:
item = System(system_key=system_key, name=name, idp_client_id=idp_client_id, status=status) item = System(system_key=system_key, name=name, provider_client_id=provider_client_id, status=status)
self.db.add(item) self.db.add(item)
self.db.commit() self.db.commit()
self.db.refresh(item) self.db.refresh(item)
@@ -41,13 +41,13 @@ class SystemsRepository:
item: System, item: System,
*, *,
name: str | None = None, name: str | None = None,
idp_client_id: str | None = None, provider_client_id: str | None = None,
status: str | None = None, status: str | None = None,
) -> System: ) -> System:
if name is not None: if name is not None:
item.name = name item.name = name
if idp_client_id is not None: if provider_client_id is not None:
item.idp_client_id = idp_client_id item.provider_client_id = provider_client_id
if status is not None: if status is not None:
item.status = status item.status = status
self.db.commit() self.db.commit()

View File

@@ -54,13 +54,13 @@ class UsersRepository:
display_name: str | None, display_name: str | None,
is_active: bool, is_active: bool,
status: str = "active", status: str = "active",
idp_user_id: str | None = None, provider_user_id: str | None = None,
) -> User: ) -> User:
user = self.get_by_sub(user_sub) user = self.get_by_sub(user_sub)
if user is None: if user is None:
user = User( user = User(
user_sub=user_sub, user_sub=user_sub,
idp_user_id=idp_user_id, provider_user_id=provider_user_id,
username=username, username=username,
email=email, email=email,
display_name=display_name, display_name=display_name,
@@ -69,8 +69,8 @@ class UsersRepository:
) )
self.db.add(user) self.db.add(user)
else: else:
if idp_user_id is not None: if provider_user_id is not None:
user.idp_user_id = idp_user_id user.provider_user_id = provider_user_id
user.username = username user.username = username
user.email = email user.email = email
user.display_name = display_name user.display_name = display_name

View File

@@ -1,7 +1,7 @@
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class KeycloakPrincipal(BaseModel): class ProviderPrincipal(BaseModel):
sub: str sub: str
email: str | None = None email: str | None = None
name: str | None = None name: str | None = None

View File

@@ -19,7 +19,7 @@ class CompanyCreateRequest(BaseModel):
class CompanyUpdateRequest(BaseModel): class CompanyUpdateRequest(BaseModel):
display_name: str | None = None display_name: str | None = None
legal_name: str | None = None legal_name: str | None = None
idp_group_id: str | None = None provider_group_id: str | None = None
status: str | None = None status: str | None = None
@@ -28,7 +28,7 @@ class CompanyItem(BaseModel):
company_key: str company_key: str
display_name: str display_name: str
legal_name: str | None = None legal_name: str | None = None
idp_group_id: str | None = None provider_group_id: str | None = None
status: str status: str
@@ -43,7 +43,7 @@ class SiteUpdateRequest(BaseModel):
company_key: str | None = None company_key: str | None = None
display_name: str | None = None display_name: str | None = None
domain: str | None = None domain: str | None = None
idp_group_id: str | None = None provider_group_id: str | None = None
status: str | None = None status: str | None = None
@@ -54,19 +54,19 @@ class SiteItem(BaseModel):
company_display_name: str company_display_name: str
display_name: str display_name: str
domain: str | None = None domain: str | None = None
idp_group_id: str | None = None provider_group_id: str | None = None
status: str status: str
class SystemCreateRequest(BaseModel): class SystemCreateRequest(BaseModel):
name: str name: str
idp_client_id: str provider_client_id: str
status: str = "active" status: str = "active"
class SystemUpdateRequest(BaseModel): class SystemUpdateRequest(BaseModel):
name: str | None = None name: str | None = None
idp_client_id: str | None = None provider_client_id: str | None = None
status: str | None = None status: str | None = None
@@ -74,14 +74,14 @@ class SystemItem(BaseModel):
id: str id: str
system_key: str system_key: str
name: str name: str
idp_client_id: str provider_client_id: str
status: str status: str
class RoleCreateRequest(BaseModel): class RoleCreateRequest(BaseModel):
system_key: str system_key: str
name: str name: str
idp_role_name: str provider_role_name: str
description: str | None = None description: str | None = None
status: str = "active" status: str = "active"
@@ -89,7 +89,7 @@ class RoleCreateRequest(BaseModel):
class RoleUpdateRequest(BaseModel): class RoleUpdateRequest(BaseModel):
system_key: str | None = None system_key: str | None = None
name: str | None = None name: str | None = None
idp_role_name: str | None = None provider_role_name: str | None = None
description: str | None = None description: str | None = None
status: str | None = None status: str | None = None
@@ -100,7 +100,7 @@ class RoleItem(BaseModel):
system_key: str system_key: str
system_name: str system_name: str
name: str name: str
idp_role_name: str provider_role_name: str
description: str | None = None description: str | None = None
status: str status: str
@@ -108,7 +108,7 @@ class RoleItem(BaseModel):
class MemberItem(BaseModel): class MemberItem(BaseModel):
id: str id: str
user_sub: str user_sub: str
idp_user_id: str | None = None provider_user_id: str | None = None
username: str | None = None username: str | None = None
email: str | None = None email: str | None = None
display_name: str | None = None display_name: str | None = None
@@ -173,7 +173,7 @@ class UserEffectiveRoleItem(BaseModel):
system_name: str system_name: str
role_key: str role_key: str
role_name: str role_name: str
idp_role_name: str provider_role_name: str
class UserEffectiveRolesResponse(BaseModel): class UserEffectiveRolesResponse(BaseModel):

View File

@@ -1,7 +1,7 @@
from pydantic import AliasChoices, BaseModel, Field from pydantic import AliasChoices, BaseModel, Field
class KeycloakEnsureUserRequest(BaseModel): class ProviderEnsureUserRequest(BaseModel):
user_sub: str | None = Field(default=None, validation_alias=AliasChoices("user_sub", "sub")) user_sub: str | None = Field(default=None, validation_alias=AliasChoices("user_sub", "sub"))
username: str | None = None username: str | None = None
email: str email: str
@@ -9,6 +9,6 @@ class KeycloakEnsureUserRequest(BaseModel):
is_active: bool = True is_active: bool = True
class KeycloakEnsureUserResponse(BaseModel): class ProviderEnsureUserResponse(BaseModel):
idp_user_id: str provider_user_id: str
action: str action: str

View File

@@ -5,7 +5,7 @@ class InternalSystemItem(BaseModel):
id: str id: str
system_key: str system_key: str
name: str name: str
idp_client_id: str provider_client_id: str
status: str status: str
@@ -22,7 +22,7 @@ class InternalRoleItem(BaseModel):
system_key: str system_key: str
system_name: str system_name: str
name: str name: str
idp_role_name: str provider_role_name: str
description: str | None = None description: str | None = None
status: str status: str
@@ -86,7 +86,7 @@ class InternalMemberListResponse(BaseModel):
class InternalUpsertUserBySubResponse(BaseModel): class InternalUpsertUserBySubResponse(BaseModel):
id: str id: str
user_sub: str user_sub: str
idp_user_id: str | None = None provider_user_id: str | None = None
username: str | None = None username: str | None = None
email: str | None = None email: str | None = None
display_name: str | None = None display_name: str | None = None
@@ -103,7 +103,7 @@ class InternalUserRoleItem(BaseModel):
system_name: str system_name: str
role_key: str role_key: str
role_name: str role_name: str
idp_role_name: str provider_role_name: str
class InternalUserRoleResponse(BaseModel): class InternalUserRoleResponse(BaseModel):

View File

@@ -10,7 +10,7 @@ class RoleSnapshotItem(BaseModel):
system_name: str system_name: str
role_key: str role_key: str
role_name: str role_name: str
idp_role_name: str provider_role_name: str
class RoleSnapshotResponse(BaseModel): class RoleSnapshotResponse(BaseModel):

View File

@@ -1,7 +1,7 @@
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from app.core.config import get_settings from app.core.config import get_settings
from app.schemas.auth import KeycloakPrincipal from app.schemas.auth import ProviderPrincipal
from app.security.idp_jwt import require_authenticated_principal from app.security.idp_jwt import require_authenticated_principal
@@ -21,8 +21,8 @@ def _expand_group_aliases(groups: set[str]) -> set[str]:
def require_admin_principal( def require_admin_principal(
principal: KeycloakPrincipal = Depends(require_authenticated_principal), principal: ProviderPrincipal = Depends(require_authenticated_principal),
) -> KeycloakPrincipal: ) -> ProviderPrincipal:
settings = get_settings() settings = get_settings()
required_groups = _expand_group_aliases(set(settings.admin_required_groups)) required_groups = _expand_group_aliases(set(settings.admin_required_groups))

View File

@@ -9,13 +9,13 @@ from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from app.core.config import get_settings from app.core.config import get_settings
from app.schemas.auth import KeycloakPrincipal from app.schemas.auth import ProviderPrincipal
bearer_scheme = HTTPBearer(auto_error=False) bearer_scheme = HTTPBearer(auto_error=False)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class KeycloakTokenVerifier: class ProviderTokenVerifier:
def __init__( def __init__(
self, self,
issuer: str | None, issuer: str | None,
@@ -99,7 +99,7 @@ class KeycloakTokenVerifier:
return base_url.rstrip("/") + "/realms/master/protocol/openid-connect/userinfo" return base_url.rstrip("/") + "/realms/master/protocol/openid-connect/userinfo"
return None return None
def _enrich_from_userinfo(self, principal: KeycloakPrincipal, token: str) -> KeycloakPrincipal: def _enrich_from_userinfo(self, principal: ProviderPrincipal, token: str) -> ProviderPrincipal:
if principal.email and (principal.name or principal.preferred_username) and principal.groups: if principal.email and (principal.name or principal.preferred_username) and principal.groups:
return principal return principal
if not self.userinfo_endpoint: if not self.userinfo_endpoint:
@@ -132,7 +132,7 @@ class KeycloakTokenVerifier:
payload_groups = data.get("groups") payload_groups = data.get("groups")
if isinstance(payload_groups, list): if isinstance(payload_groups, list):
groups = [str(g) for g in payload_groups if str(g)] groups = [str(g) for g in payload_groups if str(g)]
enriched = KeycloakPrincipal( enriched = ProviderPrincipal(
sub=principal.sub, sub=principal.sub,
email=email, email=email,
name=name, name=name,
@@ -169,7 +169,7 @@ class KeycloakTokenVerifier:
token = resp.json().get("access_token") token = resp.json().get("access_token")
return str(token) if token else None return str(token) if token else None
def _enrich_groups_from_admin(self, principal: KeycloakPrincipal) -> KeycloakPrincipal: def _enrich_groups_from_admin(self, principal: ProviderPrincipal) -> ProviderPrincipal:
if principal.groups: if principal.groups:
return principal return principal
if not self.base_url or not self.realm: if not self.base_url or not self.realm:
@@ -204,7 +204,7 @@ class KeycloakTokenVerifier:
groups.append(name) groups.append(name)
if not groups: if not groups:
return principal return principal
return KeycloakPrincipal( return ProviderPrincipal(
sub=principal.sub, sub=principal.sub,
email=principal.email, email=principal.email,
name=principal.name, name=principal.name,
@@ -212,7 +212,7 @@ class KeycloakTokenVerifier:
groups=groups, groups=groups,
) )
def verify_access_token(self, token: str) -> KeycloakPrincipal: def verify_access_token(self, token: str) -> ProviderPrincipal:
try: try:
header = jwt.get_unverified_header(token) header = jwt.get_unverified_header(token)
algorithm = str(header.get("alg", "")).upper() algorithm = str(header.get("alg", "")).upper()
@@ -255,7 +255,7 @@ class KeycloakTokenVerifier:
if not sub: if not sub:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="token_missing_sub") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="token_missing_sub")
principal = KeycloakPrincipal( principal = ProviderPrincipal(
sub=sub, sub=sub,
email=claims.get("email"), email=claims.get("email"),
name=claims.get("name"), name=claims.get("name"),
@@ -266,9 +266,9 @@ class KeycloakTokenVerifier:
@lru_cache @lru_cache
def _get_verifier() -> KeycloakTokenVerifier: def _get_verifier() -> ProviderTokenVerifier:
settings = get_settings() settings = get_settings()
return KeycloakTokenVerifier( return ProviderTokenVerifier(
issuer=settings.idp_issuer, issuer=settings.idp_issuer,
jwks_url=settings.idp_jwks_url, jwks_url=settings.idp_jwks_url,
audience=settings.idp_audience, audience=settings.idp_audience,
@@ -286,7 +286,7 @@ def _get_verifier() -> KeycloakTokenVerifier:
def require_authenticated_principal( def require_authenticated_principal(
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
) -> KeycloakPrincipal: ) -> ProviderPrincipal:
if credentials is None or credentials.scheme.lower() != "bearer": if credentials is None or credentials.scheme.lower() != "bearer":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing_bearer_token") raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing_bearer_token")

View File

@@ -11,31 +11,31 @@ from app.core.config import Settings
@dataclass @dataclass
class KeycloakSyncResult: class ProviderSyncResult:
user_id: str user_id: str
action: str action: str
user_sub: str | None = None user_sub: str | None = None
@dataclass @dataclass
class KeycloakPasswordResetResult: class ProviderPasswordResetResult:
user_id: str user_id: str
temporary_password: str temporary_password: str
@dataclass @dataclass
class KeycloakDeleteResult: class ProviderDeleteResult:
action: str action: str
user_id: str | None = None user_id: str | None = None
@dataclass @dataclass
class KeycloakGroupSyncResult: class ProviderGroupSyncResult:
group_id: str group_id: str
action: str action: str
class KeycloakAdminService: class ProviderAdminService:
def __init__(self, settings: Settings) -> None: def __init__(self, settings: Settings) -> None:
self.base_url = settings.keycloak_base_url.rstrip("/") self.base_url = settings.keycloak_base_url.rstrip("/")
self.realm = settings.keycloak_realm self.realm = settings.keycloak_realm
@@ -188,8 +188,8 @@ class KeycloakAdminService:
username: str | None, username: str | None,
display_name: str | None, display_name: str | None,
is_active: bool = True, is_active: bool = True,
idp_user_id: str | None = None, provider_user_id: str | None = None,
) -> KeycloakSyncResult: ) -> ProviderSyncResult:
resolved_username = username or self._safe_username(sub=sub, email=email) resolved_username = username or self._safe_username(sub=sub, email=email)
first_name = display_name or resolved_username first_name = display_name or resolved_username
payload = { payload = {
@@ -202,7 +202,7 @@ class KeycloakAdminService:
} }
with self._client() as client: with self._client() as client:
existing = self._lookup_user_by_id(client, idp_user_id) if idp_user_id else None existing = self._lookup_user_by_id(client, provider_user_id) if provider_user_id else None
if existing is None: if existing is None:
existing = self._lookup_user_by_email_or_username(client, email=email, username=resolved_username) existing = self._lookup_user_by_email_or_username(client, email=email, username=resolved_username)
@@ -211,7 +211,7 @@ class KeycloakAdminService:
put_resp = client.put(f"/admin/realms/{self.realm}/users/{user_id}", json=payload) put_resp = client.put(f"/admin/realms/{self.realm}/users/{user_id}", json=payload)
if put_resp.status_code >= 400: if put_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_update_failed") raise HTTPException(status_code=502, detail="idp_update_failed")
return KeycloakSyncResult(user_id=user_id, action="updated", user_sub=user_id) return ProviderSyncResult(user_id=user_id, action="updated", user_sub=user_id)
create_resp = client.post(f"/admin/realms/{self.realm}/users", json=payload) create_resp = client.post(f"/admin/realms/{self.realm}/users", json=payload)
if create_resp.status_code >= 400: if create_resp.status_code >= 400:
@@ -224,7 +224,7 @@ class KeycloakAdminService:
user_id = str(found["id"]) if found and found.get("id") else "" user_id = str(found["id"]) if found and found.get("id") else ""
if not user_id: if not user_id:
raise HTTPException(status_code=502, detail="idp_create_failed") raise HTTPException(status_code=502, detail="idp_create_failed")
return KeycloakSyncResult(user_id=user_id, action="created", user_sub=user_id) return ProviderSyncResult(user_id=user_id, action="created", user_sub=user_id)
def ensure_group( def ensure_group(
self, self,
@@ -233,7 +233,7 @@ class KeycloakAdminService:
group_id: str | None = None, group_id: str | None = None,
parent_group_id: str | None = None, parent_group_id: str | None = None,
attributes: dict[str, str | list[str]] | None = None, attributes: dict[str, str | list[str]] | None = None,
) -> KeycloakGroupSyncResult: ) -> ProviderGroupSyncResult:
if not name: if not name:
raise HTTPException(status_code=400, detail="idp_group_name_required") raise HTTPException(status_code=400, detail="idp_group_name_required")
normalized_attrs = self._normalize_group_attributes(attributes) normalized_attrs = self._normalize_group_attributes(attributes)
@@ -249,7 +249,7 @@ class KeycloakAdminService:
put_resp = client.put(f"/admin/realms/{self.realm}/groups/{resolved_id}", json=payload) put_resp = client.put(f"/admin/realms/{self.realm}/groups/{resolved_id}", json=payload)
if put_resp.status_code >= 400: if put_resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_group_update_failed") raise HTTPException(status_code=502, detail="idp_group_update_failed")
return KeycloakGroupSyncResult(group_id=resolved_id, action="updated") return ProviderGroupSyncResult(group_id=resolved_id, action="updated")
payload = {"name": name, "attributes": normalized_attrs} payload = {"name": name, "attributes": normalized_attrs}
if parent_group_id: if parent_group_id:
@@ -266,28 +266,28 @@ class KeycloakAdminService:
resolved_id = str(found.get("id")) if found and found.get("id") else "" resolved_id = str(found.get("id")) if found and found.get("id") else ""
if not resolved_id: if not resolved_id:
raise HTTPException(status_code=502, detail="idp_group_create_failed") raise HTTPException(status_code=502, detail="idp_group_create_failed")
return KeycloakGroupSyncResult(group_id=resolved_id, action="created") return ProviderGroupSyncResult(group_id=resolved_id, action="created")
def delete_group(self, *, group_id: str | None) -> KeycloakDeleteResult: def delete_group(self, *, group_id: str | None) -> ProviderDeleteResult:
if not group_id: if not group_id:
return KeycloakDeleteResult(action="not_found") return ProviderDeleteResult(action="not_found")
with self._client() as client: with self._client() as client:
resp = client.delete(f"/admin/realms/{self.realm}/groups/{group_id}") resp = client.delete(f"/admin/realms/{self.realm}/groups/{group_id}")
if resp.status_code in {204, 404}: if resp.status_code in {204, 404}:
return KeycloakDeleteResult(action="deleted" if resp.status_code == 204 else "not_found") return ProviderDeleteResult(action="deleted" if resp.status_code == 204 else "not_found")
if resp.status_code >= 400: if resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_group_delete_failed") raise HTTPException(status_code=502, detail="idp_group_delete_failed")
return KeycloakDeleteResult(action="deleted") return ProviderDeleteResult(action="deleted")
def reset_password( def reset_password(
self, self,
*, *,
idp_user_id: str | None, provider_user_id: str | None,
email: str | None, email: str | None,
username: str | None, username: str | None,
) -> KeycloakPasswordResetResult: ) -> ProviderPasswordResetResult:
with self._client() as client: with self._client() as client:
existing = self._lookup_user_by_id(client, idp_user_id) if idp_user_id else None existing = self._lookup_user_by_id(client, provider_user_id) if provider_user_id else None
if existing is None: if existing is None:
existing = self._lookup_user_by_email_or_username(client, email=email, username=username) existing = self._lookup_user_by_email_or_username(client, email=email, username=username)
if not existing or not existing.get("id"): if not existing or not existing.get("id"):
@@ -301,29 +301,29 @@ class KeycloakAdminService:
) )
if resp.status_code >= 400: if resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_set_password_failed") raise HTTPException(status_code=502, detail="idp_set_password_failed")
return KeycloakPasswordResetResult(user_id=user_id, temporary_password=temp_password) return ProviderPasswordResetResult(user_id=user_id, temporary_password=temp_password)
def delete_user( def delete_user(
self, self,
*, *,
idp_user_id: str | None, provider_user_id: str | None,
email: str | None, email: str | None,
username: str | None, username: str | None,
) -> KeycloakDeleteResult: ) -> ProviderDeleteResult:
with self._client() as client: with self._client() as client:
existing = self._lookup_user_by_id(client, idp_user_id) if idp_user_id else None existing = self._lookup_user_by_id(client, provider_user_id) if provider_user_id else None
if existing is None: if existing is None:
existing = self._lookup_user_by_email_or_username(client, email=email, username=username) existing = self._lookup_user_by_email_or_username(client, email=email, username=username)
if not existing or not existing.get("id"): if not existing or not existing.get("id"):
return KeycloakDeleteResult(action="not_found") return ProviderDeleteResult(action="not_found")
user_id = str(existing["id"]) user_id = str(existing["id"])
resp = client.delete(f"/admin/realms/{self.realm}/users/{user_id}") resp = client.delete(f"/admin/realms/{self.realm}/users/{user_id}")
if resp.status_code in {204, 404}: if resp.status_code in {204, 404}:
return KeycloakDeleteResult(action="deleted" if resp.status_code == 204 else "not_found", user_id=user_id) return ProviderDeleteResult(action="deleted" if resp.status_code == 204 else "not_found", user_id=user_id)
if resp.status_code >= 400: if resp.status_code >= 400:
raise HTTPException(status_code=502, detail="idp_delete_failed") raise HTTPException(status_code=502, detail="idp_delete_failed")
return KeycloakDeleteResult(action="deleted", user_id=user_id) return ProviderDeleteResult(action="deleted", user_id=user_id)
def list_groups_tree(self) -> list[dict]: def list_groups_tree(self) -> list[dict]:
with self._client() as client: with self._client() as client:

View File

@@ -17,7 +17,7 @@ from app.repositories.roles_repo import RolesRepository
from app.repositories.sites_repo import SitesRepository from app.repositories.sites_repo import SitesRepository
from app.repositories.systems_repo import SystemsRepository from app.repositories.systems_repo import SystemsRepository
from app.repositories.users_repo import UsersRepository from app.repositories.users_repo import UsersRepository
from app.services.idp_admin_service import KeycloakAdminService from app.services.idp_admin_service import ProviderAdminService
BUILTIN_CLIENT_IDS = { BUILTIN_CLIENT_IDS = {
"account", "account",
@@ -80,7 +80,7 @@ def _flatten_groups(nodes: list[dict], inherited_company_key: str | None = None)
"company_key": company_key, "company_key": company_key,
"display_name": _first_attr(attrs, "display_name") or name or company_key, "display_name": _first_attr(attrs, "display_name") or name or company_key,
"status": _first_attr(attrs, "status") or "active", "status": _first_attr(attrs, "status") or "active",
"idp_group_id": group_id, "provider_group_id": group_id,
} }
site_key = _first_attr(attrs, "site_key") site_key = _first_attr(attrs, "site_key")
@@ -95,7 +95,7 @@ def _flatten_groups(nodes: list[dict], inherited_company_key: str | None = None)
"display_name": _first_attr(attrs, "display_name") or name or site_key, "display_name": _first_attr(attrs, "display_name") or name or site_key,
"domain": _first_attr(attrs, "domain"), "domain": _first_attr(attrs, "domain"),
"status": _first_attr(attrs, "status") or "active", "status": _first_attr(attrs, "status") or "active",
"idp_group_id": group_id, "provider_group_id": group_id,
} }
child_companies, child_sites = _flatten_groups(children, current_company_key) child_companies, child_sites = _flatten_groups(children, current_company_key)
@@ -105,7 +105,7 @@ def _flatten_groups(nodes: list[dict], inherited_company_key: str | None = None)
return companies, sites return companies, sites
def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: def sync_from_provider(db: Session, *, force: bool = False) -> dict[str, int]:
global _last_synced_at global _last_synced_at
now = time.time() now = time.time()
if not force and now - _last_synced_at < _min_sync_interval_sec: if not force and now - _last_synced_at < _min_sync_interval_sec:
@@ -119,7 +119,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
if not force and now - _last_synced_at < _min_sync_interval_sec: if not force and now - _last_synced_at < _min_sync_interval_sec:
return {"synced": 0} return {"synced": 0}
idp = KeycloakAdminService(get_settings()) idp = ProviderAdminService(get_settings())
companies_repo = CompaniesRepository(db) companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db) sites_repo = SitesRepository(db)
systems_repo = SystemsRepository(db) systems_repo = SystemsRepository(db)
@@ -147,7 +147,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
company_key=company_key, company_key=company_key,
display_name=row["display_name"], display_name=row["display_name"],
legal_name=None, legal_name=None,
idp_group_id=row["idp_group_id"], provider_group_id=row["provider_group_id"],
status=row["status"], status=row["status"],
) )
companies_created += 1 companies_created += 1
@@ -155,7 +155,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
company = companies_repo.update( company = companies_repo.update(
company, company,
display_name=row["display_name"], display_name=row["display_name"],
idp_group_id=row["idp_group_id"], provider_group_id=row["provider_group_id"],
status=row["status"], status=row["status"],
) )
companies_updated += 1 companies_updated += 1
@@ -173,7 +173,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
company_key=company_key, company_key=company_key,
display_name=company_key, display_name=company_key,
legal_name=None, legal_name=None,
idp_group_id=None, provider_group_id=None,
status="active", status="active",
) )
companies_created += 1 companies_created += 1
@@ -187,7 +187,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
company_id=company_id, company_id=company_id,
display_name=row["display_name"], display_name=row["display_name"],
domain=row["domain"], domain=row["domain"],
idp_group_id=row["idp_group_id"], provider_group_id=row["provider_group_id"],
status=row["status"], status=row["status"],
) )
sites_created += 1 sites_created += 1
@@ -197,7 +197,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
company_id=company_id, company_id=company_id,
display_name=row["display_name"], display_name=row["display_name"],
domain=row["domain"], domain=row["domain"],
idp_group_id=row["idp_group_id"], provider_group_id=row["provider_group_id"],
status=row["status"], status=row["status"],
) )
sites_updated += 1 sites_updated += 1
@@ -212,7 +212,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
if client_id in BUILTIN_CLIENT_IDS: if client_id in BUILTIN_CLIENT_IDS:
continue continue
system = db.scalar(select(System).where(System.idp_client_id == client_id)) system = db.scalar(select(System).where(System.provider_client_id == client_id))
system_name = str(client.get("name", "")).strip() or client_id system_name = str(client.get("name", "")).strip() or client_id
system_status = "active" if client.get("enabled", True) else "inactive" system_status = "active" if client.get("enabled", True) else "inactive"
if system is None: if system is None:
@@ -220,7 +220,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
system = systems_repo.create( system = systems_repo.create(
system_key=system_key, system_key=system_key,
name=system_name, name=system_name,
idp_client_id=client_id, provider_client_id=client_id,
status=system_status, status=system_status,
) )
systems_created += 1 systems_created += 1
@@ -245,7 +245,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
role = db.scalar( role = db.scalar(
select(Role).where( select(Role).where(
Role.system_id == system.id, Role.system_id == system.id,
Role.idp_role_name == role_name, Role.provider_role_name == role_name,
) )
) )
if role is None: if role is None:
@@ -255,7 +255,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
system_id=system.id, system_id=system.id,
name=role_name, name=role_name,
description=role_desc, description=role_desc,
idp_role_name=role_name, provider_role_name=role_name,
status=role_status, status=role_status,
) )
roles_created += 1 roles_created += 1
@@ -280,7 +280,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]:
) )
users_repo.upsert_by_sub( users_repo.upsert_by_sub(
user_sub=user_id, user_sub=user_id,
idp_user_id=user_id, provider_user_id=user_id,
username=str(user.get("username", "")).strip() or None, username=str(user.get("username", "")).strip() or None,
email=str(user.get("email", "")).strip() or None, email=str(user.get("email", "")).strip() or None,
display_name=display_name, display_name=display_name,

View File

@@ -16,7 +16,7 @@ class PermissionService:
system_name=system_name, system_name=system_name,
role_key=role_key, role_key=role_key,
role_name=role_name, role_name=role_name,
idp_role_name=idp_role_name, provider_role_name=provider_role_name,
) )
for ( for (
site_key, site_key,
@@ -27,7 +27,7 @@ class PermissionService:
system_name, system_name,
role_key, role_key,
role_name, role_name,
idp_role_name, provider_role_name,
) in rows ) in rows
], ],
) )

View File

@@ -24,7 +24,7 @@ DROP TABLE IF EXISTS permission_groups CASCADE;
CREATE TABLE users ( CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_sub TEXT NOT NULL UNIQUE, user_sub TEXT NOT NULL UNIQUE,
idp_user_id VARCHAR(128) UNIQUE, provider_user_id VARCHAR(128) UNIQUE,
username TEXT UNIQUE, username TEXT UNIQUE,
email TEXT UNIQUE, email TEXT UNIQUE,
display_name TEXT, display_name TEXT,
@@ -39,7 +39,7 @@ CREATE TABLE companies (
company_key TEXT NOT NULL UNIQUE, company_key TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL, display_name TEXT NOT NULL,
legal_name TEXT, legal_name TEXT,
idp_group_id TEXT, provider_group_id TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
@@ -51,7 +51,7 @@ CREATE TABLE sites (
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
display_name TEXT NOT NULL, display_name TEXT NOT NULL,
domain TEXT, domain TEXT,
idp_group_id TEXT, provider_group_id TEXT,
status VARCHAR(16) NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
@@ -61,7 +61,7 @@ CREATE TABLE systems (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
system_key TEXT NOT NULL UNIQUE, system_key TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
idp_client_id TEXT NOT NULL UNIQUE, provider_client_id TEXT NOT NULL UNIQUE,
status VARCHAR(16) NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
@@ -73,11 +73,11 @@ CREATE TABLE roles (
system_id UUID NOT NULL REFERENCES systems(id) ON DELETE CASCADE, system_id UUID NOT NULL REFERENCES systems(id) ON DELETE CASCADE,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
idp_role_name TEXT NOT NULL, provider_role_name TEXT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'active', status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_roles_system_idp_role_name UNIQUE (system_id, idp_role_name) CONSTRAINT uq_roles_system_provider_role_name UNIQUE (system_id, provider_role_name)
); );
CREATE TABLE site_roles ( CREATE TABLE site_roles (

View File

@@ -1,11 +1,11 @@
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from app.main import app from app.main import app
from app.security.idp_jwt import KeycloakTokenVerifier from app.security.idp_jwt import ProviderTokenVerifier
def test_infer_jwks_url() -> None: def test_infer_jwks_url() -> None:
assert KeycloakTokenVerifier._infer_jwks_url("https://auth.ose.tw/application/o/member/") == ( assert ProviderTokenVerifier._infer_jwks_url("https://auth.ose.tw/application/o/member/") == (
"https://auth.ose.tw/application/o/member/protocol/openid-connect/certs" "https://auth.ose.tw/application/o/member/protocol/openid-connect/certs"
) )