cloud-api/src/organisation/router.py
luxferre 23f2ce98d7 feat: iam rbac system
Endpoints and db architecture to support a role based IAM system.
2026-05-25 09:05:17 +01:00

207 lines
7.5 KiB
Python

"""
Router endpoints for organisation module
Endpoints:
- [get]/id/{org_id} - Retrieves an organisation by its ID
- [post]/ - Creates a new organisation
- [patch]/{org_id}/questionnaire - Updates the questionnaire data for an organisation (can be partial or final submission)
- [patch]/{org_id}/status - Updates the status of an organisation
- [patch]/{org_id}/contact - Assigns a contact to an organisation (as billing, security, or owner)
- [get]/{org_id}/users - Retrieves all users associated with an organisation
- [post]/{org_id}/users - Adds a new user to an organisation
- [delete]/{org_id} - Deletes an organisation by ID
- [get]/{org_id}/contact/{contact_type} - Retrieves the contact of a specific type (owner, billing, security) for an organisation
"""
from typing import Annotated
from fastapi import APIRouter, HTTPException, status
from fastapi.params import Path, Query
from sqlalchemy.sql import exists
from src.database import db_dependency
from src.contact.models import Contact
from src.iam.models import Group
from src.organisation.dependencies import org_model_dependency
from src.organisation.constants import ContactType
from src.organisation.models import Organisation as Org, OrgUsers
from src.organisation.schemas import OrgOrgPostRequest, OrgQuestionnairePatchRequest, OrgStatusPatchRequest, \
OrgContactPatchRequest, \
OrgUserPostRequest, OrgUserGetResponse, OrgContactGetResponse, OrgOrgGetResponse
router = APIRouter(
prefix="/org",
tags=["org"],
)
@router.get("/id/{org_id}", response_model=OrgOrgGetResponse)
async def get_org_by_id(db: db_dependency, org_id: Annotated[int, Path(gt=0)]):
org_model = (db.query(Org).filter(Org.id == org_id).first())
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
response = {
"name": org_model.name,
"status": org_model.status,
"owner_contact": (db.query(Contact).filter(Contact.id == org_model.owner_contact_id).first()),
"billing_contact": (db.query(Contact).filter(Contact.id == org_model.billing_contact_id).first()),
"security_contact": (db.query(Contact).filter(Contact.id == org_model.security_contact_id).first()),
}
return response
@router.post("/")
async def create_org(db: db_dependency, org_request: OrgOrgPostRequest):
org_model = Org(**org_request.model_dump())
org_model.status = "partial" # Status is always set to partial at first, see update_questionnaire() doc
db.add(org_model)
db.commit()
@router.patch("/{org_id}/questionnaire")
async def update_questionnaire(db: db_dependency, q_request: OrgQuestionnairePatchRequest, org_id: Annotated[int, Path(gt=0)]):
"""
Route for updating questionnaire.
The partial bool allows for submission of partially completed questionnaire and/or
final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval.
"""
org_model = db.query(Org).filter(Org.id == org_id).first()
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
org_model.intake_questionnaire = q_request.intake_questionnaire
# Allows for partially completed questionnaires to be saved without being submitted for review
if not q_request.partial:
org_model.status = "submitted"
db.add(org_model)
db.commit()
@router.patch("/{org_id}/status")
async def update_status(db: db_dependency, status_request: OrgStatusPatchRequest, org_id: Annotated[int, Path(gt=0)]):
org_model = db.query(Org).filter(Org.id == org_id).first()
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
org_model.status = status_request.status
db.add(org_model)
db.commit()
@router.patch("/{org_id}/contact")
async def update_contact(db: db_dependency, contact_request: OrgContactPatchRequest, org_id: Annotated[int, Path(gt=0)]):
org_model = db.query(Org).filter(Org.id == org_id).first()
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
match contact_request.contact_type:
case "billing":
org_model.billing_contact_id = contact_request.contact_id
case "security":
org_model.security_contact_id = contact_request.contact_id
case "owner":
org_model.owner_contact_id = contact_request.contact_id
case _:
raise HTTPException(status_code=422, detail="Invalid contact type")
db.add(org_model)
db.commit()
@router.get("/{org_id}/users", response_model=list[OrgUserGetResponse])
async def get_users(db: db_dependency, org_id: Annotated[int, Path(gt=0)]):
org_exists = db.query(exists().where(Org.id == org_id)).scalar()
if not org_exists:
raise HTTPException(status_code=404, detail="Organisation not found")
org_user_models = db.query(OrgUsers).filter(OrgUsers.org_id == org_id).all()
return org_user_models
@router.post("/{org_id}/users")
async def add_user_to_org(db: db_dependency, user_request: OrgUserPostRequest, org_id: Annotated[int, Path(gt=0)]):
org_model = (db.query(Org).filter(Org.id == org_id).first())
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
org_user_model = OrgUsers(**user_request.model_dump(), org_id=org_id)
db.add(org_user_model)
db.commit()
@router.delete("/{org_id}")
async def delete_organisation_by_id(db: db_dependency, org_id: Annotated[int, Path(gt=0)]):
org_model = (db.query(Org).filter(Org.id == org_id).first())
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
db.delete(org_model)
db.commit()
@router.get("/{org_id}/contact/{contact_type}", response_model=OrgContactGetResponse)
async def get_contact(db: db_dependency, contact_type: ContactType, org_id: Annotated[int, Path(gt=0)]):
org_model = db.query(Org).filter(Org.id == org_id).first()
if org_model is None:
raise HTTPException(status_code=404, detail="Organisation not found")
match contact_type:
case "billing":
contact_id = org_model.billing_contact_id
case "security":
contact_id = org_model.security_contact_id
case "owner":
contact_id = org_model.owner_contact_id
case _:
raise HTTPException(status_code=422, detail="Invalid contact type")
contact_model = (db.query(Contact).filter(Contact.id == contact_id).first())
if contact_model is None:
raise HTTPException(status_code=404, detail="Contact not found")
return contact_model
@router.get("/{org_id}/root_user")
async def get_org_root_user(db: db_dependency, org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)]):
root_user = org_model.root_user_id
return {"root_user": root_user}
@router.patch("/{org_id}/root_user")
async def update_root_user(db: db_dependency, org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)], root_user: Annotated[int, Query(gt=0)]):
# TODO: Request model, ditch query
# TODO: Verify root_user exists, possibly with a user_model_dependency
org_model.root_user_id = root_user
db.add(org_model)
db.commit()
# TODO: Response model
@router.get("/{org_id}/groups")
async def get_org_groups(db: db_dependency, org_id: Annotated[int, Path(gt=0)]):
org_group_models = db.query(Group).filter(Group.org_id == org_id).all()
# TODO: Response model
return org_group_models
@router.delete("/{org_id}/user")
async def remove_user_from_org(db: db_dependency, org_model: org_model_dependency, org_id: Annotated[int, Path(gt=0)], user_id: Annotated[int, Query(gt=0)]):
orguser_model = db.query(OrgUsers).filter(OrgUsers.org_id == org_id, OrgUsers.user_id == user_id).first()
if orguser_model is None:
raise HTTPException(status_code=status.HTTP_204_NO_CONTENT)
db.delete(orguser_model)
db.commit()
pass