refactor: align backend with company-site-member schema and system-level RBAC groups
This commit is contained in:
328
app/api/admin_catalog.py
Normal file
328
app/api/admin_catalog.py
Normal file
@@ -0,0 +1,328 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.models.api_client import ApiClient
|
||||
from app.repositories.companies_repo import CompaniesRepository
|
||||
from app.repositories.modules_repo import ModulesRepository
|
||||
from app.repositories.permission_groups_repo import PermissionGroupsRepository
|
||||
from app.repositories.sites_repo import SitesRepository
|
||||
from app.repositories.systems_repo import SystemsRepository
|
||||
from app.repositories.users_repo import UsersRepository
|
||||
from app.schemas.catalog import (
|
||||
CompanyCreateRequest,
|
||||
CompanyItem,
|
||||
MemberItem,
|
||||
ModuleCreateRequest,
|
||||
ModuleItem,
|
||||
PermissionGroupCreateRequest,
|
||||
PermissionGroupItem,
|
||||
SiteCreateRequest,
|
||||
SiteItem,
|
||||
SystemCreateRequest,
|
||||
SystemItem,
|
||||
)
|
||||
from app.schemas.permissions import PermissionGrantRequest, PermissionRevokeRequest
|
||||
from app.security.api_client_auth import require_api_client
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
|
||||
def _resolve_module_id(db: Session, system_key: str, module_key: str | None) -> str:
|
||||
systems_repo = SystemsRepository(db)
|
||||
modules_repo = ModulesRepository(db)
|
||||
system = systems_repo.get_by_key(system_key)
|
||||
if not system:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||
target_module_key = f"{system_key}.{module_key}" if module_key else f"{system_key}.__system__"
|
||||
module = modules_repo.get_by_key(target_module_key)
|
||||
if not module:
|
||||
module = modules_repo.create(module_key=target_module_key, name=target_module_key, status="active")
|
||||
return module.id
|
||||
|
||||
|
||||
def _resolve_scope_ids(db: Session, scope_type: str, scope_id: str) -> tuple[str | None, str | None]:
|
||||
companies_repo = CompaniesRepository(db)
|
||||
sites_repo = SitesRepository(db)
|
||||
if scope_type == "company":
|
||||
company = companies_repo.get_by_key(scope_id)
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
|
||||
return company.id, None
|
||||
if scope_type == "site":
|
||||
site = sites_repo.get_by_key(scope_id)
|
||||
if not site:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="site_not_found")
|
||||
return None, site.id
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="invalid_scope_type")
|
||||
|
||||
|
||||
@router.get("/systems")
|
||||
def list_systems(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict:
|
||||
repo = SystemsRepository(db)
|
||||
items, total = repo.list(limit=limit, offset=offset)
|
||||
return {"items": [SystemItem(id=i.id, system_key=i.system_key, name=i.name, status=i.status).model_dump() for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.post("/systems", response_model=SystemItem)
|
||||
def create_system(
|
||||
payload: SystemCreateRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> SystemItem:
|
||||
repo = SystemsRepository(db)
|
||||
if repo.get_by_key(payload.system_key):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="system_key_already_exists")
|
||||
row = repo.create(system_key=payload.system_key, name=payload.name, status=payload.status)
|
||||
return SystemItem(id=row.id, system_key=row.system_key, name=row.name, status=row.status)
|
||||
|
||||
|
||||
@router.get("/modules")
|
||||
def list_modules(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = Query(default=200, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict:
|
||||
modules_repo = ModulesRepository(db)
|
||||
items, total = modules_repo.list(limit=limit, offset=offset)
|
||||
out = []
|
||||
for i in items:
|
||||
system_key = i.module_key.split(".", 1)[0] if "." in i.module_key else None
|
||||
out.append(
|
||||
ModuleItem(
|
||||
id=i.id,
|
||||
system_key=system_key,
|
||||
module_key=i.module_key,
|
||||
name=i.name,
|
||||
status=i.status,
|
||||
).model_dump()
|
||||
)
|
||||
return {"items": out, "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.post("/modules", response_model=ModuleItem)
|
||||
def create_module(
|
||||
payload: ModuleCreateRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ModuleItem:
|
||||
systems_repo = SystemsRepository(db)
|
||||
modules_repo = ModulesRepository(db)
|
||||
system = systems_repo.get_by_key(payload.system_key)
|
||||
if not system:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="system_not_found")
|
||||
full_module_key = f"{payload.system_key}.{payload.module_key}"
|
||||
if modules_repo.get_by_key(full_module_key):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="module_key_already_exists")
|
||||
row = modules_repo.create(
|
||||
module_key=full_module_key,
|
||||
name=payload.name,
|
||||
status=payload.status,
|
||||
)
|
||||
return ModuleItem(id=row.id, system_key=payload.system_key, module_key=row.module_key, name=row.name, status=row.status)
|
||||
|
||||
|
||||
@router.get("/companies")
|
||||
def list_companies(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
keyword: str | None = Query(default=None),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict:
|
||||
repo = CompaniesRepository(db)
|
||||
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
|
||||
return {"items": [CompanyItem(id=i.id, company_key=i.company_key, name=i.name, status=i.status).model_dump() for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.post("/companies", response_model=CompanyItem)
|
||||
def create_company(
|
||||
payload: CompanyCreateRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> CompanyItem:
|
||||
repo = CompaniesRepository(db)
|
||||
if repo.get_by_key(payload.company_key):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="company_key_already_exists")
|
||||
row = repo.create(company_key=payload.company_key, name=payload.name, status=payload.status)
|
||||
return CompanyItem(id=row.id, company_key=row.company_key, name=row.name, status=row.status)
|
||||
|
||||
|
||||
@router.get("/sites")
|
||||
def list_sites(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
company_key: str | None = Query(default=None),
|
||||
keyword: str | None = Query(default=None),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict:
|
||||
companies_repo = CompaniesRepository(db)
|
||||
sites_repo = SitesRepository(db)
|
||||
company_lookup: dict[str, str] = {}
|
||||
all_companies, _ = companies_repo.list(limit=1000, offset=0)
|
||||
for c in all_companies:
|
||||
company_lookup[c.id] = c.company_key
|
||||
company_id = None
|
||||
if company_key:
|
||||
company = companies_repo.get_by_key(company_key)
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
|
||||
company_id = company.id
|
||||
items, total = sites_repo.list(keyword=keyword, company_id=company_id, limit=limit, offset=offset)
|
||||
return {
|
||||
"items": [
|
||||
SiteItem(
|
||||
id=i.id,
|
||||
site_key=i.site_key,
|
||||
company_key=company_lookup.get(i.company_id, ""),
|
||||
name=i.name,
|
||||
status=i.status,
|
||||
).model_dump()
|
||||
for i in items
|
||||
],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/sites", response_model=SiteItem)
|
||||
def create_site(
|
||||
payload: SiteCreateRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> SiteItem:
|
||||
companies_repo = CompaniesRepository(db)
|
||||
sites_repo = SitesRepository(db)
|
||||
company = companies_repo.get_by_key(payload.company_key)
|
||||
if not company:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="company_not_found")
|
||||
if sites_repo.get_by_key(payload.site_key):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="site_key_already_exists")
|
||||
row = sites_repo.create(site_key=payload.site_key, company_id=company.id, name=payload.name, status=payload.status)
|
||||
return SiteItem(id=row.id, site_key=row.site_key, company_key=payload.company_key, name=row.name, status=row.status)
|
||||
|
||||
|
||||
@router.get("/members")
|
||||
def list_members(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
keyword: str | None = Query(default=None),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict:
|
||||
users_repo = UsersRepository(db)
|
||||
items, total = users_repo.list(keyword=keyword, limit=limit, offset=offset)
|
||||
return {"items": [MemberItem(id=i.id, authentik_sub=i.authentik_sub, email=i.email, display_name=i.display_name, is_active=i.is_active).model_dump() for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.get("/permission-groups")
|
||||
def list_permission_groups(
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = Query(default=100, ge=1, le=500),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> dict:
|
||||
repo = PermissionGroupsRepository(db)
|
||||
items, total = repo.list(limit=limit, offset=offset)
|
||||
return {"items": [PermissionGroupItem(id=i.id, group_key=i.group_key, name=i.name, status=i.status).model_dump() for i in items], "total": total, "limit": limit, "offset": offset}
|
||||
|
||||
|
||||
@router.post("/permission-groups", response_model=PermissionGroupItem)
|
||||
def create_permission_group(
|
||||
payload: PermissionGroupCreateRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> PermissionGroupItem:
|
||||
repo = PermissionGroupsRepository(db)
|
||||
if repo.get_by_key(payload.group_key):
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="group_key_already_exists")
|
||||
row = repo.create(group_key=payload.group_key, name=payload.name, status=payload.status)
|
||||
return PermissionGroupItem(id=row.id, group_key=row.group_key, name=row.name, status=row.status)
|
||||
|
||||
|
||||
@router.post("/permission-groups/{group_key}/members/{authentik_sub}")
|
||||
def add_group_member(
|
||||
group_key: str,
|
||||
authentik_sub: str,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, str]:
|
||||
groups_repo = PermissionGroupsRepository(db)
|
||||
group = groups_repo.get_by_key(group_key)
|
||||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||
row = groups_repo.add_member_if_not_exists(group.id, authentik_sub)
|
||||
return {"membership_id": row.id, "result": "added"}
|
||||
|
||||
|
||||
@router.delete("/permission-groups/{group_key}/members/{authentik_sub}")
|
||||
def remove_group_member(
|
||||
group_key: str,
|
||||
authentik_sub: str,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, int | str]:
|
||||
groups_repo = PermissionGroupsRepository(db)
|
||||
group = groups_repo.get_by_key(group_key)
|
||||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||
deleted = groups_repo.remove_member(group.id, authentik_sub)
|
||||
return {"deleted": deleted, "result": "removed"}
|
||||
|
||||
|
||||
@router.post("/permission-groups/{group_key}/permissions/grant")
|
||||
def grant_group_permission(
|
||||
group_key: str,
|
||||
payload: PermissionGrantRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, str]:
|
||||
groups_repo = PermissionGroupsRepository(db)
|
||||
group = groups_repo.get_by_key(group_key)
|
||||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||
_resolve_module_id(db, payload.system, payload.module)
|
||||
_resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||
module_key = f"{payload.system}.{payload.module}" if payload.module else f"{payload.system}.__system__"
|
||||
row = groups_repo.grant_group_permission(
|
||||
group_id=group.id,
|
||||
system=payload.system,
|
||||
module=module_key,
|
||||
action=payload.action,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
)
|
||||
return {"permission_id": row.id, "result": "granted"}
|
||||
|
||||
|
||||
@router.post("/permission-groups/{group_key}/permissions/revoke")
|
||||
def revoke_group_permission(
|
||||
group_key: str,
|
||||
payload: PermissionRevokeRequest,
|
||||
_: ApiClient = Depends(require_api_client),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, int | str]:
|
||||
groups_repo = PermissionGroupsRepository(db)
|
||||
group = groups_repo.get_by_key(group_key)
|
||||
if not group:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="group_not_found")
|
||||
_resolve_module_id(db, payload.system, payload.module)
|
||||
_resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||
module_key = f"{payload.system}.{payload.module}" if payload.module else f"{payload.system}.__system__"
|
||||
deleted = groups_repo.revoke_group_permission(
|
||||
group_id=group.id,
|
||||
system=payload.system,
|
||||
module=module_key,
|
||||
action=payload.action,
|
||||
scope_type=payload.scope_type,
|
||||
scope_id=payload.scope_id,
|
||||
)
|
||||
return {"deleted": deleted, "result": "revoked"}
|
||||
Reference in New Issue
Block a user