cloud-api/src/service/router.py
luxferre 58e7ae6c5c
All checks were successful
ci / ruff (push) Successful in 3s
ci / ty (push) Successful in 15s
ci / tests (push) Successful in 17s
fix: ty compliant & issues from change to mapped columns
2026-06-22 11:23:24 +01:00

236 lines
6.2 KiB
Python

"""
Router endpoints for the services module
Endpoints:
- [GET](/): [root user]: Get a list of all services(id, name)
- [POST](/): [super admin]: Register a new service(name) on the hub, returns the API key for the service to access the hub.
- [PATCH](/key): [super_admin]: Refreshes the API key for a service(id), returning a new one.
- [DELETE](/): [super_admin]: Removes a service(id) from the hub.
"""
from fastapi import APIRouter, status
from sqlalchemy.exc import IntegrityError
from psycopg.errors import UniqueViolation
from src.exceptions import ConflictException
from src.database import db_dependency
from src.auth.dependencies import (
super_admin_dependency,
org_model_root_claim_query_dependency,
)
from src.iam.service import service_key_dependency
from src.iam.models import Permission as Perm
from src.service.exceptions import ServiceNotFoundException
from src.service.models import Service
from src.service.utils import generate_api_key
from src.service.dependencies import (
service_model_body_dependency,
service_model_query_dependency,
)
from src.service.schemas import (
ServiceGetServiceResponse,
ServicePostServiceRequest,
ServicePostServiceResponse,
ServiceWithKeySchema,
ServicePatchKeyResponse,
ServicePatchKeyRequest,
ServicePostPermissionsResponse,
ServicePostPermissionsRequest,
)
router = APIRouter(
tags=["Service"],
prefix="/service",
)
@router.get(
"",
summary="Get all services",
status_code=status.HTTP_200_OK,
response_model=ServiceGetServiceResponse,
responses={
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
status.HTTP_401_UNAUTHORIZED: {
"description": "Unauthorized",
"content": {
"application/json": {
"examples": {
"awaiting_approval": {
"summary": "Organisation has not yet been approved."
},
}
}
},
},
status.HTTP_403_FORBIDDEN: {
"description": "Forbidden",
"content": {
"application/json": {
"examples": {
"not_root": {"summary": "Not authorised. Must be root user."},
}
}
},
},
},
)
async def get_all_services(
db: db_dependency, org_model: org_model_root_claim_query_dependency
):
"""
Returns the ID and name of all services registered to the hub.
"""
permission_models = db.query(Service).all()
return {"services": permission_models}
@router.post(
"",
summary="Register a new service.",
status_code=status.HTTP_200_OK,
response_model=ServicePostServiceResponse,
responses={
status.HTTP_200_OK: {"description": "Successfully registered a new service"},
status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"},
status.HTTP_409_CONFLICT: {"description": "Service with this name already exists"},
},
)
async def register_service(
db: db_dependency,
su: super_admin_dependency,
request_model: ServicePostServiceRequest,
):
"""
Registers a new service to the hub, generating and returning an API key for it.
"""
key = generate_api_key()
service_model = Service(name=request_model.name, api_key=key)
db.add(service_model)
try:
db.flush()
except IntegrityError as e:
if (
isinstance(e.orig, UniqueViolation) # Postgres unique violation
or "UNIQUE constraint failed" in str(e.orig) # SQLite unique violation
):
raise ConflictException(message="Service with this name already exists")
raise
response = ServiceWithKeySchema(**service_model.__dict__)
db.commit()
return {"service": response}
@router.patch(
"/key",
summary="Regenerate service API key.",
status_code=status.HTTP_200_OK,
response_model=ServicePatchKeyResponse,
responses={
status.HTTP_200_OK: {"description": "Successful update of API key"},
status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"},
},
)
async def regenerate_api_key(
db: db_dependency,
su: super_admin_dependency,
service_model: service_model_body_dependency,
request_model: ServicePatchKeyRequest,
):
"""
Generates and returns a new API key for the service to access the hub.
"""
key = generate_api_key()
service_model.api_key = key
db.flush()
response = ServiceWithKeySchema(**service_model.__dict__)
db.commit()
return {"service": response}
@router.delete(
"",
summary="Remove a service.",
status_code=status.HTTP_204_NO_CONTENT,
responses={
status.HTTP_204_NO_CONTENT: {"description": "Successfully removed service from db"},
status.HTTP_401_UNAUTHORIZED: {"description": "Unauthorized"},
},
)
async def remove_service(
db: db_dependency,
service_model: service_model_query_dependency,
su: super_admin_dependency,
):
"""
Removes a service from the hub.
"""
db.delete(service_model)
db.commit()
@router.post(
path="/permissions",
summary="Service endpoint for creating its own permissions.",
status_code=status.HTTP_200_OK,
response_model=ServicePostPermissionsResponse,
responses={
status.HTTP_401_UNAUTHORIZED: {
"description": "API Key missing or invalid | Issue verifying user OIDC claims"
},
},
)
async def service_create_new_permissions(
db: db_dependency,
request_model: ServicePostPermissionsRequest,
valid_key: service_key_dependency,
):
"""
Allows a service to register its own set of permissions.
"""
service_model = (
db.query(Service).filter(Service.name == request_model.rn.service).first()
)
if service_model is None:
raise ServiceNotFoundException()
else:
service_id = service_model.id
response_list = []
for new_permission in request_model.permissions:
perm_model = (
db.query(Perm)
.filter(Perm.service_id == service_id)
.filter(Perm.resource == new_permission.resource)
.filter(Perm.action == new_permission.action)
.first()
)
if perm_model is not None:
response_code = 409
response = {
"id": perm_model.id,
"service_name": perm_model.service_name,
"resource": perm_model.resource,
"action": perm_model.action,
}
response_list.append((response, response_code))
continue
new_perm_model = Perm(**new_permission.__dict__)
new_perm_model.service_id = service_id
db.add(new_perm_model)
db.flush()
response_code = 201
response = {
"id": new_perm_model.id,
"service_name": new_perm_model.service_name,
"resource": new_perm_model.resource,
"action": new_perm_model.action,
}
response_list.append((response, response_code))
db.commit()
return {"permissions": response_list}