diff --git a/backend/app/api/admin_catalog.py b/backend/app/api/admin_catalog.py index 1494343..d8fa2e6 100644 --- a/backend/app/api/admin_catalog.py +++ b/backend/app/api/admin_catalog.py @@ -51,8 +51,8 @@ from app.schemas.catalog import ( ) from app.security.admin_guard import require_admin_principal from app.security.api_client_auth import hash_api_key -from app.services.idp_admin_service import KeycloakAdminService -from app.services.idp_catalog_sync import sync_from_keycloak +from app.services.idp_admin_service import ProviderAdminService +from app.services.idp_catalog_sync import sync_from_provider from app.core.config import get_settings router = APIRouter( @@ -76,7 +76,7 @@ def _company_item(company) -> CompanyItem: company_key=company.company_key, display_name=company.display_name, legal_name=company.legal_name, - idp_group_id=company.idp_group_id, + provider_group_id=company.provider_group_id, status=company.status, ) @@ -89,7 +89,7 @@ def _site_item(site, company) -> SiteItem: company_display_name=company.display_name, display_name=site.display_name, domain=site.domain, - idp_group_id=site.idp_group_id, + provider_group_id=site.provider_group_id, status=site.status, ) @@ -99,7 +99,7 @@ def _system_item(system) -> SystemItem: id=system.id, system_key=system.system_key, name=system.name, - idp_client_id=system.idp_client_id, + provider_client_id=system.provider_client_id, status=system.status, ) @@ -108,7 +108,7 @@ def _member_item(user) -> MemberItem: return MemberItem( id=user.id, user_sub=user.user_sub, - idp_user_id=user.idp_user_id, + provider_user_id=user.provider_user_id, username=user.username, email=user.email, display_name=user.display_name, @@ -138,7 +138,7 @@ def list_companies( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_keycloak(db) + sync_from_provider(db) repo = CompaniesRepository(db) 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) @@ -147,7 +147,7 @@ def list_companies( @router.post("/companies", response_model=CompanyItem) def create_company(payload: CompanyCreateRequest, db: Session = Depends(get_db)) -> CompanyItem: 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) group_name = _company_group_name(payload.display_name, company_key) group = idp.ensure_group( @@ -163,7 +163,7 @@ def create_company(payload: CompanyCreateRequest, db: Session = Depends(get_db)) company_key=company_key, display_name=payload.display_name, legal_name=payload.legal_name, - idp_group_id=group.group_id, + provider_group_id=group.group_id, status=payload.status, ) 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) if not item: 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_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 = idp.ensure_group( group_id=resolved_group_id, @@ -194,7 +194,7 @@ def update_company(company_key: str, payload: CompanyUpdateRequest, db: Session item, display_name=payload.display_name, legal_name=payload.legal_name, - idp_group_id=group.group_id, + provider_group_id=group.group_id, status=payload.status, ) return _company_item(item) @@ -203,11 +203,11 @@ def update_company(company_key: str, payload: CompanyUpdateRequest, db: Session @router.delete("/companies/{company_key}") def delete_company(company_key: str, db: Session = Depends(get_db)) -> dict[str, str]: repo = CompaniesRepository(db) - idp = KeycloakAdminService(get_settings()) + idp = ProviderAdminService(get_settings()) item = repo.get_by_key(company_key) if not item: 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) return {"deleted": company_key} @@ -231,7 +231,7 @@ def list_sites( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_keycloak(db) + sync_from_provider(db) companies_repo = CompaniesRepository(db) sites_repo = SitesRepository(db) company_id = None @@ -252,7 +252,7 @@ def list_sites( def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> SiteItem: companies_repo = CompaniesRepository(db) sites_repo = SitesRepository(db) - idp = KeycloakAdminService(get_settings()) + idp = ProviderAdminService(get_settings()) company = companies_repo.get_by_key(payload.company_key) if not company: 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_id=None, name=group_name, - parent_group_id=company.idp_group_id, + parent_group_id=company.provider_group_id, attributes={ "member_entity_type": "site", "site_key": site_key, @@ -277,7 +277,7 @@ def create_site(payload: SiteCreateRequest, db: Session = Depends(get_db)) -> Si company_id=company.id, display_name=payload.display_name, domain=payload.domain, - idp_group_id=group.group_id, + provider_group_id=group.group_id, status=payload.status, ) 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: companies_repo = CompaniesRepository(db) sites_repo = SitesRepository(db) - idp = KeycloakAdminService(get_settings()) + idp = ProviderAdminService(get_settings()) item = sites_repo.get_by_key(site_key) 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_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_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 = idp.ensure_group( group_id=resolved_group_id, name=group_name, - parent_group_id=target_company.idp_group_id, + parent_group_id=target_company.provider_group_id, attributes={ "member_entity_type": "site", "site_key": site_key, @@ -327,7 +327,7 @@ def update_site(site_key: str, payload: SiteUpdateRequest, db: Session = Depends company_id=company_id, display_name=payload.display_name, domain=payload.domain, - idp_group_id=group.group_id, + provider_group_id=group.group_id, status=payload.status, ) 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}") def delete_site(site_key: str, db: Session = Depends(get_db)) -> dict[str, str]: repo = SitesRepository(db) - idp = KeycloakAdminService(get_settings()) + idp = ProviderAdminService(get_settings()) item = repo.get_by_key(site_key) if not item: 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) return {"deleted": site_key} @@ -356,7 +356,7 @@ def list_systems( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_keycloak(db) + sync_from_provider(db) repo = SystemsRepository(db) 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) @@ -364,17 +364,17 @@ def list_systems( @router.post("/systems", response_model=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) 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}") 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) @@ -386,7 +386,7 @@ def list_roles( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_keycloak(db) + sync_from_provider(db) systems_repo = SystemsRepository(db) roles_repo = RolesRepository(db) @@ -410,7 +410,7 @@ def list_roles( system_key=system_map[row.system_id].system_key, system_name=system_map[row.system_id].name, name=row.name, - idp_role_name=row.idp_role_name, + provider_role_name=row.provider_role_name, description=row.description, status=row.status, ) @@ -436,7 +436,7 @@ def create_role(payload: RoleCreateRequest, db: Session = Depends(get_db)) -> Ro system_id=system.id, name=payload.name, description=payload.description, - idp_role_name=payload.idp_role_name, + provider_role_name=payload.provider_role_name, status=payload.status, ) except IntegrityError: @@ -449,7 +449,7 @@ def create_role(payload: RoleCreateRequest, db: Session = Depends(get_db)) -> Ro system_key=system.system_key, system_name=system.name, name=row.name, - idp_role_name=row.idp_role_name, + provider_role_name=row.provider_role_name, description=row.description, status=row.status, ) @@ -477,7 +477,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends system_id=system_id, name=payload.name, description=payload.description, - idp_role_name=payload.idp_role_name, + provider_role_name=payload.provider_role_name, status=payload.status, ) except IntegrityError: @@ -494,7 +494,7 @@ def update_role(role_key: str, payload: RoleUpdateRequest, db: Session = Depends system_key=system.system_key, system_name=system.name, name=role.name, - idp_role_name=role.idp_role_name, + provider_role_name=role.provider_role_name, description=role.description, 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_name=system.name, name=row.name, - idp_role_name=row.idp_role_name, + provider_role_name=row.provider_role_name, description=row.description, status=row.status, ) @@ -625,7 +625,7 @@ def list_members( limit: int = Query(default=100, ge=1, le=500), offset: int = Query(default=0, ge=0), ) -> ListResponse: - sync_from_keycloak(db) + sync_from_provider(db) repo = UsersRepository(db) 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) @@ -636,11 +636,11 @@ def create_member(payload: MemberUpsertRequest, db: Session = Depends(get_db)) - users_repo = UsersRepository(db) resolved_sub = payload.user_sub - idp_user_id: str | None = None + provider_user_id: str | None = None if payload.sync_to_idp: if not payload.email: 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( sub=payload.user_sub, email=payload.email, @@ -648,7 +648,7 @@ def create_member(payload: MemberUpsertRequest, db: Session = Depends(get_db)) - display_name=payload.display_name, 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 if not resolved_sub: @@ -661,7 +661,7 @@ def create_member(payload: MemberUpsertRequest, db: Session = Depends(get_db)) - display_name=payload.display_name, is_active=payload.is_active, status=payload.status, - idp_user_id=idp_user_id, + provider_user_id=provider_user_id, ) 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 not next_email: 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( sub=user.user_sub, email=next_email, username=next_username, display_name=next_display_name, 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( 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") if sync_to_idp: - idp = KeycloakAdminService(get_settings()) - idp.delete_user(idp_user_id=user.idp_user_id, email=user.email, username=user.username) + idp = ProviderAdminService(get_settings()) + idp.delete_user(provider_user_id=user.provider_user_id, email=user.email, username=user.username) users_repo.delete(user) return {"deleted": user_sub} @@ -725,10 +725,10 @@ def reset_member_password(user_sub: str, db: Session = Depends(get_db)) -> Membe if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") - idp = KeycloakAdminService(get_settings()) - result = idp.reset_password(idp_user_id=user.idp_user_id, email=user.email, username=user.username) - if user.idp_user_id != result.user_id: - user.idp_user_id = result.user_id + idp = ProviderAdminService(get_settings()) + result = idp.reset_password(provider_user_id=user.provider_user_id, email=user.email, username=user.username) + if user.provider_user_id != result.user_id: + user.provider_user_id = result.user_id db.commit() 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, role_key=role.role_key, 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 ] @@ -836,25 +836,27 @@ def list_api_clients( ) -@router.post("/sync/from-keycloak") -def sync_catalog_from_keycloak(db: Session = Depends(get_db), force: bool = Query(default=True)) -> dict[str, int]: - return sync_from_keycloak(db, force=force) +@router.post("/sync/from-provider") +@router.post("/sync/from-keycloak", include_in_schema=False) +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") -def sync_keycloak_group_names(db: Session = Depends(get_db)) -> dict[str, int]: +@router.post("/sync/provider-group-names") +@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) sites_repo = SitesRepository(db) - idp = KeycloakAdminService(get_settings()) + idp = ProviderAdminService(get_settings()) companies, _ = companies_repo.list(limit=5000, offset=0) company_count = 0 for company in companies: - if not company.idp_group_id: + if not company.provider_group_id: continue group_name = _company_group_name(company.display_name, company.company_key) idp.ensure_group( - group_id=company.idp_group_id, + group_id=company.provider_group_id, name=group_name, attributes={ "member_entity_type": "company", @@ -869,16 +871,16 @@ def sync_keycloak_group_names(db: Session = Depends(get_db)) -> dict[str, int]: site_count = 0 company_map = {company.id: company for company in companies} for site in sites: - if not site.idp_group_id: + if not site.provider_group_id: continue company = company_map.get(site.company_id) if not company: continue group_name = _site_group_name(site.display_name, site.site_key) idp.ensure_group( - group_id=site.idp_group_id, + group_id=site.provider_group_id, name=group_name, - parent_group_id=company.idp_group_id, + parent_group_id=company.provider_group_id, attributes={ "member_entity_type": "site", "site_key": site.site_key, diff --git a/backend/app/api/internal.py b/backend/app/api/internal.py index 01b733d..c53bdbd 100644 --- a/backend/app/api/internal.py +++ b/backend/app/api/internal.py @@ -5,12 +5,12 @@ from app.core.config import get_settings from app.db.session import get_db from app.repositories.users_repo import UsersRepository 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.permissions import RoleSnapshotResponse from app.schemas.users import UserUpsertBySubRequest 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 router = APIRouter(prefix="/internal", tags=["internal"], dependencies=[Depends(require_api_client)]) @@ -33,7 +33,7 @@ def upsert_user_by_sub( return InternalUpsertUserBySubResponse( id=user.id, user_sub=user.user_sub, - idp_user_id=user.idp_user_id, + provider_user_id=user.provider_user_id, username=user.username, email=user.email, 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, role.role_key, role.name, - role.idp_role_name, + role.provider_role_name, ) 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, role_key=role_key, role_name=role_name, - idp_role_name=idp_role_name, + provider_role_name=provider_role_name, ) for ( site_key, @@ -93,7 +93,7 @@ def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUser system_name, role_key, role_name, - idp_role_name, + provider_role_name, ) in rows ], ) @@ -108,14 +108,15 @@ def get_permission_snapshot( return PermissionService.build_role_snapshot(user_sub=user_sub, rows=rows) -@router.post("/idp/users/ensure", response_model=KeycloakEnsureUserResponse) -@router.post("/keycloak/users/ensure", response_model=KeycloakEnsureUserResponse) +@router.post("/provider/users/ensure", response_model=ProviderEnsureUserResponse) +@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( - payload: KeycloakEnsureUserRequest, + payload: ProviderEnsureUserRequest, db: Session = Depends(get_db), -) -> KeycloakEnsureUserResponse: +) -> ProviderEnsureUserResponse: settings = get_settings() - idp_service = KeycloakAdminService(settings=settings) + idp_service = ProviderAdminService(settings=settings) sync_result = idp_service.ensure_user( sub=payload.user_sub, email=payload.email, @@ -136,6 +137,6 @@ def ensure_idp_user( display_name=payload.display_name, is_active=payload.is_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) diff --git a/backend/app/api/internal_catalog.py b/backend/app/api/internal_catalog.py index ab7fa49..79cb2d9 100644 --- a/backend/app/api/internal_catalog.py +++ b/backend/app/api/internal_catalog.py @@ -34,7 +34,7 @@ def internal_list_systems( "id": i.id, "system_key": i.system_key, "name": i.name, - "idp_client_id": i.idp_client_id, + "provider_client_id": i.provider_client_id, "status": i.status, } for i in items @@ -72,7 +72,7 @@ def internal_list_roles( system_key=system_map[i.system_id].system_key, system_name=system_map[i.system_id].name, name=i.name, - idp_role_name=i.idp_role_name, + provider_role_name=i.provider_role_name, description=i.description, status=i.status, ) diff --git a/backend/app/api/me.py b/backend/app/api/me.py index 5739704..c0eef4c 100644 --- a/backend/app/api/me.py +++ b/backend/app/api/me.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Session from app.db.session import get_db from app.repositories.users_repo import UsersRepository 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.security.idp_jwt import require_authenticated_principal from app.services.permission_service import PermissionService @@ -15,7 +15,7 @@ router = APIRouter(prefix="/me", tags=["me"]) @router.get("", response_model=MeSummaryResponse) def get_me( - principal: KeycloakPrincipal = Depends(require_authenticated_principal), + principal: ProviderPrincipal = Depends(require_authenticated_principal), db: Session = Depends(get_db), ) -> MeSummaryResponse: try: @@ -39,7 +39,7 @@ def get_me( @router.get("/permissions/snapshot", response_model=RoleSnapshotResponse) def get_my_permission_snapshot( - principal: KeycloakPrincipal = Depends(require_authenticated_principal), + principal: ProviderPrincipal = Depends(require_authenticated_principal), db: Session = Depends(get_db), ) -> RoleSnapshotResponse: try: @@ -65,7 +65,7 @@ def get_my_permission_snapshot( system.name, role.role_key, role.name, - role.idp_role_name, + role.provider_role_name, ) for site, company, role, system in rows ] diff --git a/backend/app/models/company.py b/backend/app/models/company.py index 764fb31..2f7caa9 100644 --- a/backend/app/models/company.py +++ b/backend/app/models/company.py @@ -15,7 +15,7 @@ class Company(Base): company_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True) display_name: Mapped[str] = mapped_column(String(255), nullable=False) 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") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/models/role.py b/backend/app/models/role.py index 0bcbe4e..f8d7f46 100644 --- a/backend/app/models/role.py +++ b/backend/app/models/role.py @@ -10,14 +10,14 @@ from app.db.base import Base class Role(Base): __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())) 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) name: Mapped[str] = mapped_column(String(255), nullable=False) 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") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/models/site.py b/backend/app/models/site.py index 94f1aff..499a2d9 100644 --- a/backend/app/models/site.py +++ b/backend/app/models/site.py @@ -16,7 +16,7 @@ class Site(Base): 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) 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") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/models/system.py b/backend/app/models/system.py index 7700f5d..5da0c1f 100644 --- a/backend/app/models/system.py +++ b/backend/app/models/system.py @@ -14,7 +14,7 @@ class System(Base): 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) 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") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) updated_at: Mapped[datetime] = mapped_column( diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 1336994..6c3a7ec 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -13,7 +13,7 @@ class User(Base): 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) - 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) email: Mapped[str | None] = mapped_column(String(320), unique=True) display_name: Mapped[str | None] = mapped_column(String(255)) diff --git a/backend/app/repositories/companies_repo.py b/backend/app/repositories/companies_repo.py index 3ef2ba2..1698e3b 100644 --- a/backend/app/repositories/companies_repo.py +++ b/backend/app/repositories/companies_repo.py @@ -36,14 +36,14 @@ class CompaniesRepository: company_key: str, display_name: str, legal_name: str | None, - idp_group_id: str | None = None, + provider_group_id: str | None = None, status: str = "active", ) -> Company: item = Company( company_key=company_key, display_name=display_name, legal_name=legal_name, - idp_group_id=idp_group_id, + provider_group_id=provider_group_id, status=status, ) self.db.add(item) @@ -57,15 +57,15 @@ class CompaniesRepository: *, display_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, ) -> Company: if display_name is not None: item.display_name = display_name if legal_name is not None: item.legal_name = legal_name - if idp_group_id is not None: - item.idp_group_id = idp_group_id + if provider_group_id is not None: + item.provider_group_id = provider_group_id if status is not None: item.status = status self.db.commit() diff --git a/backend/app/repositories/roles_repo.py b/backend/app/repositories/roles_repo.py index fc2d566..c03ab84 100644 --- a/backend/app/repositories/roles_repo.py +++ b/backend/app/repositories/roles_repo.py @@ -30,7 +30,7 @@ class RolesRepository: cond = or_( Role.role_key.ilike(pattern), Role.name.ilike(pattern), - Role.idp_role_name.ilike(pattern), + Role.provider_role_name.ilike(pattern), Role.description.ilike(pattern), ) stmt = stmt.where(cond) @@ -52,7 +52,7 @@ class RolesRepository: system_id: str, name: str, description: str | None, - idp_role_name: str, + provider_role_name: str, status: str = "active", ) -> Role: item = Role( @@ -60,7 +60,7 @@ class RolesRepository: system_id=system_id, name=name, description=description, - idp_role_name=idp_role_name, + provider_role_name=provider_role_name, status=status, ) self.db.add(item) @@ -75,7 +75,7 @@ class RolesRepository: system_id: str | None = None, name: str | None = None, description: str | None = None, - idp_role_name: str | None = None, + provider_role_name: str | None = None, status: str | None = None, ) -> Role: if system_id is not None: @@ -84,8 +84,8 @@ class RolesRepository: item.name = name if description is not None: item.description = description - if idp_role_name is not None: - item.idp_role_name = idp_role_name + if provider_role_name is not None: + item.provider_role_name = provider_role_name if status is not None: item.status = status self.db.commit() diff --git a/backend/app/repositories/sites_repo.py b/backend/app/repositories/sites_repo.py index 8ed1164..3f5277e 100644 --- a/backend/app/repositories/sites_repo.py +++ b/backend/app/repositories/sites_repo.py @@ -45,7 +45,7 @@ class SitesRepository: company_id: str, display_name: str, domain: str | None, - idp_group_id: str | None = None, + provider_group_id: str | None = None, status: str = "active", ) -> Site: item = Site( @@ -53,7 +53,7 @@ class SitesRepository: company_id=company_id, display_name=display_name, domain=domain, - idp_group_id=idp_group_id, + provider_group_id=provider_group_id, status=status, ) self.db.add(item) @@ -68,7 +68,7 @@ class SitesRepository: company_id: str | None = None, display_name: str | None = None, domain: str | None = None, - idp_group_id: str | None = None, + provider_group_id: str | None = None, status: str | None = None, ) -> Site: if company_id is not None: @@ -77,8 +77,8 @@ class SitesRepository: item.display_name = display_name if domain is not None: item.domain = domain - if idp_group_id is not None: - item.idp_group_id = idp_group_id + if provider_group_id is not None: + item.provider_group_id = provider_group_id if status is not None: item.status = status self.db.commit() diff --git a/backend/app/repositories/systems_repo.py b/backend/app/repositories/systems_repo.py index c76cdaa..dced2bd 100644 --- a/backend/app/repositories/systems_repo.py +++ b/backend/app/repositories/systems_repo.py @@ -19,7 +19,7 @@ class SystemsRepository: count_stmt = select(func.count()).select_from(System) if 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) count_stmt = count_stmt.where(cond) if status: @@ -29,8 +29,8 @@ class SystemsRepository: 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) - def create(self, *, system_key: str, name: str, idp_client_id: str, status: str = "active") -> System: - item = System(system_key=system_key, name=name, idp_client_id=idp_client_id, status=status) + def create(self, *, system_key: str, name: str, provider_client_id: str, status: str = "active") -> System: + item = System(system_key=system_key, name=name, provider_client_id=provider_client_id, status=status) self.db.add(item) self.db.commit() self.db.refresh(item) @@ -41,13 +41,13 @@ class SystemsRepository: item: System, *, name: str | None = None, - idp_client_id: str | None = None, + provider_client_id: str | None = None, status: str | None = None, ) -> System: if name is not None: item.name = name - if idp_client_id is not None: - item.idp_client_id = idp_client_id + if provider_client_id is not None: + item.provider_client_id = provider_client_id if status is not None: item.status = status self.db.commit() diff --git a/backend/app/repositories/users_repo.py b/backend/app/repositories/users_repo.py index 0801055..6f346ec 100644 --- a/backend/app/repositories/users_repo.py +++ b/backend/app/repositories/users_repo.py @@ -54,13 +54,13 @@ class UsersRepository: display_name: str | None, is_active: bool, status: str = "active", - idp_user_id: str | None = None, + provider_user_id: str | None = None, ) -> User: user = self.get_by_sub(user_sub) if user is None: user = User( user_sub=user_sub, - idp_user_id=idp_user_id, + provider_user_id=provider_user_id, username=username, email=email, display_name=display_name, @@ -69,8 +69,8 @@ class UsersRepository: ) self.db.add(user) else: - if idp_user_id is not None: - user.idp_user_id = idp_user_id + if provider_user_id is not None: + user.provider_user_id = provider_user_id user.username = username user.email = email user.display_name = display_name diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index ea9e100..a3c523c 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, Field -class KeycloakPrincipal(BaseModel): +class ProviderPrincipal(BaseModel): sub: str email: str | None = None name: str | None = None diff --git a/backend/app/schemas/catalog.py b/backend/app/schemas/catalog.py index f9d8dad..9fcc91f 100644 --- a/backend/app/schemas/catalog.py +++ b/backend/app/schemas/catalog.py @@ -19,7 +19,7 @@ class CompanyCreateRequest(BaseModel): class CompanyUpdateRequest(BaseModel): display_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 @@ -28,7 +28,7 @@ class CompanyItem(BaseModel): company_key: str display_name: str legal_name: str | None = None - idp_group_id: str | None = None + provider_group_id: str | None = None status: str @@ -43,7 +43,7 @@ class SiteUpdateRequest(BaseModel): company_key: str | None = None display_name: str | None = None domain: str | None = None - idp_group_id: str | None = None + provider_group_id: str | None = None status: str | None = None @@ -54,19 +54,19 @@ class SiteItem(BaseModel): company_display_name: str display_name: str domain: str | None = None - idp_group_id: str | None = None + provider_group_id: str | None = None status: str class SystemCreateRequest(BaseModel): name: str - idp_client_id: str + provider_client_id: str status: str = "active" class SystemUpdateRequest(BaseModel): name: str | None = None - idp_client_id: str | None = None + provider_client_id: str | None = None status: str | None = None @@ -74,14 +74,14 @@ class SystemItem(BaseModel): id: str system_key: str name: str - idp_client_id: str + provider_client_id: str status: str class RoleCreateRequest(BaseModel): system_key: str name: str - idp_role_name: str + provider_role_name: str description: str | None = None status: str = "active" @@ -89,7 +89,7 @@ class RoleCreateRequest(BaseModel): class RoleUpdateRequest(BaseModel): system_key: str | None = None name: str | None = None - idp_role_name: str | None = None + provider_role_name: str | None = None description: str | None = None status: str | None = None @@ -100,7 +100,7 @@ class RoleItem(BaseModel): system_key: str system_name: str name: str - idp_role_name: str + provider_role_name: str description: str | None = None status: str @@ -108,7 +108,7 @@ class RoleItem(BaseModel): class MemberItem(BaseModel): id: str user_sub: str - idp_user_id: str | None = None + provider_user_id: str | None = None username: str | None = None email: str | None = None display_name: str | None = None @@ -173,7 +173,7 @@ class UserEffectiveRoleItem(BaseModel): system_name: str role_key: str role_name: str - idp_role_name: str + provider_role_name: str class UserEffectiveRolesResponse(BaseModel): diff --git a/backend/app/schemas/idp_admin.py b/backend/app/schemas/idp_admin.py index 9f094ed..131c894 100644 --- a/backend/app/schemas/idp_admin.py +++ b/backend/app/schemas/idp_admin.py @@ -1,7 +1,7 @@ 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")) username: str | None = None email: str @@ -9,6 +9,6 @@ class KeycloakEnsureUserRequest(BaseModel): is_active: bool = True -class KeycloakEnsureUserResponse(BaseModel): - idp_user_id: str +class ProviderEnsureUserResponse(BaseModel): + provider_user_id: str action: str diff --git a/backend/app/schemas/internal.py b/backend/app/schemas/internal.py index d244a3f..cf6c660 100644 --- a/backend/app/schemas/internal.py +++ b/backend/app/schemas/internal.py @@ -5,7 +5,7 @@ class InternalSystemItem(BaseModel): id: str system_key: str name: str - idp_client_id: str + provider_client_id: str status: str @@ -22,7 +22,7 @@ class InternalRoleItem(BaseModel): system_key: str system_name: str name: str - idp_role_name: str + provider_role_name: str description: str | None = None status: str @@ -86,7 +86,7 @@ class InternalMemberListResponse(BaseModel): class InternalUpsertUserBySubResponse(BaseModel): id: str user_sub: str - idp_user_id: str | None = None + provider_user_id: str | None = None username: str | None = None email: str | None = None display_name: str | None = None @@ -103,7 +103,7 @@ class InternalUserRoleItem(BaseModel): system_name: str role_key: str role_name: str - idp_role_name: str + provider_role_name: str class InternalUserRoleResponse(BaseModel): diff --git a/backend/app/schemas/permissions.py b/backend/app/schemas/permissions.py index 5513423..3242ccd 100644 --- a/backend/app/schemas/permissions.py +++ b/backend/app/schemas/permissions.py @@ -10,7 +10,7 @@ class RoleSnapshotItem(BaseModel): system_name: str role_key: str role_name: str - idp_role_name: str + provider_role_name: str class RoleSnapshotResponse(BaseModel): diff --git a/backend/app/security/admin_guard.py b/backend/app/security/admin_guard.py index eef569d..428995a 100644 --- a/backend/app/security/admin_guard.py +++ b/backend/app/security/admin_guard.py @@ -1,7 +1,7 @@ from fastapi import Depends, HTTPException, status 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 @@ -21,8 +21,8 @@ def _expand_group_aliases(groups: set[str]) -> set[str]: def require_admin_principal( - principal: KeycloakPrincipal = Depends(require_authenticated_principal), -) -> KeycloakPrincipal: + principal: ProviderPrincipal = Depends(require_authenticated_principal), +) -> ProviderPrincipal: settings = get_settings() required_groups = _expand_group_aliases(set(settings.admin_required_groups)) diff --git a/backend/app/security/idp_jwt.py b/backend/app/security/idp_jwt.py index 4aefe8e..ade871e 100644 --- a/backend/app/security/idp_jwt.py +++ b/backend/app/security/idp_jwt.py @@ -9,13 +9,13 @@ from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 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) logger = logging.getLogger(__name__) -class KeycloakTokenVerifier: +class ProviderTokenVerifier: def __init__( self, issuer: str | None, @@ -99,7 +99,7 @@ class KeycloakTokenVerifier: return base_url.rstrip("/") + "/realms/master/protocol/openid-connect/userinfo" 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: return principal if not self.userinfo_endpoint: @@ -132,7 +132,7 @@ class KeycloakTokenVerifier: payload_groups = data.get("groups") if isinstance(payload_groups, list): groups = [str(g) for g in payload_groups if str(g)] - enriched = KeycloakPrincipal( + enriched = ProviderPrincipal( sub=principal.sub, email=email, name=name, @@ -169,7 +169,7 @@ class KeycloakTokenVerifier: token = resp.json().get("access_token") 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: return principal if not self.base_url or not self.realm: @@ -204,7 +204,7 @@ class KeycloakTokenVerifier: groups.append(name) if not groups: return principal - return KeycloakPrincipal( + return ProviderPrincipal( sub=principal.sub, email=principal.email, name=principal.name, @@ -212,7 +212,7 @@ class KeycloakTokenVerifier: groups=groups, ) - def verify_access_token(self, token: str) -> KeycloakPrincipal: + def verify_access_token(self, token: str) -> ProviderPrincipal: try: header = jwt.get_unverified_header(token) algorithm = str(header.get("alg", "")).upper() @@ -255,7 +255,7 @@ class KeycloakTokenVerifier: if not sub: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="token_missing_sub") - principal = KeycloakPrincipal( + principal = ProviderPrincipal( sub=sub, email=claims.get("email"), name=claims.get("name"), @@ -266,9 +266,9 @@ class KeycloakTokenVerifier: @lru_cache -def _get_verifier() -> KeycloakTokenVerifier: +def _get_verifier() -> ProviderTokenVerifier: settings = get_settings() - return KeycloakTokenVerifier( + return ProviderTokenVerifier( issuer=settings.idp_issuer, jwks_url=settings.idp_jwks_url, audience=settings.idp_audience, @@ -286,7 +286,7 @@ def _get_verifier() -> KeycloakTokenVerifier: def require_authenticated_principal( credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme), -) -> KeycloakPrincipal: +) -> ProviderPrincipal: if credentials is None or credentials.scheme.lower() != "bearer": raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing_bearer_token") diff --git a/backend/app/services/idp_admin_service.py b/backend/app/services/idp_admin_service.py index 18ee26a..097e0e8 100644 --- a/backend/app/services/idp_admin_service.py +++ b/backend/app/services/idp_admin_service.py @@ -11,31 +11,31 @@ from app.core.config import Settings @dataclass -class KeycloakSyncResult: +class ProviderSyncResult: user_id: str action: str user_sub: str | None = None @dataclass -class KeycloakPasswordResetResult: +class ProviderPasswordResetResult: user_id: str temporary_password: str @dataclass -class KeycloakDeleteResult: +class ProviderDeleteResult: action: str user_id: str | None = None @dataclass -class KeycloakGroupSyncResult: +class ProviderGroupSyncResult: group_id: str action: str -class KeycloakAdminService: +class ProviderAdminService: def __init__(self, settings: Settings) -> None: self.base_url = settings.keycloak_base_url.rstrip("/") self.realm = settings.keycloak_realm @@ -188,8 +188,8 @@ class KeycloakAdminService: username: str | None, display_name: str | None, is_active: bool = True, - idp_user_id: str | None = None, - ) -> KeycloakSyncResult: + provider_user_id: str | None = None, + ) -> ProviderSyncResult: resolved_username = username or self._safe_username(sub=sub, email=email) first_name = display_name or resolved_username payload = { @@ -202,7 +202,7 @@ class KeycloakAdminService: } 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: 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) if put_resp.status_code >= 400: 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) if create_resp.status_code >= 400: @@ -224,7 +224,7 @@ class KeycloakAdminService: user_id = str(found["id"]) if found and found.get("id") else "" if not user_id: 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( self, @@ -233,7 +233,7 @@ class KeycloakAdminService: group_id: str | None = None, parent_group_id: str | None = None, attributes: dict[str, str | list[str]] | None = None, - ) -> KeycloakGroupSyncResult: + ) -> ProviderGroupSyncResult: if not name: raise HTTPException(status_code=400, detail="idp_group_name_required") 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) if put_resp.status_code >= 400: 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} if parent_group_id: @@ -266,28 +266,28 @@ class KeycloakAdminService: resolved_id = str(found.get("id")) if found and found.get("id") else "" if not resolved_id: 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: - return KeycloakDeleteResult(action="not_found") + return ProviderDeleteResult(action="not_found") with self._client() as client: resp = client.delete(f"/admin/realms/{self.realm}/groups/{group_id}") 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: raise HTTPException(status_code=502, detail="idp_group_delete_failed") - return KeycloakDeleteResult(action="deleted") + return ProviderDeleteResult(action="deleted") def reset_password( self, *, - idp_user_id: str | None, + provider_user_id: str | None, email: str | None, username: str | None, - ) -> KeycloakPasswordResetResult: + ) -> ProviderPasswordResetResult: 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: existing = self._lookup_user_by_email_or_username(client, email=email, username=username) if not existing or not existing.get("id"): @@ -301,29 +301,29 @@ class KeycloakAdminService: ) if resp.status_code >= 400: 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( self, *, - idp_user_id: str | None, + provider_user_id: str | None, email: str | None, username: str | None, - ) -> KeycloakDeleteResult: + ) -> ProviderDeleteResult: 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: existing = self._lookup_user_by_email_or_username(client, email=email, username=username) if not existing or not existing.get("id"): - return KeycloakDeleteResult(action="not_found") + return ProviderDeleteResult(action="not_found") user_id = str(existing["id"]) resp = client.delete(f"/admin/realms/{self.realm}/users/{user_id}") 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: 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]: with self._client() as client: diff --git a/backend/app/services/idp_catalog_sync.py b/backend/app/services/idp_catalog_sync.py index e460fbd..cfad473 100644 --- a/backend/app/services/idp_catalog_sync.py +++ b/backend/app/services/idp_catalog_sync.py @@ -17,7 +17,7 @@ from app.repositories.roles_repo import RolesRepository from app.repositories.sites_repo import SitesRepository from app.repositories.systems_repo import SystemsRepository 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 = { "account", @@ -80,7 +80,7 @@ def _flatten_groups(nodes: list[dict], inherited_company_key: str | None = None) "company_key": company_key, "display_name": _first_attr(attrs, "display_name") or name or company_key, "status": _first_attr(attrs, "status") or "active", - "idp_group_id": group_id, + "provider_group_id": group_id, } 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, "domain": _first_attr(attrs, "domain"), "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) @@ -105,7 +105,7 @@ def _flatten_groups(nodes: list[dict], inherited_company_key: str | None = None) 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 now = time.time() 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: return {"synced": 0} - idp = KeycloakAdminService(get_settings()) + idp = ProviderAdminService(get_settings()) companies_repo = CompaniesRepository(db) sites_repo = SitesRepository(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, display_name=row["display_name"], legal_name=None, - idp_group_id=row["idp_group_id"], + provider_group_id=row["provider_group_id"], status=row["status"], ) companies_created += 1 @@ -155,7 +155,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: company = companies_repo.update( company, display_name=row["display_name"], - idp_group_id=row["idp_group_id"], + provider_group_id=row["provider_group_id"], status=row["status"], ) companies_updated += 1 @@ -173,7 +173,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: company_key=company_key, display_name=company_key, legal_name=None, - idp_group_id=None, + provider_group_id=None, status="active", ) companies_created += 1 @@ -187,7 +187,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: company_id=company_id, display_name=row["display_name"], domain=row["domain"], - idp_group_id=row["idp_group_id"], + provider_group_id=row["provider_group_id"], status=row["status"], ) sites_created += 1 @@ -197,7 +197,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: company_id=company_id, display_name=row["display_name"], domain=row["domain"], - idp_group_id=row["idp_group_id"], + provider_group_id=row["provider_group_id"], status=row["status"], ) 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: 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_status = "active" if client.get("enabled", True) else "inactive" 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_key=system_key, name=system_name, - idp_client_id=client_id, + provider_client_id=client_id, status=system_status, ) systems_created += 1 @@ -245,7 +245,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: role = db.scalar( select(Role).where( Role.system_id == system.id, - Role.idp_role_name == role_name, + Role.provider_role_name == role_name, ) ) if role is None: @@ -255,7 +255,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: system_id=system.id, name=role_name, description=role_desc, - idp_role_name=role_name, + provider_role_name=role_name, status=role_status, ) roles_created += 1 @@ -280,7 +280,7 @@ def sync_from_keycloak(db: Session, *, force: bool = False) -> dict[str, int]: ) users_repo.upsert_by_sub( user_sub=user_id, - idp_user_id=user_id, + provider_user_id=user_id, username=str(user.get("username", "")).strip() or None, email=str(user.get("email", "")).strip() or None, display_name=display_name, diff --git a/backend/app/services/permission_service.py b/backend/app/services/permission_service.py index d31ea1b..08fbcf0 100644 --- a/backend/app/services/permission_service.py +++ b/backend/app/services/permission_service.py @@ -16,7 +16,7 @@ class PermissionService: system_name=system_name, role_key=role_key, role_name=role_name, - idp_role_name=idp_role_name, + provider_role_name=provider_role_name, ) for ( site_key, @@ -27,7 +27,7 @@ class PermissionService: system_name, role_key, role_name, - idp_role_name, + provider_role_name, ) in rows ], ) diff --git a/backend/scripts/init_schema.sql b/backend/scripts/init_schema.sql index bd2dfdb..e4e3778 100644 --- a/backend/scripts/init_schema.sql +++ b/backend/scripts/init_schema.sql @@ -24,7 +24,7 @@ DROP TABLE IF EXISTS permission_groups CASCADE; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_sub TEXT NOT NULL UNIQUE, - idp_user_id VARCHAR(128) UNIQUE, + provider_user_id VARCHAR(128) UNIQUE, username TEXT UNIQUE, email TEXT UNIQUE, display_name TEXT, @@ -39,7 +39,7 @@ CREATE TABLE companies ( company_key TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, legal_name TEXT, - idp_group_id TEXT, + provider_group_id TEXT, status VARCHAR(16) NOT NULL DEFAULT 'active', created_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, display_name TEXT NOT NULL, domain TEXT, - idp_group_id TEXT, + provider_group_id TEXT, status VARCHAR(16) NOT NULL DEFAULT 'active', created_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(), system_key TEXT NOT NULL UNIQUE, 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', created_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, name TEXT NOT NULL, description TEXT, - idp_role_name TEXT NOT NULL, + provider_role_name TEXT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active', created_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 ( diff --git a/backend/tests/test_idp_jwt.py b/backend/tests/test_idp_jwt.py index a1061f3..0092a3d 100644 --- a/backend/tests/test_idp_jwt.py +++ b/backend/tests/test_idp_jwt.py @@ -1,11 +1,11 @@ from fastapi.testclient import TestClient 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: - 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" ) diff --git a/docs/DB_SCHEMA.md b/docs/DB_SCHEMA.md index 0e8fe39..5953a1e 100644 --- a/docs/DB_SCHEMA.md +++ b/docs/DB_SCHEMA.md @@ -8,7 +8,7 @@ - `company_key` TEXT NOT NULL UNIQUE - `display_name` TEXT NOT NULL - `legal_name` TEXT -- `idp_group_id` TEXT +- `provider_group_id` TEXT - `status` VARCHAR(16) NOT NULL default `'active'` - `created_at` TIMESTAMPTZ NOT NULL default `now()` - `updated_at` TIMESTAMPTZ NOT NULL default `now()` @@ -19,7 +19,7 @@ - `company_id` UUID NOT NULL FK -> `companies(id)` ON DELETE CASCADE - `display_name` TEXT NOT NULL - `domain` TEXT -- `idp_group_id` TEXT +- `provider_group_id` TEXT - `status` VARCHAR(16) NOT NULL default `'active'` - `created_at` TIMESTAMPTZ NOT NULL default `now()` - `updated_at` TIMESTAMPTZ NOT NULL default `now()` @@ -28,7 +28,7 @@ - `id` UUID PK default `gen_random_uuid()` - `system_key` TEXT NOT NULL UNIQUE - `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'` - `created_at` TIMESTAMPTZ NOT NULL default `now()` - `updated_at` TIMESTAMPTZ NOT NULL default `now()` @@ -39,11 +39,11 @@ - `system_id` UUID NOT NULL FK -> `systems(id)` ON DELETE CASCADE - `name` TEXT NOT NULL - `description` TEXT -- `idp_role_name` TEXT NOT NULL +- `provider_role_name` TEXT NOT NULL - `status` VARCHAR(16) NOT NULL default `'active'` - `created_at` TIMESTAMPTZ NOT NULL default `now()` - `updated_at` TIMESTAMPTZ NOT NULL default `now()` -- UNIQUE(`system_id`, `idp_role_name`) +- UNIQUE(`system_id`, `provider_role_name`) ## 5) site_roles - `id` UUID PK default `gen_random_uuid()` @@ -55,7 +55,7 @@ ## 6) users - `id` UUID PK default `gen_random_uuid()` - `user_sub` TEXT NOT NULL UNIQUE -- `idp_user_id` TEXT UNIQUE +- `provider_user_id` TEXT UNIQUE - `username` TEXT UNIQUE - `email` TEXT UNIQUE - `display_name` TEXT diff --git a/docs/FRONTEND_HANDOFF.md b/docs/FRONTEND_HANDOFF.md index c6cae45..2befc8a 100644 --- a/docs/FRONTEND_HANDOFF.md +++ b/docs/FRONTEND_HANDOFF.md @@ -15,12 +15,12 @@ - 此站台包含的 `users` 3. 系統管理(唯讀 + 同步) -- 欄位:`system_key`, `name`, `idp_client_id`, `status` +- 欄位:`system_key`, `name`, `provider_client_id`, `status` - 系統詳情需顯示底下 `roles` 列表 - 建立/修改/刪除在 Keycloak 處理,member 後台提供「同步 Keycloak」按鈕 4. 角色管理(DB 關聯為主) -- 欄位:`role_key`, `system_key`, `name`, `description`, `idp_role_name`, `status` +- 欄位:`role_key`, `system_key`, `name`, `description`, `provider_role_name`, `status` - 關聯操作:指派到 Site(新增/刪除 `site_roles`) 5. 會員管理(CRUD) diff --git a/docs/INTERNAL_API_HANDOFF.md b/docs/INTERNAL_API_HANDOFF.md index 8dee0af..d142352 100644 --- a/docs/INTERNAL_API_HANDOFF.md +++ b/docs/INTERNAL_API_HANDOFF.md @@ -22,8 +22,9 @@ 6. `POST /internal/users/upsert-by-sub` 7. `GET /internal/users/{user_sub}/roles` 8. `GET /internal/permissions/{user_sub}/snapshot`(相容路徑,回 role 聚合資料) -9. `POST /internal/idp/users/ensure` -10. `POST /internal/keycloak/users/ensure` +9. `POST /internal/provider/users/ensure` +10. `POST /internal/idp/users/ensure`(舊路徑相容,不建議新串接使用) +11. `POST /internal/keycloak/users/ensure`(舊路徑相容,不建議新串接使用) ## 角色聚合回應(`GET /internal/users/{user_sub}/roles`) ```json @@ -39,7 +40,7 @@ "system_name": "Marketing", "role_key": "RL20260402X0002", "role_name": "campaign_edit", - "idp_role_name": "campaign_edit" + "provider_role_name": "campaign_edit" } ] } diff --git a/docs/LOCAL_DEV_RUNBOOK.md b/docs/LOCAL_DEV_RUNBOOK.md index a0fcf32..fe352a8 100644 --- a/docs/LOCAL_DEV_RUNBOOK.md +++ b/docs/LOCAL_DEV_RUNBOOK.md @@ -37,7 +37,7 @@ npm run dev 2. 前端按「前往 Keycloak 登入」應可成功導轉與回跳。 3. `GET /me` 登入後應有資料。 4. 非 admin 群組帳號打 `/admin/*` 應為 403。 -5. `POST /admin/sync/from-keycloak?force=true` 可手動觸發全量補齊同步。 +5. `POST /admin/sync/from-provider?force=true` 可手動觸發全量補齊同步。 ## 6) 新模型驗收路徑 1. 新增 Company、Site。 diff --git a/frontend/src/pages/LoginPage.vue b/frontend/src/pages/LoginPage.vue index 2065e4c..2971b1e 100644 --- a/frontend/src/pages/LoginPage.vue +++ b/frontend/src/pages/LoginPage.vue @@ -4,7 +4,7 @@ @@ -23,11 +23,11 @@ :loading="loginLoading" @click="handleLogin" > - 前往 Keycloak 登入 + 前往登入
-

登入會統一跳轉到 Keycloak 登入頁,完成後自動返回。

+

登入會統一跳轉到身分提供者登入頁,完成後自動返回。

登入成功後 access token 會存於本機 localStorage。

diff --git a/frontend/src/pages/admin/MembersPage.vue b/frontend/src/pages/admin/MembersPage.vue index dc66a37..bae9c4a 100644 --- a/frontend/src/pages/admin/MembersPage.vue +++ b/frontend/src/pages/admin/MembersPage.vue @@ -46,7 +46,7 @@ - +