refactor: rename idp fields to provider naming
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user