forked from sr2/cloud-api
feat: iam rbac system
Endpoints and db architecture to support a role based IAM system.
This commit is contained in:
parent
7b3ee9d5fa
commit
23f2ce98d7
31 changed files with 634 additions and 317 deletions
|
|
@ -10,6 +10,8 @@ from src.config import SQLALCHEMY_DATABASE_URI
|
|||
from src.contact.models import Contact
|
||||
from src.organisation.models import Organisation, OrgUsers
|
||||
from src.user.models import User
|
||||
from src.service.models import Service
|
||||
from src.iam.models import Permission, Group, GroupPermissions, UserGroups
|
||||
from src.database import Base
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
|
|
|
|||
83
.alembic/versions/2026-05-22_init_iam.py
Normal file
83
.alembic/versions/2026-05-22_init_iam.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"""Init IAM
|
||||
|
||||
Revision ID: a147965e644e
|
||||
Revises: 8fe51426321d
|
||||
Create Date: 2026-05-22 15:59:36.469374
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a147965e644e'
|
||||
down_revision: Union[str, Sequence[str], None] = '8fe51426321d'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('service',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=True),
|
||||
sa.Column('api_key', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('api_key'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('permission',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('resource', sa.String(), nullable=False),
|
||||
sa.Column('action', sa.String(), nullable=False),
|
||||
sa.Column('service_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['service_id'], ['service.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('group',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('org_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['org_id'], ['organisation.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('group_permissions',
|
||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||
sa.Column('permission_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['permission_id'], ['permission.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('group_id', 'permission_id')
|
||||
)
|
||||
op.create_table('user_groups',
|
||||
sa.Column('org_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['org_id'], ['organisation.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('org_id', 'user_id', 'group_id')
|
||||
)
|
||||
op.add_column('organisation', sa.Column('root_user_id', sa.Integer(), nullable=True))
|
||||
op.create_unique_constraint("organisation_name_key", 'organisation', ['name'])
|
||||
op.create_foreign_key("organisation_root_user_fkey", 'organisation', 'user', ['root_user_id'], ['id'])
|
||||
op.drop_column('orgusers', 'is_admin')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('orgusers', sa.Column('is_admin', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
|
||||
op.drop_constraint("organisation_root_user_fkey", 'organisation', type_='foreignkey')
|
||||
op.drop_constraint("organisation_name_key", 'organisation', type_='unique')
|
||||
op.drop_column('organisation', 'root_user_id')
|
||||
op.drop_table('user_groups')
|
||||
op.drop_table('group_permissions')
|
||||
op.drop_table('group')
|
||||
op.drop_table('permission')
|
||||
op.drop_table('service')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -5,43 +5,9 @@ Endpoints:
|
|||
- List: Description
|
||||
- Endpoints: Description
|
||||
"""
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.params import Path
|
||||
|
||||
from src.organisation.constants import ContactType
|
||||
from src.organisation.schemas import OrgContactGetResponse
|
||||
from src.organisation.models import Organisation as Org
|
||||
from src.contact.models import Contact
|
||||
|
||||
from src.auth.service import claims_dependency, org_or_super_admin_dependency
|
||||
from src.database import db_dependency
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(
|
||||
tags=["admin"],
|
||||
prefix="/admin",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{org_id}/contact/{contact_type}", response_model=OrgContactGetResponse)
|
||||
async def get_contact(db: db_dependency, user: claims_dependency, is_admin: org_or_super_admin_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
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ from src.contact.router import router as contact_router
|
|||
from src.organisation.router import router as organisation_router
|
||||
from src.user.router import router as user_router
|
||||
from src.admin.router import router as admin_router
|
||||
from src.iam.router import router as iam_router
|
||||
from src.service.router import router as service_router
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
|
|
@ -17,6 +19,8 @@ api_router.include_router(contact_router)
|
|||
api_router.include_router(organisation_router)
|
||||
api_router.include_router(user_router)
|
||||
api_router.include_router(admin_router)
|
||||
api_router.include_router(service_router)
|
||||
api_router.include_router(iam_router)
|
||||
|
||||
|
||||
@api_router.get("/healthcheck", include_in_schema=False)
|
||||
|
|
|
|||
|
|
@ -8,54 +8,4 @@ from fastapi import APIRouter
|
|||
|
||||
router = APIRouter(
|
||||
tags=["auth"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
# oauth = OAuth()
|
||||
# oauth.register(
|
||||
# name="oidc",
|
||||
# server_metadata_url=auth_settings.OIDC_CONFIG,
|
||||
# client_id=auth_settings.CLIENT_ID,
|
||||
# client_secret=None,
|
||||
# code_challenge_method="S256",
|
||||
# client_kwargs={
|
||||
# "code_challenge_method": "S256",
|
||||
# "scope": "openid profile email",
|
||||
# }
|
||||
# )
|
||||
|
||||
|
||||
# @auth_router.get('/login')
|
||||
# async def login(request: Request):
|
||||
# redirect_uri = request.url_for('auth')
|
||||
# return await oauth.oidc.authorize_redirect(request, redirect_uri, code_challenge_method="S256")
|
||||
#
|
||||
#
|
||||
# @auth_router.get('/auth', include_in_schema=False)
|
||||
# async def auth(db: db_dependency, request: Request):
|
||||
# token = await oauth.oidc.authorize_access_token(request)
|
||||
# user = token.get("userinfo")
|
||||
# request.session["user"] = user
|
||||
#
|
||||
# try:
|
||||
# valid_user = OIDCUser(first_name=user["given_name"], last_name=user["family_name"], email=user["email"], oidc_id=user["sub"])
|
||||
# except Exception as e:
|
||||
# print(e)
|
||||
# raise HTTPException(status_code=422, detail="Invalid or missing OIDC data")
|
||||
#
|
||||
# user_exists = db.query(exists().where(User.oidc_id == valid_user.oidc_id)).scalar()
|
||||
#
|
||||
# if not user_exists:
|
||||
# user_model = User(**valid_user.model_dump())
|
||||
# db.add(user_model)
|
||||
# db.commit()
|
||||
#
|
||||
# return RedirectResponse(url="/")
|
||||
#
|
||||
#
|
||||
# @auth_router.get('/logout')
|
||||
# async def logout(request: Request):
|
||||
# request.session.pop('user', None)
|
||||
# return RedirectResponse(url='/')
|
||||
)
|
||||
|
|
@ -88,33 +88,6 @@ async def is_org_user(claims: claims_dependency, db: db_dependency, org_id: int
|
|||
org_user_dependency = Annotated[dict[str, Any], Depends(is_org_user)]
|
||||
|
||||
|
||||
async def is_org_admin(claims: claims_dependency, db: db_dependency, org_id: 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")
|
||||
|
||||
db_id = claims.get("db_id", None)
|
||||
if db_id is None:
|
||||
raise HTTPException(status_code=404, detail="User not found in db")
|
||||
|
||||
exists_query = (db.query(OrgUsers)
|
||||
.filter(OrgUsers.org_id == org_id,
|
||||
OrgUsers.user_id == db_id,
|
||||
OrgUsers.is_admin == True
|
||||
).exists()
|
||||
)
|
||||
|
||||
org_admin_exists = db.query(exists_query).scalar()
|
||||
|
||||
if not org_admin_exists:
|
||||
raise HTTPException(status_code=401, detail="Not authorised")
|
||||
|
||||
return org_admin_exists
|
||||
|
||||
|
||||
org_admin_dependency = Annotated[dict[str, Any], Depends(is_org_admin)]
|
||||
|
||||
|
||||
async def is_super_admin(claims: claims_dependency):
|
||||
super_admin_ids = []
|
||||
|
||||
|
|
@ -128,114 +101,3 @@ async def is_super_admin(claims: claims_dependency):
|
|||
|
||||
|
||||
super_admin_dependency = Annotated[dict[str, Any], Depends(is_super_admin)]
|
||||
|
||||
|
||||
async def is_admin(claims: claims_dependency, db: db_dependency, org_id: int = Path(gt=0)):
|
||||
try:
|
||||
await is_super_admin(claims)
|
||||
return True
|
||||
except HTTPException as e:
|
||||
pass
|
||||
try:
|
||||
await is_org_admin(claims, db, org_id)
|
||||
return True
|
||||
except HTTPException as e:
|
||||
raise HTTPException(status_code=401, detail="Not authorised")
|
||||
|
||||
org_or_super_admin_dependency = Annotated[dict[str, Any], Depends(is_admin)]
|
||||
|
||||
# Middleware version of user auth
|
||||
# import json
|
||||
# import logging
|
||||
#
|
||||
# from threading import Timer
|
||||
# from urllib.request import urlopen
|
||||
# from starlette.requests import HTTPConnection, Request
|
||||
#
|
||||
# from authlib.jose.rfc7517.jwk import JsonWebKey
|
||||
# from authlib.jose.rfc7517.key_set import KeySet
|
||||
# from authlib.oauth2 import OAuth2Error, ResourceProtector
|
||||
# from authlib.oauth2.rfc6749 import MissingAuthorizationError
|
||||
# from authlib.oauth2.rfc7523 import JWTBearerTokenValidator
|
||||
# from authlib.oauth2.rfc7523.validator import JWTBearerToken
|
||||
#
|
||||
# from starlette.authentication import (
|
||||
# AuthCredentials,
|
||||
# AuthenticationBackend,
|
||||
# AuthenticationError,
|
||||
# SimpleUser,
|
||||
# )
|
||||
#
|
||||
# logger = logging.getLogger(__name__)
|
||||
#
|
||||
#
|
||||
# class RepeatTimer(Timer):
|
||||
# def __init__(self, *args, **kwargs) -> None:
|
||||
# super().__init__(*args, **kwargs)
|
||||
# self.daemon = True
|
||||
#
|
||||
# def run(self):
|
||||
# while not self.finished.wait(self.interval):
|
||||
# self.function(*self.args, **self.kwargs)
|
||||
#
|
||||
#
|
||||
# class BearerTokenValidator(JWTBearerTokenValidator):
|
||||
# def __init__(self, issuer: str, audience: str):
|
||||
# self._issuer = issuer
|
||||
# self._jwks_uri: str | None = None
|
||||
# super().__init__(public_key=self.fetch_key(), issuer=issuer)
|
||||
# self.claims_options = {
|
||||
# "exp": {"essential": True},
|
||||
# "aud": {"essential": True, "value": audience},
|
||||
# "iss": {"essential": True, "value": issuer},
|
||||
# }
|
||||
# self._timer = RepeatTimer(3600, self.refresh)
|
||||
# self._timer.start()
|
||||
#
|
||||
# def refresh(self):
|
||||
# try:
|
||||
# self.public_key = self.fetch_key()
|
||||
# except Exception as exc:
|
||||
# logger.warning(f"Could not update jwks public key: {exc}")
|
||||
#
|
||||
# def fetch_key(self) -> KeySet:
|
||||
# """Fetch the jwks_uri document and return the KeySet."""
|
||||
# response = urlopen(self.jwks_uri)
|
||||
# logger.debug(f"OK GET {self.jwks_uri}")
|
||||
# return JsonWebKey.import_key_set(json.loads(response.read()))
|
||||
#
|
||||
# @property
|
||||
# def jwks_uri(self) -> str:
|
||||
# """The jwks_uri field of the openid-configuration document."""
|
||||
# if self._jwks_uri is None:
|
||||
# config_url = urlopen(f"{self._issuer}/.well-known/openid-configuration")
|
||||
# config = json.loads(config_url.read())
|
||||
# self._jwks_uri = config["jwks_uri"]
|
||||
# return self._jwks_uri
|
||||
#
|
||||
#
|
||||
# class BearerTokenAuthBackend(AuthenticationBackend):
|
||||
# def __init__(self, issuer: str, audience: str) -> None:
|
||||
# rp = ResourceProtector()
|
||||
# validator = BearerTokenValidator(
|
||||
# issuer=issuer,
|
||||
# audience=audience,
|
||||
# )
|
||||
# rp.register_token_validator(validator)
|
||||
# self.resource_protector = rp
|
||||
#
|
||||
# async def authenticate(self, conn: HTTPConnection):
|
||||
# if "Authorization" not in conn.headers:
|
||||
# return
|
||||
# request = Request(conn.scope)
|
||||
# try:
|
||||
# token: JWTBearerToken = self.resource_protector.validate_request(
|
||||
# scopes=["openid"],
|
||||
# request=request,
|
||||
# )
|
||||
# except (MissingAuthorizationError, OAuth2Error) as error:
|
||||
# raise AuthenticationError(error.description) from error
|
||||
# scope: str = token.get_scope()
|
||||
# scopes = scope.split()
|
||||
# scopes.append("authenticated")
|
||||
# return AuthCredentials(scopes=scopes), SimpleUser(username=token["email"])
|
||||
|
|
|
|||
7
src/iam/config.py
Normal file
7
src/iam/config.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Configurations for <this module>
|
||||
|
||||
Configurations:
|
||||
- List: Description
|
||||
- Configs: Description
|
||||
"""
|
||||
7
src/iam/constants.py
Normal file
7
src/iam/constants.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Constants and error codes for <this module>
|
||||
|
||||
Constants:
|
||||
- List: Description
|
||||
- Consts: Description
|
||||
"""
|
||||
11
src/iam/dependencies.py
Normal file
11
src/iam/dependencies.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
Router dependencies for <this module>
|
||||
|
||||
Classes:
|
||||
- List: Description
|
||||
- Classes: Description
|
||||
|
||||
Functions:
|
||||
- List: Description
|
||||
- Functions: Description
|
||||
"""
|
||||
7
src/iam/exceptions.py
Normal file
7
src/iam/exceptions.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Module specific exceptions for <this module>
|
||||
|
||||
Exceptions:
|
||||
- List: Description
|
||||
- Exceptions: Description
|
||||
"""
|
||||
43
src/iam/models.py
Normal file
43
src/iam/models.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
Database models for the IAM module
|
||||
|
||||
Models:
|
||||
- List: Description
|
||||
- Models: Description
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
|
||||
|
||||
from src.database import Base
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
__tablename__ = "permission"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
resource = Column(String, nullable=False)
|
||||
action = Column(String, nullable=False)
|
||||
|
||||
service_id = Column(Integer, ForeignKey("service.id", ondelete="CASCADE"))
|
||||
|
||||
UniqueConstraint("service_id", "resource", "action", name="uniq_permission_resource_and_action")
|
||||
|
||||
|
||||
class Group(Base):
|
||||
__tablename__ = "group"
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, nullable=False, unique=True)
|
||||
|
||||
org_id = Column(Integer, ForeignKey("organisation.id", ondelete="CASCADE"))
|
||||
|
||||
|
||||
class GroupPermissions(Base):
|
||||
__tablename__ = "group_permissions"
|
||||
group_id = Column(Integer, ForeignKey("group.id", ondelete="CASCADE"), primary_key=True)
|
||||
permission_id = Column(Integer, ForeignKey("permission.id", ondelete="CASCADE"), primary_key=True)
|
||||
|
||||
|
||||
class UserGroups(Base):
|
||||
__tablename__ = "user_groups"
|
||||
org_id = Column(Integer, ForeignKey("organisation.id", ondelete="CASCADE"), primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
|
||||
group_id = Column(Integer, ForeignKey("group.id", ondelete="CASCADE"), primary_key=True)
|
||||
192
src/iam/router.py
Normal file
192
src/iam/router.py
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
"""
|
||||
Router endpoints for <this module>
|
||||
|
||||
Endpoints:
|
||||
- List: Description
|
||||
- Endpoints: Description
|
||||
"""
|
||||
from typing import Annotated, Optional
|
||||
|
||||
from fastapi import APIRouter, Query, HTTPException
|
||||
|
||||
from src.database import db_dependency
|
||||
from src.schemas import ResourceName
|
||||
from src.auth.service import claims_dependency
|
||||
from src.user.models import User
|
||||
from src.organisation.models import Organisation as Org
|
||||
from src.service.models import Service
|
||||
from src.organisation.dependencies import org_model_dependency
|
||||
|
||||
from src.iam.service import service_key_dependency
|
||||
from src.iam.models import Permission as Perm, GroupPermissions as GPerms, Group, UserGroups
|
||||
|
||||
router = APIRouter(
|
||||
tags=["IAM"],
|
||||
prefix="/iam",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/can_act_on_resource")
|
||||
async def can_act_on_resource(valid_key: service_key_dependency, db: db_dependency, user_claims: claims_dependency,
|
||||
rn: ResourceName, action: str) -> bool:
|
||||
try:
|
||||
user_id = user_claims["db_id"]
|
||||
rn_org = rn.organisation
|
||||
rn_service = rn.service
|
||||
rn_resource = rn.resource
|
||||
|
||||
result = (db.query(Perm)
|
||||
.join(Service, Service.id == Perm.service_id)
|
||||
.join(GPerms, GPerms.permission_id == Perm.id)
|
||||
.join(Group, Group.id == GPerms.group_id)
|
||||
.join(Org, Org.id == Group.org_id)
|
||||
.join(UserGroups, UserGroups.group_id == Group.id)
|
||||
.join(User, User.id == UserGroups.user_id)
|
||||
.filter(User.id == user_id)
|
||||
.filter(Org.name == rn_org)
|
||||
.filter(Service.name == rn_service)
|
||||
.filter(Perm.resource == rn_resource)
|
||||
.filter(Perm.action == action)
|
||||
).first()
|
||||
|
||||
if result:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/group/permissions")
|
||||
async def get_group_permissions(db: db_dependency, group_id: Annotated[int, Query(gt=0)]):
|
||||
# TODO: iam_admin_dependency
|
||||
group_perms = db.query(Perm).join(GPerms).filter(GPerms.group_id==group_id).all()
|
||||
|
||||
# TODO: Response model
|
||||
return group_perms
|
||||
|
||||
|
||||
@router.get("/group/users")
|
||||
async def get_group_users(db: db_dependency, group_id: Annotated[int, Query(gt=0)]):
|
||||
# TODO: iam_admin_dependency
|
||||
group_users = db.query(User).join(UserGroups).filter(UserGroups.group_id == group_id).all()
|
||||
|
||||
# TODO: Response model
|
||||
return group_users
|
||||
|
||||
|
||||
@router.post("/group")
|
||||
async def create_group(db: db_dependency, group_name: str, org_model: org_model_dependency, org_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: Request model
|
||||
group_model = Group(name=group_name, org_id=org_id)
|
||||
|
||||
db.add(group_model)
|
||||
db.commit()
|
||||
# TODO: Response model
|
||||
|
||||
|
||||
@router.put("/group/permissions")
|
||||
async def add_group_permissions(db: db_dependency, group_id: int, permission_id: int, org_model: org_model_dependency, org_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: Request model
|
||||
g_perm_model = GPerms(group_id=group_id, permission_id=permission_id)
|
||||
|
||||
db.add(g_perm_model)
|
||||
db.commit()
|
||||
# TODO: Response model
|
||||
|
||||
|
||||
@router.put("/group/users")
|
||||
async def add_group_users(db: db_dependency, group_id: int, user_ids: list[int], org_model: org_model_dependency, org_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: Request model
|
||||
for user_id in user_ids:
|
||||
user_group_model = UserGroups(group_id=group_id, user_id=user_id, org_id=org_id)
|
||||
db.add(user_group_model)
|
||||
|
||||
db.commit()
|
||||
# TODO: Response model
|
||||
|
||||
|
||||
@router.delete("/group/permissions")
|
||||
async def remove_group_permissions(db: db_dependency, group_id: int, org_model: org_model_dependency, org_id: int, permission_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: Request model
|
||||
g_perm_model = db.query(GPerms).filter(GPerms.group_id == group_id, GPerms.permission_id == permission_id).first()
|
||||
if g_perm_model is None:
|
||||
return
|
||||
|
||||
db.delete(g_perm_model)
|
||||
db.commit()
|
||||
return
|
||||
# TODO: Response model
|
||||
|
||||
|
||||
@router.delete("/group/user")
|
||||
async def remove_group_user(db: db_dependency, group_id: int, user_id: int, org_model: org_model_dependency, org_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: Request model
|
||||
user_group_model = db.query(UserGroups).filter(UserGroups.group_id == group_id, UserGroups.user_id == user_id).first()
|
||||
if user_group_model is None:
|
||||
return
|
||||
|
||||
db.delete(user_group_model)
|
||||
db.commit()
|
||||
return
|
||||
# TODO: Response model
|
||||
|
||||
|
||||
@router.get("/permissions")
|
||||
async def get_permissions(db: db_dependency, org_model: org_model_dependency, org_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: request model
|
||||
permission_models = db.query(Perm).all()
|
||||
|
||||
# TODO: Response model
|
||||
return permission_models
|
||||
|
||||
|
||||
@router.post("/permission")
|
||||
async def create_new_permission(db: db_dependency, service_id: int, resource: str, action: str):
|
||||
# TODO: super_admin_dependency
|
||||
perm_model = Perm(service_id=service_id, resource=resource, action=action)
|
||||
|
||||
db.add(perm_model)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.delete("/permission")
|
||||
async def delete_permission(db: db_dependency, service_id: int, resource: str, action: str, org_model: org_model_dependency, org_id: int):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: Request model
|
||||
perm_model = db.query(Perm).filter(Perm.service_id==service_id, Perm.resource==resource, Perm.action==action).first()
|
||||
if perm_model is None:
|
||||
return
|
||||
|
||||
db.delete(perm_model)
|
||||
db.commit()
|
||||
return
|
||||
# TODO: Response model
|
||||
|
||||
|
||||
@router.get("/permissions/search")
|
||||
async def get_permissions(db: db_dependency, org_model: org_model_dependency, org_id: int, service_id: Optional[int] = None, resource: Optional[str] = None, action: Optional[str] = None):
|
||||
# TODO: iam_admin_dependency
|
||||
# TODO: request model
|
||||
permission_query = db.query(Perm)
|
||||
|
||||
if service_id is not None:
|
||||
permission_query = permission_query.filter(Perm.service_id == service_id)
|
||||
|
||||
if resource is not None:
|
||||
permission_query = permission_query.filter(Perm.resource == resource)
|
||||
|
||||
if action is not None:
|
||||
permission_query = permission_query.filter(Perm.action == action)
|
||||
|
||||
permission_models = permission_query.all()
|
||||
|
||||
# TODO: Response model
|
||||
return permission_models
|
||||
7
src/iam/schemas.py
Normal file
7
src/iam/schemas.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Pydantic models for <this module>
|
||||
|
||||
Models:
|
||||
- List: Description
|
||||
- Models: Description
|
||||
"""
|
||||
26
src/iam/service.py
Normal file
26
src/iam/service.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""
|
||||
Module specific business logic for <this module>
|
||||
|
||||
Exports service_key_dependency
|
||||
"""
|
||||
from typing import Annotated
|
||||
|
||||
from src.service.models import Service
|
||||
from src.database import db_dependency
|
||||
from src.schemas import ResourceName
|
||||
|
||||
from fastapi import HTTPException, status, Request, Depends
|
||||
|
||||
|
||||
def valid_service_key(db: db_dependency, request: Request, rn: ResourceName) -> bool:
|
||||
api_key = request.headers.get("X-API-Key", None)
|
||||
if not api_key:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
service = rn.service
|
||||
result = db.query(Service).filter(Service.name == service).filter(Service.api_key == api_key).first()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
return True
|
||||
|
||||
service_key_dependency = Annotated[bool, Depends(valid_service_key)]
|
||||
11
src/iam/utils.py
Normal file
11
src/iam/utils.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
Non-business logic reusable functions and classes for <this module>
|
||||
|
||||
Classes:
|
||||
- List: Description
|
||||
- Classes: Description
|
||||
|
||||
Functions:
|
||||
- List: Description
|
||||
- Functions: Description
|
||||
"""
|
||||
|
|
@ -8,4 +8,21 @@ Classes:
|
|||
Functions:
|
||||
- List: Description
|
||||
- Functions: Description
|
||||
"""
|
||||
"""
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import HTTPException, Depends
|
||||
|
||||
from src.database import db_dependency
|
||||
|
||||
from src.organisation.models import Organisation as Org
|
||||
|
||||
|
||||
def get_org_model(db: db_dependency, org_id: int) -> type[Org]:
|
||||
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")
|
||||
|
||||
return org_model
|
||||
|
||||
org_model_dependency = Annotated[type[Org], Depends(get_org_model)]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Models:
|
|||
billing_contact_id[fk], security_contact_id[fk], owner_contact_id[fk]
|
||||
- OrgUsers: org_id[fk][cpk], user_id[fk][cpk], is_admin
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, JSON, false
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, JSON
|
||||
|
||||
from src.database import Base
|
||||
|
||||
|
|
@ -15,10 +15,12 @@ class Organisation(Base):
|
|||
__tablename__ = "organisation"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String)
|
||||
name = Column(String, unique=True)
|
||||
status = Column(String, default="partial")
|
||||
intake_questionnaire = Column(JSON)
|
||||
|
||||
root_user_id = Column(Integer, ForeignKey("user.id"))
|
||||
|
||||
billing_contact_id = Column(Integer, ForeignKey("contact.id"))
|
||||
security_contact_id = Column(Integer, ForeignKey("contact.id"))
|
||||
owner_contact_id = Column(Integer, ForeignKey("contact.id"))
|
||||
|
|
@ -29,4 +31,3 @@ class OrgUsers(Base):
|
|||
|
||||
org_id = Column(Integer, ForeignKey("organisation.id", ondelete="CASCADE"), primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
|
||||
is_admin = Column(Boolean, nullable=False, server_default=false())
|
||||
|
|
|
|||
|
|
@ -8,22 +8,22 @@ Endpoints:
|
|||
- [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
|
||||
- [get]/{org_id}/users/admins - Retrieves only the admin users of an organisation
|
||||
- [post]/{org_id}/users - Adds a new user to an organisation
|
||||
- [patch]/{org_id}/users - Updates details of an existing organisation user (e.g., admin status)
|
||||
- [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
|
||||
from fastapi.params import Path
|
||||
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, \
|
||||
|
|
@ -127,17 +127,6 @@ async def get_users(db: db_dependency, org_id: Annotated[int, Path(gt=0)]):
|
|||
return org_user_models
|
||||
|
||||
|
||||
@router.get("/{org_id}/users/admins", response_model=list[OrgUserGetResponse])
|
||||
async def get_admin_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).filter(OrgUsers.is_admin == True).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())
|
||||
|
|
@ -150,27 +139,6 @@ async def add_user_to_org(db: db_dependency, user_request: OrgUserPostRequest, o
|
|||
db.commit()
|
||||
|
||||
|
||||
@router.patch("/{org_id}/users")
|
||||
async def update_user_details(db: db_dependency, user_request: OrgUserPostRequest, org_id: Annotated[int, Path(gt=0)]):
|
||||
"""
|
||||
Currently used only to update user admin status for organisation.
|
||||
"""
|
||||
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 = db.query(OrgUsers).filter(OrgUsers.org_id == org_id).filter(OrgUsers.user_id == user_request.user_id).first()
|
||||
|
||||
if org_user_model is None:
|
||||
raise HTTPException(status_code=404, detail="Organisation user not found")
|
||||
|
||||
if user_request.is_admin is not None:
|
||||
org_user_model.is_admin = user_request.is_admin
|
||||
|
||||
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())
|
||||
|
|
@ -201,3 +169,39 @@ async def get_contact(db: db_dependency, contact_type: ContactType, org_id: Anno
|
|||
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
|
||||
|
|
|
|||
|
|
@ -38,11 +38,9 @@ class OrgContactPatchRequest(CustomBaseModel):
|
|||
|
||||
class OrgUserPostRequest(CustomBaseModel):
|
||||
user_id: int
|
||||
is_admin: Optional[bool] = False
|
||||
|
||||
class OrgUserGetResponse(CustomBaseModel):
|
||||
user_id: int
|
||||
is_admin: bool
|
||||
|
||||
class OrgContactGetResponse(CustomBaseModel):
|
||||
email: str
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CustomBaseModel(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class ResourceName(CustomBaseModel):
|
||||
service: str
|
||||
organisation: str
|
||||
resource: str
|
||||
instance: Optional[str] = None
|
||||
|
|
|
|||
7
src/service/config.py
Normal file
7
src/service/config.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Configurations for <this module>
|
||||
|
||||
Configurations:
|
||||
- List: Description
|
||||
- Configs: Description
|
||||
"""
|
||||
7
src/service/constants.py
Normal file
7
src/service/constants.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Constants and error codes for <this module>
|
||||
|
||||
Constants:
|
||||
- List: Description
|
||||
- Consts: Description
|
||||
"""
|
||||
11
src/service/dependencies.py
Normal file
11
src/service/dependencies.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
Router dependencies for <this module>
|
||||
|
||||
Classes:
|
||||
- List: Description
|
||||
- Classes: Description
|
||||
|
||||
Functions:
|
||||
- List: Description
|
||||
- Functions: Description
|
||||
"""
|
||||
7
src/service/exceptions.py
Normal file
7
src/service/exceptions.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Module specific exceptions for <this module>
|
||||
|
||||
Exceptions:
|
||||
- List: Description
|
||||
- Exceptions: Description
|
||||
"""
|
||||
18
src/service/models.py
Normal file
18
src/service/models.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
"""
|
||||
Database models for the services module
|
||||
|
||||
Models:
|
||||
- List: Description
|
||||
- Models: Description
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String
|
||||
|
||||
from src.database import Base
|
||||
|
||||
|
||||
class Service(Base):
|
||||
__tablename__ = "service"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String, unique=True)
|
||||
api_key = Column(String, unique=True)
|
||||
63
src/service/router.py
Normal file
63
src/service/router.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""
|
||||
Router endpoints for <this module>
|
||||
|
||||
Endpoints:
|
||||
- List: Description
|
||||
- Endpoints: Description
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
|
||||
from src.database import db_dependency
|
||||
|
||||
from src.service.models import Service
|
||||
from src.service.utils import generate_api_key
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
tags=["Service"],
|
||||
prefix="/service",
|
||||
)
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_services(db: db_dependency):
|
||||
# TODO: user_dependency
|
||||
# TODO: request model
|
||||
permission_models = db.query(Service).all()
|
||||
|
||||
# TODO: Response model
|
||||
return permission_models
|
||||
|
||||
@router.post("/")
|
||||
async def register_service(db: db_dependency, service_name: str):
|
||||
# TODO: super_admin_dependency
|
||||
# TODO: request model
|
||||
key = generate_api_key()
|
||||
service_model = Service(name=service_name, api_key=key)
|
||||
|
||||
db.add(service_model)
|
||||
db.commit()
|
||||
# TODO: response model
|
||||
|
||||
@router.patch("/{service_id}/key")
|
||||
async def regenerate_api_key(db: db_dependency, service_id: int):
|
||||
# TODO: super_admin_dependency
|
||||
# TODO: request model
|
||||
key = generate_api_key()
|
||||
service_model = db.query(Service).filter(Service.id==service_id).first()
|
||||
service_model.api_key = key
|
||||
|
||||
db.add(service_model)
|
||||
db.commit()
|
||||
# TODO: response model
|
||||
|
||||
@router.delete("/{service_id}")
|
||||
async def remove_service(db: db_dependency, service_id: int):
|
||||
# TODO: super_admin_dependency
|
||||
# TODO: request model
|
||||
service_model = db.query(Service).filter(Service.id==service_id).first()
|
||||
if service_model is None:
|
||||
return
|
||||
|
||||
db.delete(service_model)
|
||||
db.commit()
|
||||
# TODO: response model
|
||||
7
src/service/schemas.py
Normal file
7
src/service/schemas.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Pydantic models for <this module>
|
||||
|
||||
Models:
|
||||
- List: Description
|
||||
- Models: Description
|
||||
"""
|
||||
11
src/service/service.py
Normal file
11
src/service/service.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
Module specific business logic for <this module>
|
||||
|
||||
Classes:
|
||||
- List: Description
|
||||
- Classes: Description
|
||||
|
||||
Functions:
|
||||
- List: Description
|
||||
- Functions: Description
|
||||
"""
|
||||
16
src/service/utils.py
Normal file
16
src/service/utils.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
Non-business logic reusable functions and classes for <this module>
|
||||
|
||||
Classes:
|
||||
- List: Description
|
||||
- Classes: Description
|
||||
|
||||
Functions:
|
||||
- List: Description
|
||||
- Functions: Description
|
||||
"""
|
||||
import uuid
|
||||
|
||||
|
||||
def generate_api_key() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
|
@ -23,7 +23,7 @@ from src.user.schemas import UserResponse, OrgResponse, OIDCClaims
|
|||
from src.user.exceptions import UserNotFoundException
|
||||
|
||||
from src.organisation.models import OrgUsers, Organisation
|
||||
|
||||
from src.iam.models import Group, UserGroups
|
||||
from src.auth.service import claims_dependency
|
||||
from src.database import db_dependency
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ async def get_current_organisations(db: db_dependency, user: claims_dependency):
|
|||
if not user_exists:
|
||||
raise UserNotFoundException(user_id=user_id)
|
||||
|
||||
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
|
||||
org_user_models = (db.query(OrgUsers.org_id, Organisation.name)
|
||||
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
|
||||
.filter(OrgUsers.user_id == user_id)
|
||||
.all()
|
||||
|
|
@ -87,31 +87,6 @@ async def get_current_organisations(db: db_dependency, user: claims_dependency):
|
|||
return org_user_models
|
||||
|
||||
|
||||
@router.get("/self/orgs/admin", response_model=list[OrgResponse], status_code=status.HTTP_200_OK, responses={
|
||||
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
|
||||
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
|
||||
})
|
||||
async def get_current_admin_organisations(db: db_dependency, user: claims_dependency):
|
||||
"""
|
||||
Returns the organisations for which the currently logged-in user is an admin.
|
||||
"""
|
||||
user_id = user.get("db_id", None)
|
||||
if user_id is None:
|
||||
raise UserNotFoundException()
|
||||
user_exists = db.query(exists().where(User.id == user_id)).scalar()
|
||||
if not user_exists:
|
||||
raise UserNotFoundException(user_id=user_id)
|
||||
|
||||
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
|
||||
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
|
||||
.filter(OrgUsers.user_id == user_id)
|
||||
.filter(OrgUsers.is_admin == True)
|
||||
.all()
|
||||
)
|
||||
|
||||
return org_user_models
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse, status_code=status.HTTP_200_OK, responses={
|
||||
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
|
||||
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
|
||||
|
|
@ -139,7 +114,7 @@ async def get_organisations(db: db_dependency, user_id: Annotated[int, Path(gt=0
|
|||
if not user_exists:
|
||||
raise UserNotFoundException(user_id=user_id)
|
||||
|
||||
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
|
||||
org_user_models = (db.query(OrgUsers.org_id, Organisation.name)
|
||||
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
|
||||
.filter(OrgUsers.user_id == user_id)
|
||||
.all()
|
||||
|
|
@ -148,28 +123,6 @@ async def get_organisations(db: db_dependency, user_id: Annotated[int, Path(gt=0
|
|||
return org_user_models
|
||||
|
||||
|
||||
@router.get("/{user_id}/orgs/admin", response_model=list[OrgResponse], status_code=status.HTTP_200_OK, responses={
|
||||
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
|
||||
status.HTTP_200_OK: {"description": "Successful retrieval from database"},
|
||||
})
|
||||
async def get_admin_organisations(db: db_dependency, user_id: Annotated[int, Path(gt=0,description="User database ID")]):
|
||||
"""
|
||||
Returns the organisations for which the user with the provided user ID is an admin.
|
||||
"""
|
||||
user_exists = db.query(exists().where(User.id == user_id)).scalar()
|
||||
if not user_exists:
|
||||
raise UserNotFoundException(user_id=user_id)
|
||||
|
||||
org_user_models = (db.query(OrgUsers.org_id, OrgUsers.is_admin, Organisation.name)
|
||||
.join(OrgUsers, Organisation.id == OrgUsers.org_id)
|
||||
.filter(OrgUsers.user_id == user_id)
|
||||
.filter(OrgUsers.is_admin == True)
|
||||
.all()
|
||||
)
|
||||
|
||||
return org_user_models
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT, responses={
|
||||
status.HTTP_204_NO_CONTENT: {"description": "User deleted"},
|
||||
status.HTTP_404_NOT_FOUND: {"description": "User not found"},
|
||||
|
|
@ -183,3 +136,15 @@ async def delete_user_by_id(user_id: Annotated[int, Path(gt=0)], db: db_dependen
|
|||
raise UserNotFoundException(user_id=user_id)
|
||||
db.delete(user_model)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/{user_id}/groups")
|
||||
async def get_user_groups(db: db_dependency, user_id: Annotated[int, Path(gt=0,description="User database ID")]):
|
||||
user_model = (db.query(User).filter(User.id == user_id).first())
|
||||
if user_model is None:
|
||||
raise UserNotFoundException(user_id=user_id)
|
||||
|
||||
user_groups = db.query(Group).join(UserGroups).filter(UserGroups.user_id==user_id).all()
|
||||
|
||||
# TODO: Response model
|
||||
return user_groups
|
||||
|
|
|
|||
|
|
@ -49,4 +49,3 @@ class UserResponse(CustomBaseModel):
|
|||
class OrgResponse(CustomBaseModel):
|
||||
org_id: int
|
||||
name: str
|
||||
is_admin: bool
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue