refactor: align backend with company-site-member schema and system-level RBAC groups
This commit is contained in:
22
README.md
22
README.md
@@ -43,7 +43,8 @@ python scripts/generate_api_key_hash.py 'YOUR_PLAIN_KEY'
|
|||||||
## Main APIs
|
## Main APIs
|
||||||
|
|
||||||
- `GET /healthz`
|
- `GET /healthz`
|
||||||
- `POST /auth/login`
|
- `GET /auth/oidc/url`
|
||||||
|
- `POST /auth/oidc/exchange`
|
||||||
- `GET /me` (Bearer token required)
|
- `GET /me` (Bearer token required)
|
||||||
- `GET /me/permissions/snapshot` (Bearer token required)
|
- `GET /me/permissions/snapshot` (Bearer token required)
|
||||||
- `POST /internal/users/upsert-by-sub`
|
- `POST /internal/users/upsert-by-sub`
|
||||||
@@ -51,11 +52,16 @@ python scripts/generate_api_key_hash.py 'YOUR_PLAIN_KEY'
|
|||||||
- `POST /internal/authentik/users/ensure`
|
- `POST /internal/authentik/users/ensure`
|
||||||
- `POST /admin/permissions/grant`
|
- `POST /admin/permissions/grant`
|
||||||
- `POST /admin/permissions/revoke`
|
- `POST /admin/permissions/revoke`
|
||||||
- `GET|POST|PATCH /admin/organizations...`
|
- `GET|POST /admin/systems`
|
||||||
- `GET|POST|PATCH /admin/members...`
|
- `GET|POST /admin/modules`
|
||||||
- `GET|POST|DELETE /admin/members/{member_id}/organizations...`
|
- `GET|POST /admin/companies`
|
||||||
|
- `GET|POST /admin/sites`
|
||||||
|
- `GET /admin/members`
|
||||||
|
- `GET|POST /admin/permission-groups`
|
||||||
|
- `POST|DELETE /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||||
|
- `POST /admin/permission-groups/{group_key}/permissions/grant|revoke`
|
||||||
|
- `GET /internal/systems`
|
||||||
|
- `GET /internal/modules`
|
||||||
|
- `GET /internal/companies`
|
||||||
|
- `GET /internal/sites`
|
||||||
- `GET /internal/members`
|
- `GET /internal/members`
|
||||||
- `GET /internal/members/by-sub/{authentik_sub}`
|
|
||||||
- `GET /internal/organizations`
|
|
||||||
- `GET /internal/organizations/by-code/{org_code}`
|
|
||||||
- `GET /internal/members/{member_id}/organizations`
|
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.api_client import ApiClient
|
from app.models.api_client import ApiClient
|
||||||
|
from app.repositories.companies_repo import CompaniesRepository
|
||||||
|
from app.repositories.modules_repo import ModulesRepository
|
||||||
from app.repositories.permissions_repo import PermissionsRepository
|
from app.repositories.permissions_repo import PermissionsRepository
|
||||||
|
from app.repositories.sites_repo import SitesRepository
|
||||||
|
from app.repositories.systems_repo import SystemsRepository
|
||||||
from app.repositories.users_repo import UsersRepository
|
from app.repositories.users_repo import UsersRepository
|
||||||
from app.schemas.permissions import PermissionGrantRequest, PermissionRevokeRequest
|
from app.schemas.permissions import PermissionGrantRequest, PermissionRevokeRequest
|
||||||
from app.security.api_client_auth import require_api_client
|
from app.security.api_client_auth import require_api_client
|
||||||
@@ -11,6 +15,36 @@ from app.security.api_client_auth import require_api_client
|
|||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
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.post("/permissions/grant")
|
@router.post("/permissions/grant")
|
||||||
def grant_permission(
|
def grant_permission(
|
||||||
payload: PermissionGrantRequest,
|
payload: PermissionGrantRequest,
|
||||||
@@ -26,12 +60,15 @@ def grant_permission(
|
|||||||
display_name=payload.display_name,
|
display_name=payload.display_name,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
module_id = _resolve_module_id(db, payload.system, payload.module)
|
||||||
|
company_id, site_id = _resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||||
permission = perms_repo.create_if_not_exists(
|
permission = perms_repo.create_if_not_exists(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
scope_type=payload.scope_type,
|
module_id=module_id,
|
||||||
scope_id=payload.scope_id,
|
|
||||||
module=payload.module,
|
|
||||||
action=payload.action,
|
action=payload.action,
|
||||||
|
scope_type=payload.scope_type,
|
||||||
|
company_id=company_id,
|
||||||
|
site_id=site_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"permission_id": permission.id, "result": "granted"}
|
return {"permission_id": permission.id, "result": "granted"}
|
||||||
@@ -50,11 +87,14 @@ def revoke_permission(
|
|||||||
if user is None:
|
if user is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user_not_found")
|
||||||
|
|
||||||
|
module_id = _resolve_module_id(db, payload.system, payload.module)
|
||||||
|
company_id, site_id = _resolve_scope_ids(db, payload.scope_type, payload.scope_id)
|
||||||
deleted = perms_repo.revoke(
|
deleted = perms_repo.revoke(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
scope_type=payload.scope_type,
|
module_id=module_id,
|
||||||
scope_id=payload.scope_id,
|
|
||||||
module=payload.module,
|
|
||||||
action=payload.action,
|
action=payload.action,
|
||||||
|
scope_type=payload.scope_type,
|
||||||
|
company_id=company_id,
|
||||||
|
site_id=site_id,
|
||||||
)
|
)
|
||||||
return {"deleted": deleted, "result": "revoked"}
|
return {"deleted": deleted, "result": "revoked"}
|
||||||
|
|||||||
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"}
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
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.member_organizations_repo import MemberOrganizationsRepository
|
|
||||||
from app.repositories.organizations_repo import OrganizationsRepository
|
|
||||||
from app.repositories.users_repo import UsersRepository
|
|
||||||
from app.schemas.members import (
|
|
||||||
MemberCreateRequest,
|
|
||||||
MemberListResponse,
|
|
||||||
MemberOrganizationsResponse,
|
|
||||||
MemberSummary,
|
|
||||||
MemberUpdateRequest,
|
|
||||||
)
|
|
||||||
from app.schemas.organizations import (
|
|
||||||
OrganizationCreateRequest,
|
|
||||||
OrganizationListResponse,
|
|
||||||
OrganizationSummary,
|
|
||||||
OrganizationUpdateRequest,
|
|
||||||
)
|
|
||||||
from app.security.api_client_auth import require_api_client
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
|
||||||
|
|
||||||
|
|
||||||
def _to_member_summary(member) -> MemberSummary:
|
|
||||||
return MemberSummary(
|
|
||||||
id=member.id,
|
|
||||||
authentik_sub=member.authentik_sub,
|
|
||||||
authentik_user_id=member.authentik_user_id,
|
|
||||||
email=member.email,
|
|
||||||
display_name=member.display_name,
|
|
||||||
is_active=member.is_active,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _to_org_summary(org) -> OrganizationSummary:
|
|
||||||
return OrganizationSummary(
|
|
||||||
id=org.id,
|
|
||||||
org_code=org.org_code,
|
|
||||||
name=org.name,
|
|
||||||
tax_id=org.tax_id,
|
|
||||||
status=org.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/organizations", response_model=OrganizationListResponse)
|
|
||||||
def list_organizations(
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
keyword: str | None = Query(default=None),
|
|
||||||
status_filter: str | None = Query(default=None, alias="status"),
|
|
||||||
limit: int = Query(default=50, ge=1, le=200),
|
|
||||||
offset: int = Query(default=0, ge=0),
|
|
||||||
) -> OrganizationListResponse:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
|
|
||||||
return OrganizationListResponse(items=[_to_org_summary(i) for i in items], total=total, limit=limit, offset=offset)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/organizations", response_model=OrganizationSummary)
|
|
||||||
def create_organization(
|
|
||||||
payload: OrganizationCreateRequest,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> OrganizationSummary:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
existing = repo.get_by_code(payload.org_code)
|
|
||||||
if existing:
|
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="organization_code_already_exists")
|
|
||||||
org = repo.create(org_code=payload.org_code, name=payload.name, tax_id=payload.tax_id, status=payload.status)
|
|
||||||
return _to_org_summary(org)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/organizations/{org_id}", response_model=OrganizationSummary)
|
|
||||||
def get_organization(
|
|
||||||
org_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> OrganizationSummary:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
org = repo.get_by_id(org_id)
|
|
||||||
if not org:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found")
|
|
||||||
return _to_org_summary(org)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/organizations/{org_id}", response_model=OrganizationSummary)
|
|
||||||
def update_organization(
|
|
||||||
org_id: str,
|
|
||||||
payload: OrganizationUpdateRequest,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> OrganizationSummary:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
org = repo.get_by_id(org_id)
|
|
||||||
if not org:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found")
|
|
||||||
updated = repo.update(org, name=payload.name, tax_id=payload.tax_id, status=payload.status)
|
|
||||||
return _to_org_summary(updated)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/organizations/{org_id}/activate", response_model=OrganizationSummary)
|
|
||||||
def activate_organization(
|
|
||||||
org_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> OrganizationSummary:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
org = repo.get_by_id(org_id)
|
|
||||||
if not org:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found")
|
|
||||||
return _to_org_summary(repo.update(org, status="active"))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/organizations/{org_id}/deactivate", response_model=OrganizationSummary)
|
|
||||||
def deactivate_organization(
|
|
||||||
org_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> OrganizationSummary:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
org = repo.get_by_id(org_id)
|
|
||||||
if not org:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found")
|
|
||||||
return _to_org_summary(repo.update(org, status="inactive"))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/members", response_model=MemberListResponse)
|
|
||||||
def list_members(
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
keyword: str | None = Query(default=None),
|
|
||||||
is_active: bool | None = Query(default=None),
|
|
||||||
limit: int = Query(default=50, ge=1, le=200),
|
|
||||||
offset: int = Query(default=0, ge=0),
|
|
||||||
) -> MemberListResponse:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
items, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset)
|
|
||||||
return MemberListResponse(items=[_to_member_summary(i) for i in items], total=total, limit=limit, offset=offset)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/members", response_model=MemberSummary)
|
|
||||||
def create_member(
|
|
||||||
payload: MemberCreateRequest,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberSummary:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
existing = repo.get_by_sub(payload.authentik_sub)
|
|
||||||
if existing:
|
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="member_sub_already_exists")
|
|
||||||
member = repo.upsert_by_sub(
|
|
||||||
authentik_sub=payload.authentik_sub,
|
|
||||||
email=payload.email,
|
|
||||||
display_name=payload.display_name,
|
|
||||||
is_active=payload.is_active,
|
|
||||||
)
|
|
||||||
return _to_member_summary(member)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/members/{member_id}", response_model=MemberSummary)
|
|
||||||
def get_member(
|
|
||||||
member_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberSummary:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
member = repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
|
|
||||||
return _to_member_summary(member)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/members/{member_id}", response_model=MemberSummary)
|
|
||||||
def update_member(
|
|
||||||
member_id: str,
|
|
||||||
payload: MemberUpdateRequest,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberSummary:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
member = repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
|
|
||||||
updated = repo.update_member(member, email=payload.email, display_name=payload.display_name, is_active=payload.is_active)
|
|
||||||
return _to_member_summary(updated)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/members/{member_id}/activate", response_model=MemberSummary)
|
|
||||||
def activate_member(
|
|
||||||
member_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberSummary:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
member = repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
|
|
||||||
return _to_member_summary(repo.update_member(member, is_active=True))
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/members/{member_id}/deactivate", response_model=MemberSummary)
|
|
||||||
def deactivate_member(
|
|
||||||
member_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberSummary:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
member = repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
|
|
||||||
return _to_member_summary(repo.update_member(member, is_active=False))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/members/{member_id}/organizations", response_model=MemberOrganizationsResponse)
|
|
||||||
def list_member_organizations(
|
|
||||||
member_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberOrganizationsResponse:
|
|
||||||
users_repo = UsersRepository(db)
|
|
||||||
link_repo = MemberOrganizationsRepository(db)
|
|
||||||
|
|
||||||
member = users_repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
|
|
||||||
|
|
||||||
orgs = link_repo.list_organizations_by_member_id(member_id)
|
|
||||||
return MemberOrganizationsResponse(member=_to_member_summary(member), organizations=[_to_org_summary(o) for o in orgs])
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/members/{member_id}/organizations/{org_id}")
|
|
||||||
def bind_member_organization(
|
|
||||||
member_id: str,
|
|
||||||
org_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> dict[str, str]:
|
|
||||||
users_repo = UsersRepository(db)
|
|
||||||
orgs_repo = OrganizationsRepository(db)
|
|
||||||
link_repo = MemberOrganizationsRepository(db)
|
|
||||||
|
|
||||||
member = users_repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found")
|
|
||||||
org = orgs_repo.get_by_id(org_id)
|
|
||||||
if not org:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found")
|
|
||||||
|
|
||||||
relation = link_repo.add_if_not_exists(member_id=member_id, organization_id=org_id)
|
|
||||||
return {"relation_id": relation.id, "result": "bound"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/members/{member_id}/organizations/{org_id}")
|
|
||||||
def unbind_member_organization(
|
|
||||||
member_id: str,
|
|
||||||
org_id: str,
|
|
||||||
_: ApiClient = Depends(require_api_client),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> dict[str, int | str]:
|
|
||||||
link_repo = MemberOrganizationsRepository(db)
|
|
||||||
deleted = link_repo.remove(member_id=member_id, organization_id=org_id)
|
|
||||||
return {"deleted": deleted, "result": "unbound"}
|
|
||||||
@@ -58,9 +58,8 @@ def get_permission_snapshot(
|
|||||||
if user is None:
|
if user is None:
|
||||||
return PermissionSnapshotResponse(authentik_sub=authentik_sub, permissions=[])
|
return PermissionSnapshotResponse(authentik_sub=authentik_sub, permissions=[])
|
||||||
|
|
||||||
permissions = perms_repo.list_by_user_id(user.id)
|
permissions = perms_repo.list_by_user(user.id, user.authentik_sub)
|
||||||
tuples = [(p.scope_type, p.scope_id, p.module, p.action) for p in permissions]
|
return PermissionService.build_snapshot(authentik_sub=authentik_sub, permissions=permissions)
|
||||||
return PermissionService.build_snapshot(authentik_sub=authentik_sub, permissions=tuples)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/authentik/users/ensure", response_model=AuthentikEnsureUserResponse)
|
@router.post("/authentik/users/ensure", response_model=AuthentikEnsureUserResponse)
|
||||||
|
|||||||
97
app/api/internal_catalog.py
Normal file
97
app/api/internal_catalog.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.api.internal import verify_internal_secret
|
||||||
|
from app.db.session import get_db
|
||||||
|
from app.repositories.companies_repo import CompaniesRepository
|
||||||
|
from app.repositories.modules_repo import ModulesRepository
|
||||||
|
from app.repositories.sites_repo import SitesRepository
|
||||||
|
from app.repositories.systems_repo import SystemsRepository
|
||||||
|
from app.repositories.users_repo import UsersRepository
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/internal", tags=["internal"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/systems")
|
||||||
|
def internal_list_systems(
|
||||||
|
_: None = Depends(verify_internal_secret),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
limit: int = Query(default=200, ge=1, le=1000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
) -> dict:
|
||||||
|
repo = SystemsRepository(db)
|
||||||
|
items, total = repo.list(limit=limit, offset=offset)
|
||||||
|
return {"items": [{"id": i.id, "system_key": i.system_key, "name": i.name, "status": i.status} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/modules")
|
||||||
|
def internal_list_modules(
|
||||||
|
_: None = Depends(verify_internal_secret),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
limit: int = Query(default=500, ge=1, le=2000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
) -> dict:
|
||||||
|
modules_repo = ModulesRepository(db)
|
||||||
|
items, total = modules_repo.list(limit=limit, offset=offset)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": i.id,
|
||||||
|
"module_key": i.module_key,
|
||||||
|
"system_key": i.module_key.split(".", 1)[0] if "." in i.module_key else None,
|
||||||
|
"name": i.name,
|
||||||
|
"status": i.status,
|
||||||
|
}
|
||||||
|
for i in items
|
||||||
|
],
|
||||||
|
"total": total,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/companies")
|
||||||
|
def internal_list_companies(
|
||||||
|
_: None = Depends(verify_internal_secret),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
keyword: str | None = Query(default=None),
|
||||||
|
limit: int = Query(default=500, ge=1, le=2000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
) -> dict:
|
||||||
|
repo = CompaniesRepository(db)
|
||||||
|
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
|
||||||
|
return {"items": [{"id": i.id, "company_key": i.company_key, "name": i.name, "status": i.status} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sites")
|
||||||
|
def internal_list_sites(
|
||||||
|
_: None = Depends(verify_internal_secret),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
company_key: str | None = Query(default=None),
|
||||||
|
limit: int = Query(default=500, ge=1, le=2000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
) -> dict:
|
||||||
|
companies_repo = CompaniesRepository(db)
|
||||||
|
sites_repo = SitesRepository(db)
|
||||||
|
company_id = None
|
||||||
|
if company_key:
|
||||||
|
company = companies_repo.get_by_key(company_key)
|
||||||
|
if company:
|
||||||
|
company_id = company.id
|
||||||
|
companies, _ = companies_repo.list(limit=2000, offset=0)
|
||||||
|
mapping = {c.id: c.company_key for c in companies}
|
||||||
|
items, total = sites_repo.list(company_id=company_id, limit=limit, offset=offset)
|
||||||
|
return {"items": [{"id": i.id, "site_key": i.site_key, "company_key": mapping.get(i.company_id), "name": i.name, "status": i.status} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/members")
|
||||||
|
def internal_list_members(
|
||||||
|
_: None = Depends(verify_internal_secret),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
keyword: str | None = Query(default=None),
|
||||||
|
limit: int = Query(default=500, ge=1, le=2000),
|
||||||
|
offset: int = Query(default=0, ge=0),
|
||||||
|
) -> dict:
|
||||||
|
repo = UsersRepository(db)
|
||||||
|
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
|
||||||
|
return {"items": [{"id": i.id, "authentik_sub": i.authentik_sub, "email": i.email, "display_name": i.display_name, "is_active": i.is_active} for i in items], "total": total, "limit": limit, "offset": offset}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
from fastapi import APIRouter, Depends, Query
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.api.internal import verify_internal_secret
|
|
||||||
from app.db.session import get_db
|
|
||||||
from app.repositories.member_organizations_repo import MemberOrganizationsRepository
|
|
||||||
from app.repositories.organizations_repo import OrganizationsRepository
|
|
||||||
from app.repositories.users_repo import UsersRepository
|
|
||||||
from app.schemas.members import MemberListResponse, MemberOrganizationsResponse, MemberSummary
|
|
||||||
from app.schemas.organizations import OrganizationListResponse, OrganizationSummary
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/internal", tags=["internal"])
|
|
||||||
|
|
||||||
|
|
||||||
def _to_member_summary(member) -> MemberSummary:
|
|
||||||
return MemberSummary(
|
|
||||||
id=member.id,
|
|
||||||
authentik_sub=member.authentik_sub,
|
|
||||||
authentik_user_id=member.authentik_user_id,
|
|
||||||
email=member.email,
|
|
||||||
display_name=member.display_name,
|
|
||||||
is_active=member.is_active,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _to_org_summary(org) -> OrganizationSummary:
|
|
||||||
return OrganizationSummary(
|
|
||||||
id=org.id,
|
|
||||||
org_code=org.org_code,
|
|
||||||
name=org.name,
|
|
||||||
tax_id=org.tax_id,
|
|
||||||
status=org.status,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/members", response_model=MemberListResponse)
|
|
||||||
def internal_list_members(
|
|
||||||
_: None = Depends(verify_internal_secret),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
keyword: str | None = Query(default=None),
|
|
||||||
is_active: bool | None = Query(default=None),
|
|
||||||
limit: int = Query(default=200, ge=1, le=500),
|
|
||||||
offset: int = Query(default=0, ge=0),
|
|
||||||
) -> MemberListResponse:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
items, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset)
|
|
||||||
return MemberListResponse(items=[_to_member_summary(i) for i in items], total=total, limit=limit, offset=offset)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/members/by-sub/{authentik_sub}", response_model=MemberSummary | None)
|
|
||||||
def internal_get_member_by_sub(
|
|
||||||
authentik_sub: str,
|
|
||||||
_: None = Depends(verify_internal_secret),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberSummary | None:
|
|
||||||
repo = UsersRepository(db)
|
|
||||||
member = repo.get_by_sub(authentik_sub)
|
|
||||||
return _to_member_summary(member) if member else None
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/organizations", response_model=OrganizationListResponse)
|
|
||||||
def internal_list_organizations(
|
|
||||||
_: None = Depends(verify_internal_secret),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
keyword: str | None = Query(default=None),
|
|
||||||
status_filter: str | None = Query(default=None, alias="status"),
|
|
||||||
limit: int = Query(default=200, ge=1, le=500),
|
|
||||||
offset: int = Query(default=0, ge=0),
|
|
||||||
) -> OrganizationListResponse:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
|
|
||||||
return OrganizationListResponse(items=[_to_org_summary(i) for i in items], total=total, limit=limit, offset=offset)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/organizations/by-code/{org_code}", response_model=OrganizationSummary | None)
|
|
||||||
def internal_get_organization_by_code(
|
|
||||||
org_code: str,
|
|
||||||
_: None = Depends(verify_internal_secret),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> OrganizationSummary | None:
|
|
||||||
repo = OrganizationsRepository(db)
|
|
||||||
org = repo.get_by_code(org_code)
|
|
||||||
return _to_org_summary(org) if org else None
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/members/{member_id}/organizations", response_model=MemberOrganizationsResponse | None)
|
|
||||||
def internal_list_member_organizations(
|
|
||||||
member_id: str,
|
|
||||||
_: None = Depends(verify_internal_secret),
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> MemberOrganizationsResponse | None:
|
|
||||||
users_repo = UsersRepository(db)
|
|
||||||
link_repo = MemberOrganizationsRepository(db)
|
|
||||||
|
|
||||||
member = users_repo.get_by_id(member_id)
|
|
||||||
if not member:
|
|
||||||
return None
|
|
||||||
|
|
||||||
orgs = link_repo.list_organizations_by_member_id(member_id)
|
|
||||||
return MemberOrganizationsResponse(member=_to_member_summary(member), organizations=[_to_org_summary(o) for o in orgs])
|
|
||||||
@@ -51,8 +51,7 @@ def get_my_permission_snapshot(
|
|||||||
display_name=principal.name or principal.preferred_username,
|
display_name=principal.name or principal.preferred_username,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
permissions = perms_repo.list_by_user_id(user.id)
|
permissions = perms_repo.list_by_user(user.id, user.authentik_sub)
|
||||||
tuples = [(p.scope_type, p.scope_id, p.module, p.action) for p in permissions]
|
return PermissionService.build_snapshot(authentik_sub=principal.sub, permissions=permissions)
|
||||||
return PermissionService.build_snapshot(authentik_sub=principal.sub, permissions=tuples)
|
|
||||||
except SQLAlchemyError:
|
except SQLAlchemyError:
|
||||||
return PermissionSnapshotResponse(authentik_sub=principal.sub, permissions=[])
|
return PermissionSnapshotResponse(authentik_sub=principal.sub, permissions=[])
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.api.admin import router as admin_router
|
from app.api.admin import router as admin_router
|
||||||
from app.api.admin_entities import router as admin_entities_router
|
from app.api.admin_catalog import router as admin_catalog_router
|
||||||
from app.api.auth import router as auth_router
|
from app.api.auth import router as auth_router
|
||||||
|
from app.api.internal_catalog import router as internal_catalog_router
|
||||||
from app.api.internal import router as internal_router
|
from app.api.internal import router as internal_router
|
||||||
from app.api.internal_entities import router as internal_entities_router
|
|
||||||
from app.api.me import router as me_router
|
from app.api.me import router as me_router
|
||||||
from app.core.config import get_settings
|
from app.core.config import get_settings
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ def healthz() -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
app.include_router(internal_router)
|
app.include_router(internal_router)
|
||||||
app.include_router(internal_entities_router)
|
app.include_router(internal_catalog_router)
|
||||||
app.include_router(admin_router)
|
app.include_router(admin_router)
|
||||||
app.include_router(admin_entities_router)
|
app.include_router(admin_catalog_router)
|
||||||
app.include_router(me_router)
|
app.include_router(me_router)
|
||||||
app.include_router(auth_router)
|
app.include_router(auth_router)
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
from app.models.api_client import ApiClient
|
from app.models.api_client import ApiClient
|
||||||
from app.models.member_organization import MemberOrganization
|
from app.models.company import Company
|
||||||
from app.models.organization import Organization
|
from app.models.module import Module
|
||||||
from app.models.permission import Permission
|
from app.models.permission import Permission
|
||||||
|
from app.models.permission_group import PermissionGroup
|
||||||
|
from app.models.permission_group_member import PermissionGroupMember
|
||||||
|
from app.models.permission_group_permission import PermissionGroupPermission
|
||||||
|
from app.models.site import Site
|
||||||
|
from app.models.system import System
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
from app.models.user_scope_permission import UserScopePermission
|
||||||
|
|
||||||
__all__ = ["ApiClient", "MemberOrganization", "Organization", "Permission", "User"]
|
__all__ = [
|
||||||
|
"ApiClient",
|
||||||
|
"Company",
|
||||||
|
"Module",
|
||||||
|
"Permission",
|
||||||
|
"PermissionGroup",
|
||||||
|
"PermissionGroupMember",
|
||||||
|
"PermissionGroupPermission",
|
||||||
|
"Site",
|
||||||
|
"System",
|
||||||
|
"User",
|
||||||
|
"UserScopePermission",
|
||||||
|
]
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ from sqlalchemy.orm import Mapped, mapped_column
|
|||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
class Organization(Base):
|
class Company(Base):
|
||||||
__tablename__ = "organizations"
|
__tablename__ = "companies"
|
||||||
|
|
||||||
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()))
|
||||||
org_code: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
company_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
tax_id: Mapped[str | None] = mapped_column(String(32))
|
|
||||||
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(
|
||||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from sqlalchemy import DateTime, ForeignKey, UniqueConstraint, func
|
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
from app.db.base import Base
|
|
||||||
|
|
||||||
|
|
||||||
class MemberOrganization(Base):
|
|
||||||
__tablename__ = "member_organizations"
|
|
||||||
__table_args__ = (
|
|
||||||
UniqueConstraint("member_id", "organization_id", name="uq_member_organizations_member_org"),
|
|
||||||
)
|
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
|
||||||
member_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
||||||
organization_id: Mapped[str] = mapped_column(
|
|
||||||
UUID(as_uuid=False), ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False
|
|
||||||
)
|
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
|
||||||
21
app/models/module.py
Normal file
21
app/models/module.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Module(Base):
|
||||||
|
__tablename__ = "modules"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
module_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
|
)
|
||||||
21
app/models/permission_group.py
Normal file
21
app/models/permission_group.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionGroup(Base):
|
||||||
|
__tablename__ = "permission_groups"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
group_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
|
)
|
||||||
20
app/models/permission_group_member.py
Normal file
20
app/models/permission_group_member.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, String, UniqueConstraint, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionGroupMember(Base):
|
||||||
|
__tablename__ = "permission_group_members"
|
||||||
|
__table_args__ = (UniqueConstraint("group_id", "authentik_sub", name="uq_permission_group_members_group_sub"),)
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
group_id: Mapped[str] = mapped_column(
|
||||||
|
UUID(as_uuid=False), ForeignKey("permission_groups.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
authentik_sub: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
23
app/models/permission_group_permission.py
Normal file
23
app/models/permission_group_permission.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionGroupPermission(Base):
|
||||||
|
__tablename__ = "permission_group_permissions"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
group_id: Mapped[str] = mapped_column(
|
||||||
|
UUID(as_uuid=False), ForeignKey("permission_groups.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
system: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
module: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
action: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
scope_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
scope_id: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
22
app/models/site.py
Normal file
22
app/models/site.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Site(Base):
|
||||||
|
__tablename__ = "sites"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
site_key: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, index=True)
|
||||||
|
company_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
|
)
|
||||||
21
app/models/system.py
Normal file
21
app/models/system.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class System(Base):
|
||||||
|
__tablename__ = "systems"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
system_key: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
|
)
|
||||||
24
app/models/user_scope_permission.py
Normal file
24
app/models/user_scope_permission.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class UserScopePermission(Base):
|
||||||
|
__tablename__ = "user_scope_permissions"
|
||||||
|
|
||||||
|
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
||||||
|
user_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
module_id: Mapped[str] = mapped_column(UUID(as_uuid=False), ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
|
||||||
|
action: Mapped[str] = mapped_column(String(32), nullable=False)
|
||||||
|
scope_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||||
|
company_id: Mapped[str | None] = mapped_column(UUID(as_uuid=False), ForeignKey("companies.id", ondelete="CASCADE"))
|
||||||
|
site_id: Mapped[str | None] = mapped_column(UUID(as_uuid=False), ForeignKey("sites.id", ondelete="CASCADE"))
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
|
)
|
||||||
35
app/repositories/companies_repo.py
Normal file
35
app/repositories/companies_repo.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from sqlalchemy import func, or_, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.company import Company
|
||||||
|
|
||||||
|
|
||||||
|
class CompaniesRepository:
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_by_key(self, company_key: str) -> Company | None:
|
||||||
|
stmt = select(Company).where(Company.company_key == company_key)
|
||||||
|
return self.db.scalar(stmt)
|
||||||
|
|
||||||
|
def get_by_id(self, company_id: str) -> Company | None:
|
||||||
|
stmt = select(Company).where(Company.id == company_id)
|
||||||
|
return self.db.scalar(stmt)
|
||||||
|
|
||||||
|
def list(self, keyword: str | None = None, limit: int = 100, offset: int = 0) -> tuple[list[Company], int]:
|
||||||
|
stmt = select(Company)
|
||||||
|
count_stmt = select(func.count()).select_from(Company)
|
||||||
|
if keyword:
|
||||||
|
pattern = f"%{keyword}%"
|
||||||
|
cond = or_(Company.company_key.ilike(pattern), Company.name.ilike(pattern))
|
||||||
|
stmt = stmt.where(cond)
|
||||||
|
count_stmt = count_stmt.where(cond)
|
||||||
|
stmt = stmt.order_by(Company.created_at.desc()).limit(limit).offset(offset)
|
||||||
|
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||||
|
|
||||||
|
def create(self, company_key: str, name: str, status: str = "active") -> Company:
|
||||||
|
item = Company(company_key=company_key, name=name, status=status)
|
||||||
|
self.db.add(item)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(item)
|
||||||
|
return item
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
from sqlalchemy import delete, select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.models.member_organization import MemberOrganization
|
|
||||||
from app.models.organization import Organization
|
|
||||||
|
|
||||||
|
|
||||||
class MemberOrganizationsRepository:
|
|
||||||
def __init__(self, db: Session) -> None:
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
def list_organizations_by_member_id(self, member_id: str) -> list[Organization]:
|
|
||||||
stmt = (
|
|
||||||
select(Organization)
|
|
||||||
.join(MemberOrganization, MemberOrganization.organization_id == Organization.id)
|
|
||||||
.where(MemberOrganization.member_id == member_id)
|
|
||||||
.order_by(Organization.name.asc())
|
|
||||||
)
|
|
||||||
return list(self.db.scalars(stmt).all())
|
|
||||||
|
|
||||||
def add_if_not_exists(self, member_id: str, organization_id: str) -> MemberOrganization:
|
|
||||||
stmt = select(MemberOrganization).where(
|
|
||||||
MemberOrganization.member_id == member_id,
|
|
||||||
MemberOrganization.organization_id == organization_id,
|
|
||||||
)
|
|
||||||
existing = self.db.scalar(stmt)
|
|
||||||
if existing:
|
|
||||||
return existing
|
|
||||||
|
|
||||||
relation = MemberOrganization(member_id=member_id, organization_id=organization_id)
|
|
||||||
self.db.add(relation)
|
|
||||||
self.db.commit()
|
|
||||||
self.db.refresh(relation)
|
|
||||||
return relation
|
|
||||||
|
|
||||||
def remove(self, member_id: str, organization_id: str) -> int:
|
|
||||||
stmt = delete(MemberOrganization).where(
|
|
||||||
MemberOrganization.member_id == member_id,
|
|
||||||
MemberOrganization.organization_id == organization_id,
|
|
||||||
)
|
|
||||||
result = self.db.execute(stmt)
|
|
||||||
self.db.commit()
|
|
||||||
return int(result.rowcount or 0)
|
|
||||||
26
app/repositories/modules_repo.py
Normal file
26
app/repositories/modules_repo.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.module import Module
|
||||||
|
|
||||||
|
|
||||||
|
class ModulesRepository:
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_by_key(self, module_key: str) -> Module | None:
|
||||||
|
stmt = select(Module).where(Module.module_key == module_key)
|
||||||
|
return self.db.scalar(stmt)
|
||||||
|
|
||||||
|
def list(self, limit: int = 200, offset: int = 0) -> tuple[list[Module], int]:
|
||||||
|
stmt = select(Module)
|
||||||
|
count_stmt = select(func.count()).select_from(Module)
|
||||||
|
stmt = stmt.order_by(Module.created_at.desc()).limit(limit).offset(offset)
|
||||||
|
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||||
|
|
||||||
|
def create(self, module_key: str, name: str, status: str = "active") -> Module:
|
||||||
|
item = Module(module_key=module_key, name=name, status=status)
|
||||||
|
self.db.add(item)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(item)
|
||||||
|
return item
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
from sqlalchemy import func, or_, select
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.models.organization import Organization
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationsRepository:
|
|
||||||
def __init__(self, db: Session) -> None:
|
|
||||||
self.db = db
|
|
||||||
|
|
||||||
def list(
|
|
||||||
self,
|
|
||||||
keyword: str | None = None,
|
|
||||||
status: str | None = None,
|
|
||||||
limit: int = 50,
|
|
||||||
offset: int = 0,
|
|
||||||
) -> tuple[list[Organization], int]:
|
|
||||||
stmt = select(Organization)
|
|
||||||
count_stmt = select(func.count()).select_from(Organization)
|
|
||||||
|
|
||||||
if keyword:
|
|
||||||
pattern = f"%{keyword}%"
|
|
||||||
cond = or_(Organization.org_code.ilike(pattern), Organization.name.ilike(pattern))
|
|
||||||
stmt = stmt.where(cond)
|
|
||||||
count_stmt = count_stmt.where(cond)
|
|
||||||
|
|
||||||
if status:
|
|
||||||
stmt = stmt.where(Organization.status == status)
|
|
||||||
count_stmt = count_stmt.where(Organization.status == status)
|
|
||||||
|
|
||||||
stmt = stmt.order_by(Organization.created_at.desc()).limit(limit).offset(offset)
|
|
||||||
items = list(self.db.scalars(stmt).all())
|
|
||||||
total = int(self.db.scalar(count_stmt) or 0)
|
|
||||||
return items, total
|
|
||||||
|
|
||||||
def get_by_id(self, org_id: str) -> Organization | None:
|
|
||||||
stmt = select(Organization).where(Organization.id == org_id)
|
|
||||||
return self.db.scalar(stmt)
|
|
||||||
|
|
||||||
def get_by_code(self, org_code: str) -> Organization | None:
|
|
||||||
stmt = select(Organization).where(Organization.org_code == org_code)
|
|
||||||
return self.db.scalar(stmt)
|
|
||||||
|
|
||||||
def create(self, org_code: str, name: str, tax_id: str | None, status: str = "active") -> Organization:
|
|
||||||
org = Organization(org_code=org_code, name=name, tax_id=tax_id, status=status)
|
|
||||||
self.db.add(org)
|
|
||||||
self.db.commit()
|
|
||||||
self.db.refresh(org)
|
|
||||||
return org
|
|
||||||
|
|
||||||
def update(
|
|
||||||
self,
|
|
||||||
org: Organization,
|
|
||||||
*,
|
|
||||||
name: str | None = None,
|
|
||||||
tax_id: str | None = None,
|
|
||||||
status: str | None = None,
|
|
||||||
) -> Organization:
|
|
||||||
if name is not None:
|
|
||||||
org.name = name
|
|
||||||
if tax_id is not None:
|
|
||||||
org.tax_id = tax_id
|
|
||||||
if status is not None:
|
|
||||||
org.status = status
|
|
||||||
self.db.commit()
|
|
||||||
self.db.refresh(org)
|
|
||||||
return org
|
|
||||||
106
app/repositories/permission_groups_repo.py
Normal file
106
app/repositories/permission_groups_repo.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
from sqlalchemy import delete, func, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.permission_group import PermissionGroup
|
||||||
|
from app.models.permission_group_member import PermissionGroupMember
|
||||||
|
from app.models.permission_group_permission import PermissionGroupPermission
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionGroupsRepository:
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_by_key(self, group_key: str) -> PermissionGroup | None:
|
||||||
|
return self.db.scalar(select(PermissionGroup).where(PermissionGroup.group_key == group_key))
|
||||||
|
|
||||||
|
def get_by_id(self, group_id: str) -> PermissionGroup | None:
|
||||||
|
return self.db.scalar(select(PermissionGroup).where(PermissionGroup.id == group_id))
|
||||||
|
|
||||||
|
def list(self, limit: int = 100, offset: int = 0) -> tuple[list[PermissionGroup], int]:
|
||||||
|
stmt = select(PermissionGroup).order_by(PermissionGroup.created_at.desc()).limit(limit).offset(offset)
|
||||||
|
count_stmt = select(func.count()).select_from(PermissionGroup)
|
||||||
|
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||||
|
|
||||||
|
def create(self, group_key: str, name: str, status: str = "active") -> PermissionGroup:
|
||||||
|
item = PermissionGroup(group_key=group_key, name=name, status=status)
|
||||||
|
self.db.add(item)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def add_member_if_not_exists(self, group_id: str, authentik_sub: str) -> PermissionGroupMember:
|
||||||
|
existing = self.db.scalar(
|
||||||
|
select(PermissionGroupMember).where(
|
||||||
|
PermissionGroupMember.group_id == group_id, PermissionGroupMember.authentik_sub == authentik_sub
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
row = PermissionGroupMember(group_id=group_id, authentik_sub=authentik_sub)
|
||||||
|
self.db.add(row)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def remove_member(self, group_id: str, authentik_sub: str) -> int:
|
||||||
|
result = self.db.execute(
|
||||||
|
delete(PermissionGroupMember).where(
|
||||||
|
PermissionGroupMember.group_id == group_id, PermissionGroupMember.authentik_sub == authentik_sub
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.db.commit()
|
||||||
|
return int(result.rowcount or 0)
|
||||||
|
|
||||||
|
def grant_group_permission(
|
||||||
|
self,
|
||||||
|
group_id: str,
|
||||||
|
system: str,
|
||||||
|
module: str,
|
||||||
|
action: str,
|
||||||
|
scope_type: str,
|
||||||
|
scope_id: str,
|
||||||
|
) -> PermissionGroupPermission:
|
||||||
|
where = [
|
||||||
|
PermissionGroupPermission.group_id == group_id,
|
||||||
|
PermissionGroupPermission.system == system,
|
||||||
|
PermissionGroupPermission.module == module,
|
||||||
|
PermissionGroupPermission.action == action,
|
||||||
|
PermissionGroupPermission.scope_type == scope_type,
|
||||||
|
PermissionGroupPermission.scope_id == scope_id,
|
||||||
|
]
|
||||||
|
existing = self.db.scalar(select(PermissionGroupPermission).where(*where))
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
row = PermissionGroupPermission(
|
||||||
|
group_id=group_id,
|
||||||
|
system=system,
|
||||||
|
module=module,
|
||||||
|
action=action,
|
||||||
|
scope_type=scope_type,
|
||||||
|
scope_id=scope_id,
|
||||||
|
)
|
||||||
|
self.db.add(row)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(row)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def revoke_group_permission(
|
||||||
|
self,
|
||||||
|
group_id: str,
|
||||||
|
system: str,
|
||||||
|
module: str,
|
||||||
|
action: str,
|
||||||
|
scope_type: str,
|
||||||
|
scope_id: str,
|
||||||
|
) -> int:
|
||||||
|
stmt = delete(PermissionGroupPermission).where(
|
||||||
|
PermissionGroupPermission.group_id == group_id,
|
||||||
|
PermissionGroupPermission.system == system,
|
||||||
|
PermissionGroupPermission.module == module,
|
||||||
|
PermissionGroupPermission.action == action,
|
||||||
|
PermissionGroupPermission.scope_type == scope_type,
|
||||||
|
PermissionGroupPermission.scope_id == scope_id,
|
||||||
|
)
|
||||||
|
result = self.db.execute(stmt)
|
||||||
|
self.db.commit()
|
||||||
|
return int(result.rowcount or 0)
|
||||||
@@ -1,42 +1,93 @@
|
|||||||
from sqlalchemy import delete, select
|
from sqlalchemy import and_, delete, or_, select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.models.permission import Permission
|
from app.models.company import Company
|
||||||
|
from app.models.module import Module
|
||||||
|
from app.models.permission_group_member import PermissionGroupMember
|
||||||
|
from app.models.permission_group_permission import PermissionGroupPermission
|
||||||
|
from app.models.site import Site
|
||||||
|
from app.models.user_scope_permission import UserScopePermission
|
||||||
|
|
||||||
|
|
||||||
class PermissionsRepository:
|
class PermissionsRepository:
|
||||||
def __init__(self, db: Session) -> None:
|
def __init__(self, db: Session) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
def list_by_user_id(self, user_id: str) -> list[Permission]:
|
def list_by_user(self, user_id: str, authentik_sub: str) -> list[tuple[str, str, str | None, str, str]]:
|
||||||
stmt = select(Permission).where(Permission.user_id == user_id)
|
direct_stmt = (
|
||||||
return list(self.db.scalars(stmt).all())
|
select(
|
||||||
|
UserScopePermission.scope_type,
|
||||||
|
Company.company_key,
|
||||||
|
Site.site_key,
|
||||||
|
Module.module_key,
|
||||||
|
UserScopePermission.action,
|
||||||
|
)
|
||||||
|
.select_from(UserScopePermission)
|
||||||
|
.join(Module, Module.id == UserScopePermission.module_id)
|
||||||
|
.join(Company, Company.id == UserScopePermission.company_id, isouter=True)
|
||||||
|
.join(Site, Site.id == UserScopePermission.site_id, isouter=True)
|
||||||
|
.where(UserScopePermission.user_id == user_id)
|
||||||
|
)
|
||||||
|
group_stmt = (
|
||||||
|
select(
|
||||||
|
PermissionGroupPermission.scope_type,
|
||||||
|
PermissionGroupPermission.scope_id,
|
||||||
|
PermissionGroupPermission.system,
|
||||||
|
PermissionGroupPermission.module,
|
||||||
|
PermissionGroupPermission.action,
|
||||||
|
)
|
||||||
|
.select_from(PermissionGroupPermission)
|
||||||
|
.join(PermissionGroupMember, PermissionGroupMember.group_id == PermissionGroupPermission.group_id)
|
||||||
|
.where(PermissionGroupMember.authentik_sub == authentik_sub)
|
||||||
|
)
|
||||||
|
rows = self.db.execute(direct_stmt).all() + self.db.execute(group_stmt).all()
|
||||||
|
result: list[tuple[str, str, str | None, str, str]] = []
|
||||||
|
dedup = set()
|
||||||
|
for row in rows:
|
||||||
|
if len(row) == 5:
|
||||||
|
scope_type, scope_id, system_key, module_key, action = row
|
||||||
|
else:
|
||||||
|
scope_type, company_key, site_key, module_key, action = row
|
||||||
|
scope_id = company_key if scope_type == "company" else site_key
|
||||||
|
system_key = module_key.split(".", 1)[0] if isinstance(module_key, str) and "." in module_key else None
|
||||||
|
key = (scope_type, scope_id or "", system_key, module_key, action)
|
||||||
|
if key in dedup:
|
||||||
|
continue
|
||||||
|
dedup.add(key)
|
||||||
|
result.append(key)
|
||||||
|
return result
|
||||||
|
|
||||||
def create_if_not_exists(
|
def create_if_not_exists(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
scope_type: str,
|
module_id: str,
|
||||||
scope_id: str,
|
|
||||||
module: str,
|
|
||||||
action: str,
|
action: str,
|
||||||
) -> Permission:
|
scope_type: str,
|
||||||
stmt = select(Permission).where(
|
company_id: str | None,
|
||||||
Permission.user_id == user_id,
|
site_id: str | None,
|
||||||
Permission.scope_type == scope_type,
|
) -> UserScopePermission:
|
||||||
Permission.scope_id == scope_id,
|
where_expr = [
|
||||||
Permission.module == module,
|
UserScopePermission.user_id == user_id,
|
||||||
Permission.action == action,
|
UserScopePermission.module_id == module_id,
|
||||||
)
|
UserScopePermission.action == action,
|
||||||
existing = self.db.scalar(stmt)
|
UserScopePermission.scope_type == scope_type,
|
||||||
|
]
|
||||||
|
if scope_type == "company":
|
||||||
|
where_expr.append(UserScopePermission.company_id == company_id)
|
||||||
|
else:
|
||||||
|
where_expr.append(UserScopePermission.site_id == site_id)
|
||||||
|
|
||||||
|
existing = self.db.scalar(select(UserScopePermission).where(and_(*where_expr)))
|
||||||
if existing:
|
if existing:
|
||||||
return existing
|
return existing
|
||||||
|
|
||||||
item = Permission(
|
item = UserScopePermission(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
scope_type=scope_type,
|
module_id=module_id,
|
||||||
scope_id=scope_id,
|
|
||||||
module=module,
|
|
||||||
action=action,
|
action=action,
|
||||||
|
scope_type=scope_type,
|
||||||
|
company_id=company_id,
|
||||||
|
site_id=site_id,
|
||||||
)
|
)
|
||||||
self.db.add(item)
|
self.db.add(item)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
@@ -46,17 +97,21 @@ class PermissionsRepository:
|
|||||||
def revoke(
|
def revoke(
|
||||||
self,
|
self,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
scope_type: str,
|
module_id: str,
|
||||||
scope_id: str,
|
|
||||||
module: str,
|
|
||||||
action: str,
|
action: str,
|
||||||
|
scope_type: str,
|
||||||
|
company_id: str | None,
|
||||||
|
site_id: str | None,
|
||||||
) -> int:
|
) -> int:
|
||||||
stmt = delete(Permission).where(
|
stmt = delete(UserScopePermission).where(
|
||||||
Permission.user_id == user_id,
|
UserScopePermission.user_id == user_id,
|
||||||
Permission.scope_type == scope_type,
|
UserScopePermission.module_id == module_id,
|
||||||
Permission.scope_id == scope_id,
|
UserScopePermission.action == action,
|
||||||
Permission.module == module,
|
UserScopePermission.scope_type == scope_type,
|
||||||
Permission.action == action,
|
or_(
|
||||||
|
and_(scope_type == "company", UserScopePermission.company_id == company_id),
|
||||||
|
and_(scope_type == "site", UserScopePermission.site_id == site_id),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
result = self.db.execute(stmt)
|
result = self.db.execute(stmt)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|||||||
40
app/repositories/sites_repo.py
Normal file
40
app/repositories/sites_repo.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from sqlalchemy import func, or_, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.site import Site
|
||||||
|
|
||||||
|
|
||||||
|
class SitesRepository:
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_by_key(self, site_key: str) -> Site | None:
|
||||||
|
stmt = select(Site).where(Site.site_key == site_key)
|
||||||
|
return self.db.scalar(stmt)
|
||||||
|
|
||||||
|
def list(
|
||||||
|
self,
|
||||||
|
keyword: str | None = None,
|
||||||
|
company_id: str | None = None,
|
||||||
|
limit: int = 100,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> tuple[list[Site], int]:
|
||||||
|
stmt = select(Site)
|
||||||
|
count_stmt = select(func.count()).select_from(Site)
|
||||||
|
if keyword:
|
||||||
|
pattern = f"%{keyword}%"
|
||||||
|
cond = or_(Site.site_key.ilike(pattern), Site.name.ilike(pattern))
|
||||||
|
stmt = stmt.where(cond)
|
||||||
|
count_stmt = count_stmt.where(cond)
|
||||||
|
if company_id:
|
||||||
|
stmt = stmt.where(Site.company_id == company_id)
|
||||||
|
count_stmt = count_stmt.where(Site.company_id == company_id)
|
||||||
|
stmt = stmt.order_by(Site.created_at.desc()).limit(limit).offset(offset)
|
||||||
|
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||||
|
|
||||||
|
def create(self, site_key: str, company_id: str, name: str, status: str = "active") -> Site:
|
||||||
|
item = Site(site_key=site_key, company_id=company_id, name=name, status=status)
|
||||||
|
self.db.add(item)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(item)
|
||||||
|
return item
|
||||||
33
app/repositories/systems_repo.py
Normal file
33
app/repositories/systems_repo.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.system import System
|
||||||
|
|
||||||
|
|
||||||
|
class SystemsRepository:
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def get_by_key(self, system_key: str) -> System | None:
|
||||||
|
stmt = select(System).where(System.system_key == system_key)
|
||||||
|
return self.db.scalar(stmt)
|
||||||
|
|
||||||
|
def get_by_id(self, system_id: str) -> System | None:
|
||||||
|
stmt = select(System).where(System.id == system_id)
|
||||||
|
return self.db.scalar(stmt)
|
||||||
|
|
||||||
|
def list(self, status: str | None = None, limit: int = 100, offset: int = 0) -> tuple[list[System], int]:
|
||||||
|
stmt = select(System)
|
||||||
|
count_stmt = select(func.count()).select_from(System)
|
||||||
|
if status:
|
||||||
|
stmt = stmt.where(System.status == status)
|
||||||
|
count_stmt = count_stmt.where(System.status == status)
|
||||||
|
stmt = stmt.order_by(System.created_at.desc()).limit(limit).offset(offset)
|
||||||
|
return list(self.db.scalars(stmt).all()), int(self.db.scalar(count_stmt) or 0)
|
||||||
|
|
||||||
|
def create(self, system_key: str, name: str, status: str = "active") -> System:
|
||||||
|
item = System(system_key=system_key, name=name, status=status)
|
||||||
|
self.db.add(item)
|
||||||
|
self.db.commit()
|
||||||
|
self.db.refresh(item)
|
||||||
|
return item
|
||||||
85
app/schemas/catalog.py
Normal file
85
app/schemas/catalog.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class SystemCreateRequest(BaseModel):
|
||||||
|
system_key: str
|
||||||
|
name: str
|
||||||
|
status: str = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class SystemItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
system_key: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleCreateRequest(BaseModel):
|
||||||
|
system_key: str
|
||||||
|
module_key: str
|
||||||
|
name: str
|
||||||
|
status: str = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
system_key: str | None = None
|
||||||
|
module_key: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyCreateRequest(BaseModel):
|
||||||
|
company_key: str
|
||||||
|
name: str
|
||||||
|
status: str = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
company_key: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class SiteCreateRequest(BaseModel):
|
||||||
|
site_key: str
|
||||||
|
company_key: str
|
||||||
|
name: str
|
||||||
|
status: str = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class SiteItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
site_key: str
|
||||||
|
company_key: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
|
||||||
|
|
||||||
|
class MemberItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
authentik_sub: str
|
||||||
|
email: str | None = None
|
||||||
|
display_name: str | None = None
|
||||||
|
is_active: bool
|
||||||
|
|
||||||
|
|
||||||
|
class ListResponse(BaseModel):
|
||||||
|
items: list
|
||||||
|
total: int
|
||||||
|
limit: int
|
||||||
|
offset: int
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionGroupCreateRequest(BaseModel):
|
||||||
|
group_key: str
|
||||||
|
name: str
|
||||||
|
status: str = "active"
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionGroupItem(BaseModel):
|
||||||
|
id: str
|
||||||
|
group_key: str
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from app.schemas.organizations import OrganizationSummary
|
|
||||||
|
|
||||||
|
|
||||||
class MemberCreateRequest(BaseModel):
|
|
||||||
authentik_sub: str
|
|
||||||
email: str | None = None
|
|
||||||
display_name: str | None = None
|
|
||||||
is_active: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class MemberUpdateRequest(BaseModel):
|
|
||||||
email: str | None = None
|
|
||||||
display_name: str | None = None
|
|
||||||
is_active: bool | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class MemberSummary(BaseModel):
|
|
||||||
id: str
|
|
||||||
authentik_sub: str
|
|
||||||
authentik_user_id: int | None = None
|
|
||||||
email: str | None = None
|
|
||||||
display_name: str | None = None
|
|
||||||
is_active: bool
|
|
||||||
|
|
||||||
|
|
||||||
class MemberListResponse(BaseModel):
|
|
||||||
items: list[MemberSummary]
|
|
||||||
total: int
|
|
||||||
limit: int
|
|
||||||
offset: int
|
|
||||||
|
|
||||||
|
|
||||||
class MemberOrganizationsResponse(BaseModel):
|
|
||||||
member: MemberSummary
|
|
||||||
organizations: list[OrganizationSummary]
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationCreateRequest(BaseModel):
|
|
||||||
org_code: str
|
|
||||||
name: str
|
|
||||||
tax_id: str | None = None
|
|
||||||
status: str = "active"
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationUpdateRequest(BaseModel):
|
|
||||||
name: str | None = None
|
|
||||||
tax_id: str | None = None
|
|
||||||
status: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationSummary(BaseModel):
|
|
||||||
id: str
|
|
||||||
org_code: str
|
|
||||||
name: str
|
|
||||||
tax_id: str | None = None
|
|
||||||
status: str
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizationListResponse(BaseModel):
|
|
||||||
items: list[OrganizationSummary]
|
|
||||||
total: int
|
|
||||||
limit: int
|
|
||||||
offset: int
|
|
||||||
@@ -7,7 +7,8 @@ class PermissionGrantRequest(BaseModel):
|
|||||||
display_name: str | None = None
|
display_name: str | None = None
|
||||||
scope_type: str
|
scope_type: str
|
||||||
scope_id: str
|
scope_id: str
|
||||||
module: str
|
system: str
|
||||||
|
module: str | None = None
|
||||||
action: str
|
action: str
|
||||||
|
|
||||||
|
|
||||||
@@ -15,13 +16,15 @@ class PermissionRevokeRequest(BaseModel):
|
|||||||
authentik_sub: str
|
authentik_sub: str
|
||||||
scope_type: str
|
scope_type: str
|
||||||
scope_id: str
|
scope_id: str
|
||||||
module: str
|
system: str
|
||||||
|
module: str | None = None
|
||||||
action: str
|
action: str
|
||||||
|
|
||||||
|
|
||||||
class PermissionItem(BaseModel):
|
class PermissionItem(BaseModel):
|
||||||
scope_type: str
|
scope_type: str
|
||||||
scope_id: str
|
scope_id: str
|
||||||
|
system: str | None = None
|
||||||
module: str
|
module: str
|
||||||
action: str
|
action: str
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ from app.schemas.permissions import PermissionItem, PermissionSnapshotResponse
|
|||||||
|
|
||||||
class PermissionService:
|
class PermissionService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_snapshot(authentik_sub: str, permissions: list[tuple[str, str, str, str]]) -> PermissionSnapshotResponse:
|
def build_snapshot(authentik_sub: str, permissions: list[tuple[str, str, str | None, str, str]]) -> PermissionSnapshotResponse:
|
||||||
return PermissionSnapshotResponse(
|
return PermissionSnapshotResponse(
|
||||||
authentik_sub=authentik_sub,
|
authentik_sub=authentik_sub,
|
||||||
permissions=[
|
permissions=[
|
||||||
PermissionItem(scope_type=s_type, scope_id=s_id, module=module, action=action)
|
PermissionItem(scope_type=s_type, scope_id=s_id, system=system, module=module, action=action)
|
||||||
for s_type, s_id, module, action in permissions
|
for s_type, s_id, system, module, action in permissions
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,54 +2,138 @@ BEGIN;
|
|||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'record_status') THEN
|
||||||
|
CREATE TYPE record_status AS ENUM ('active','inactive');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
authentik_sub VARCHAR(255) NOT NULL UNIQUE,
|
authentik_sub TEXT NOT NULL UNIQUE,
|
||||||
authentik_user_id INTEGER,
|
authentik_user_id INTEGER,
|
||||||
email VARCHAR(320),
|
email TEXT UNIQUE,
|
||||||
display_name VARCHAR(255),
|
display_name TEXT,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
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()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS permissions (
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
company_key TEXT NOT NULL UNIQUE,
|
||||||
scope_type VARCHAR(32) NOT NULL,
|
name TEXT NOT NULL,
|
||||||
scope_id VARCHAR(128) NOT NULL,
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
module VARCHAR(128) NOT NULL,
|
|
||||||
action VARCHAR(32) NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT uq_permissions_user_scope_module_action
|
|
||||||
UNIQUE (user_id, scope_type, scope_id, module, action)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_authentik_sub ON users(authentik_sub);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_permissions_user_id ON permissions(user_id);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS organizations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
org_code VARCHAR(64) NOT NULL UNIQUE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
tax_id VARCHAR(32),
|
|
||||||
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()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_organizations_org_code ON organizations(org_code);
|
CREATE TABLE IF NOT EXISTS systems (
|
||||||
CREATE INDEX IF NOT EXISTS idx_organizations_status ON organizations(status);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS member_organizations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
member_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
system_key TEXT NOT NULL UNIQUE,
|
||||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
CONSTRAINT uq_member_organizations_member_org UNIQUE (member_id, organization_id)
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_member_organizations_member_id ON member_organizations(member_id);
|
CREATE TABLE IF NOT EXISTS modules (
|
||||||
CREATE INDEX IF NOT EXISTS idx_member_organizations_org_id ON member_organizations(organization_id);
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
module_key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sites (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
site_key TEXT NOT NULL UNIQUE,
|
||||||
|
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'scope_type') THEN
|
||||||
|
CREATE TYPE scope_type AS ENUM ('company','site');
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'permission_action') THEN
|
||||||
|
CREATE TYPE permission_action AS ENUM ('view','create','update','delete','manage');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_scope_permissions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||||
|
action permission_action NOT NULL,
|
||||||
|
scope_type scope_type NOT NULL,
|
||||||
|
company_id UUID REFERENCES companies(id) ON DELETE CASCADE,
|
||||||
|
site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE user_scope_permissions DROP CONSTRAINT IF EXISTS user_scope_permissions_check;
|
||||||
|
ALTER TABLE user_scope_permissions
|
||||||
|
ADD CONSTRAINT user_scope_permissions_check
|
||||||
|
CHECK (
|
||||||
|
((scope_type = 'company' AND company_id IS NOT NULL AND site_id IS NULL)
|
||||||
|
OR (scope_type = 'site' AND site_id IS NOT NULL AND company_id IS NULL))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_usp_company
|
||||||
|
ON user_scope_permissions(user_id, module_id, action, scope_type, company_id)
|
||||||
|
WHERE scope_type = 'company';
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_usp_site
|
||||||
|
ON user_scope_permissions(user_id, module_id, action, scope_type, site_id)
|
||||||
|
WHERE scope_type = 'site';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permission_groups (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
group_key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permission_group_members (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||||
|
authentik_sub TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uq_permission_group_members_group_sub UNIQUE (group_id, authentik_sub)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permission_group_permissions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||||
|
system TEXT NOT NULL,
|
||||||
|
module TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
scope_type TEXT NOT NULL,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_pgp_group_rule
|
||||||
|
ON permission_group_permissions(group_id, system, module, action, scope_type, scope_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_authentik_sub ON users(authentik_sub);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sites_company_id ON sites(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_usp_user_id ON user_scope_permissions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_usp_module_id ON user_scope_permissions(module_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_usp_company_id ON user_scope_permissions(company_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_usp_site_id ON user_scope_permissions(site_id);
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS organizations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
org_code VARCHAR(64) NOT NULL UNIQUE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
tax_id VARCHAR(32),
|
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_organizations_org_code ON organizations(org_code);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_organizations_status ON organizations(status);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS member_organizations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
member_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
CONSTRAINT uq_member_organizations_member_org UNIQUE (member_id, organization_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_member_organizations_member_id ON member_organizations(member_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_member_organizations_org_id ON member_organizations(organization_id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
73
scripts/migrate_align_company_site_member_system.sql
Normal file
73
scripts/migrate_align_company_site_member_system.sql
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'record_status') THEN
|
||||||
|
CREATE TYPE record_status AS ENUM ('active','inactive');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS systems (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
system_key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO systems (system_key, name, status)
|
||||||
|
VALUES ('member', 'Member Center', 'active')
|
||||||
|
ON CONFLICT (system_key) DO NOTHING;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'scope_type') THEN
|
||||||
|
CREATE TYPE scope_type AS ENUM ('company','site');
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'permission_action') THEN
|
||||||
|
CREATE TYPE permission_action AS ENUM ('view','create','update','delete','manage');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permission_groups (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
group_key TEXT NOT NULL UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
status record_status NOT NULL DEFAULT 'active',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permission_group_members (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||||
|
authentik_sub TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT uq_permission_group_members_group_sub UNIQUE (group_id, authentik_sub)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS permission_group_permissions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
group_id UUID NOT NULL REFERENCES permission_groups(id) ON DELETE CASCADE,
|
||||||
|
system TEXT NOT NULL,
|
||||||
|
module TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
scope_type TEXT NOT NULL,
|
||||||
|
scope_id TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_systems_system_key ON systems(system_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pgm_group_id ON permission_group_members(group_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pgm_authentik_sub ON permission_group_members(authentik_sub);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_pgp_group_id ON permission_group_permissions(group_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_pgp_group_rule
|
||||||
|
ON permission_group_permissions(group_id, system, module, action, scope_type, scope_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user