Compare commits

..

12 commits

Author SHA1 Message Date
71f26a4c9b feat: example .env 2026-05-28 14:32:08 +01:00
00dcf7ce35 minor: service schema nomenclature 2026-05-28 14:27:14 +01:00
33e78d4a9b docs: org docstrings
issue: #13
2026-05-28 14:23:36 +01:00
82c2b13a7f minor: contact endpoints allowed preapproval 2026-05-28 14:05:31 +01:00
9a1975c389 minor: iam schema nomenclature 2026-05-28 13:37:32 +01:00
7a0f43d34f minor: org schema nomenclature 2026-05-28 13:32:59 +01:00
0e169af456 docs: iam docstrings
Issue: #13
2026-05-28 13:22:24 +01:00
0c1c9f62ee fix: permission dependency 2026-05-28 13:19:54 +01:00
39d3f2d560 docs: contact docstrings
Issue: #13
2026-05-28 11:22:47 +01:00
42349b0182 docs: auth docstrings
Issue: #13
2026-05-28 11:22:47 +01:00
a86cfea65a minor: type hint 2026-05-28 11:22:37 +01:00
d8abe17618 docs: admin module docstrings
Issue: #13
2026-05-28 11:22:37 +01:00
48 changed files with 168 additions and 284 deletions

10
example.env Normal file
View file

@ -0,0 +1,10 @@
SECRET_KEY=""
OIDC_CONFIG="https://sso.sr2.uk/realms/sr2/.well-known/openid-configuration"
OIDC_ISSUER="https://sso.sr2.uk/realms/sr2"
OIDC_AUDIENCE="account"
CLIENT_ID=""
DATABASE_NAME="cloud-api"
DATABASE_PORT="5432"
DATABASE_HOSTNAME="localhost"
DATABASE_CREDENTIALS="user:password"

View file

@ -1,7 +1,3 @@
""" """
Configurations for the admin module Configurations for the admin module
Configurations:
- List: Description
- Configs: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Constants and error codes for the admin module Constants for the admin module
Constants:
- List: Description
- Consts: Description
""" """

View file

@ -1,11 +1,3 @@
""" """
Router dependencies for the admin module Dependencies for the admin module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Module specific exceptions for the admin module Custom exceptions for the admin module
Exceptions:
- List: Description
- Exceptions: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Database models for the admin module Database models for the admin module
Models:
- List: Description
- Models: Description
""" """

View file

@ -1,9 +1,8 @@
""" """
Router endpoints for the admin module Router endpoints for the admin module
Endpoints: Exports:
- List: Description - router: fastapi.APIRouter
- Endpoints: Description
""" """
from fastapi import APIRouter from fastapi import APIRouter

View file

@ -1,7 +1,3 @@
""" """
Pydantic models for the admin module Pydantic models for the admin module
Models:
- List: Description
- Models: Description
""" """

View file

@ -1,11 +1,3 @@
""" """
Module specific business logic for the admin module Module specific business logic for the admin module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,11 +1,3 @@
""" """
Non-business logic reusable functions and classes for the admin module Non-business logic reusable functions and classes for the admin module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,9 +1,8 @@
""" """
Configurations for auth module, import auth_settings Configurations for the auth module
Configurations: Exports:
- List: Description - auth_settings: Contains OIDC information
- Configs: Description
""" """
from src.config import CustomBaseSettings from src.config import CustomBaseSettings

View file

@ -1,7 +1,3 @@
""" """
Constants and error codes for auth module Constants for the auth module
Constants:
- List: Description
- Consts: Description
""" """

View file

@ -1,18 +1,17 @@
""" """
Router dependencies for auth module Auth dependencies
Classes: Exports:
- List: Description - org_query_user_claims_dependency: bool: Verifies user belongs to org
- Classes: Description - org_model_root_claim_query_dependency: org_model: verifies org exists and user is either root or su, gets org from query
- org_model_root_claim_body_dependency: org_model: verifies org exists and user is either root or su, gets org from body
Functions: - super_admin_dependency: user_model: verifies the user is a super admin
- List: Description
- Functions: Description
""" """
from typing import Annotated from typing import Annotated
from fastapi import Depends from fastapi import Depends
from src.user.dependencies import user_model_claims_dependency from src.user.dependencies import user_model_claims_dependency
from src.user.models import User
from src.organisation.dependencies import org_model_query_dependency, org_model_body_dependency from src.organisation.dependencies import org_model_query_dependency, org_model_body_dependency
from src.organisation.models import Organisation as Org from src.organisation.models import Organisation as Org
@ -69,4 +68,4 @@ async def user_model_super_admin(user_model: user_model_claims_dependency):
raise UnauthorizedException() raise UnauthorizedException()
super_admin_dependency = Annotated[bool, Depends(user_model_super_admin)] super_admin_dependency = Annotated[type[User], Depends(user_model_super_admin)]

View file

@ -1,9 +1,8 @@
""" """
Module specific exceptions for auth module Module specific exceptions for the auth module
Exceptions: Exceptions:
- List: Description - UnauthorizedException: Takes an optional message string
- Exceptions: Description
""" """
from typing import Optional from typing import Optional

View file

@ -1,7 +1,3 @@
""" """
Database models for auth module Database models for the auth module
Models:
- List: Description
- Models: Description
""" """

View file

@ -1,8 +1,8 @@
""" """
Router endpoints for auth module Router endpoints for the auth module
Contains oauth registration
Endpoints: Exports:
- router: fastapi.APIRouter
""" """
from fastapi import APIRouter from fastapi import APIRouter

View file

@ -1,7 +1,3 @@
""" """
Pydantic models for auth module Pydantic models for the auth module
Models:
- List: Description
- Models: Description
""" """

View file

@ -1,8 +1,8 @@
""" """
Module specific business logic for auth module Module specific business logic for the auth module
Exports: Exports:
- claims_dependency - claims_dependency: Dict[str, Any] containing OIDC claims and database ID
""" """
import json import json
import requests import requests

View file

@ -1,11 +1,3 @@
""" """
Non-business logic reusable functions and classes for auth module Non-business logic reusable functions and classes for the auth module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Configurations for contact module Configurations for the contact module
Configurations:
- List: Description
- Configs: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Constants and error codes for contact module Constants for the contact module
Constants:
- List: Description
- Consts: Description
""" """

View file

@ -1,11 +1,3 @@
""" """
Router dependencies for contact module Dependencies for the contact module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,9 +1,8 @@
""" """
Module specific exceptions for contact module Exceptions related to the contact module
Exceptions: Exports:
- List: Description - ContactNotFoundException: Takes an optional contact ID int
- Exceptions: Description
""" """
from typing import Optional from typing import Optional

View file

@ -1,9 +1,9 @@
""" """
Database models for contact module Database models for the contact module
Models: Models:
- Contact: id[pk], email, first_name, last_name, phonenumber, vat_number - Contact: id[pk], email, first_name, last_name, phonenumber, vat_number
street_address, post_office_box_number, address_locality, country_code, address_region, postal_code street_address, street_address_line_2, post_office_box_number, address_locality, country_code, address_region, postal_code
""" """
from sqlalchemy import Column, Integer, String, ForeignKey from sqlalchemy import Column, Integer, String, ForeignKey

View file

@ -1,13 +1,5 @@
""" """
Router endpoints for contact module Router endpoints for the contact module
Endpoints:
- [get]/{contact_id} - Returns non-address type details for contact
- [get]/{contact_id}/address - Returns address details for contact
- [get]/{contact_id}/orgs - Returns a list of orgs which the contact is assigned to, and what they are assigned as
- [post]/ - Creates a new contact
- [patch]/{contact_id} - Updates the details of an existing contact
- [delete]/{contact_id} - Deletes a contact by ID
""" """
from fastapi import APIRouter from fastapi import APIRouter

View file

@ -1,9 +1,9 @@
""" """
Pydantic models for contact module Pydantic models for the contact module
Models: Models:
- List: Description - ContactAddress
- Models: Description - ContactModel: Contains ContactAddress as a property
""" """
from typing import Optional from typing import Optional

View file

@ -1,11 +1,3 @@
""" """
Module specific business logic for contact module Module specific business logic for the contact module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,11 +1,3 @@
""" """
Non-business logic reusable functions and classes for contact module Non-business logic reusable functions and classes for the contact module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Configurations for <this module> Configurations for the IAM module
Configurations:
- List: Description
- Configs: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Constants and error codes for <this module> Constants for the IAM module
Constants:
- List: Description
- Consts: Description
""" """

View file

@ -1,13 +1,10 @@
""" """
Router dependencies for the IAM module Dependencies for the IAM module
Classes: Exports:
- List: Description - group_model_query_dependency: group_model: Gets group model from db, if it exists. Uses group_id from query param.
- Classes: Description - group_model_body_dependency: group_model: Gets group model from db, if it exists. Uses group_id from request body.
- perm_model_body_dependency: perm_model: Gets perm model from db, if it exists. Uses perm_id from request body.
Functions:
- List: Description
- Functions: Description
""" """
from typing import Annotated, Optional from typing import Annotated, Optional
@ -15,7 +12,7 @@ from fastapi import Depends, Query
from src.database import db_dependency from src.database import db_dependency
from src.iam.models import Group from src.iam.models import Group, Permission
from src.iam.exceptions import GroupNotFoundException, PermNotFoundException from src.iam.exceptions import GroupNotFoundException, PermNotFoundException
from src.iam.schemas import GroupIDMixin, PermIDMixin from src.iam.schemas import GroupIDMixin, PermIDMixin
@ -43,14 +40,14 @@ def get_group_model_body(db: db_dependency, request_model: Optional[GroupIDMixin
group_model_body_dependency = Annotated[type[Group], Depends(get_group_model_body)] group_model_body_dependency = Annotated[type[Group], Depends(get_group_model_body)]
def get_perm_model_body(db: db_dependency, request_model: Optional[PermIDMixin] = None) -> type[Group]: def get_perm_model_body(db: db_dependency, request_model: Optional[PermIDMixin] = None) -> type[Permission]:
perm_id = getattr(request_model, "permission_id", None) perm_id = getattr(request_model, "permission_id", None)
if perm_id is None: if perm_id is None:
raise PermNotFoundException raise PermNotFoundException
group_model = db.get(Group, perm_id) perm_model = db.get(Permission, perm_id)
if group_model is None: if perm_model is None:
raise PermNotFoundException(perm_id) raise PermNotFoundException(perm_id)
return group_model return perm_model
perm_model_body_dependency = Annotated[type[Group], Depends(get_perm_model_body)] perm_model_body_dependency = Annotated[type[Permission], Depends(get_perm_model_body)]

View file

@ -1,9 +1,9 @@
""" """
Module specific exceptions for the IAM module Exceptions related to the IAM module
Exceptions: Exceptions:
- List: Description - GroupNotFoundException: Takes an optional group_id int
- Exceptions: Description - PermNotFoundException: Takes an optional perm_id int
""" """
from typing import Optional from typing import Optional

View file

@ -2,8 +2,11 @@
Database models for the IAM module Database models for the IAM module
Models: Models:
- List: Description - Permission:
- Models: Description - id[PK], resource[U1], action[U1], service_id[FK][U1]
- service_rel: ORM relationship over service_id FK
- group_rel: ORM relationship backpops to Group.permission_rel
- service_name: Calc property service_rel.name
""" """
from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View file

@ -1,9 +1,19 @@
""" """
Router endpoints for <this module> Router endpoints for IAM
Endpoints: Endpoints:
- List: Description - [POST](/iam/can_act_on_resource): [API key & user claim]: Service access point to verify user permissions
- Endpoints: Description - [GET](/iam/group/permissions): [root user]: Gets list of perms(service, resource, action) the given group(id) has
- [DELETE](/iam/group/permissions): [root user]: Removes a given perm(id) from the given group(id)
- [GET](/iam/group/users): [root user]: Gets a list of users(id, name, email) that are assigned to the given group(id)
- [POST](/iam/group): [root user]: Creates a new group for the given org(id)
- [PUT](/iam/group/permission): [root user]: Assigns a perm(id) to the given group(id)
- [PUT](/iam/group/user): [root user]: Assigns a user(id) to a group(id)
- [DELETE](/iam/group/user): [root user]: Removes a user(id) from the given group(id)
- [GET](/iam/permissions): [root user]: Gets a list of all permissions
- [POST](/iam/permission): [super admin]: Creates a new permission
- [DELETE](/iam/permission): [super admin]: Removes a permission
- [GET](/iam/permissions/search): [root user]: Returns a list of permissions matching a filter(service|resource|action)
""" """
from fastapi import APIRouter, status from fastapi import APIRouter, status
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@ -25,10 +35,10 @@ from src.iam.service import service_key_dependency
from src.iam.models import Permission as Perm, GroupPermissions as GPerms, Group, UserGroups from src.iam.models import Permission as Perm, GroupPermissions as GPerms, Group, UserGroups
from src.iam.dependencies import group_model_query_dependency, group_model_body_dependency, perm_model_body_dependency from src.iam.dependencies import group_model_query_dependency, group_model_body_dependency, perm_model_body_dependency
from src.iam.schemas import IAMGetGroupPermissionsResponse, IAMGetGroupUsersResponse, IAMPostGroupRequest, \ from src.iam.schemas import IAMGetGroupPermissionsResponse, IAMGetGroupUsersResponse, IAMPostGroupRequest, \
GroupResponse, IAMPostGroupResponse, IAMPutGroupPermissionRequest, IAMPutGroupPermissionResponse, \ GroupSchema, IAMPostGroupResponse, IAMPutGroupPermissionRequest, IAMPutGroupPermissionResponse, \
IAMPutGroupUserRequest, IAMPutGroupUserResponse, IAMDeleteGroupPermissionRequest, IAMDeleteGroupPermissionResponse, \ IAMPutGroupUserRequest, IAMPutGroupUserResponse, IAMDeleteGroupPermissionRequest, IAMDeleteGroupPermissionResponse, \
IAMDeleteGroupUserRequest, IAMDeleteGroupUserResponse, IAMGetPermissionsResponse, IAMPostPermissionRequest, \ IAMDeleteGroupUserRequest, IAMDeleteGroupUserResponse, IAMGetPermissionsResponse, IAMPostPermissionRequest, \
IAMPostPermissionResponse, PermissionResponse, IAMDeletePermissionRequest, IAMGetPermissionsSearchRequest, IAMGetPermissionsSearchResponse IAMPostPermissionResponse, PermissionSchema, IAMDeletePermissionRequest, IAMGetPermissionsSearchRequest, IAMGetPermissionsSearchResponse
router = APIRouter( router = APIRouter(
tags=["IAM"], tags=["IAM"],
@ -91,7 +101,7 @@ async def create_group(db: db_dependency, org_model: org_model_root_claim_body_d
except IntegrityError as e: except IntegrityError as e:
if isinstance(e.orig, errors.UniqueViolation): if isinstance(e.orig, errors.UniqueViolation):
raise Conflict("Group with this name already exists") raise Conflict("Group with this name already exists")
response = GroupResponse(**group_model.__dict__) response = GroupSchema(**group_model.__dict__)
db.commit() db.commit()
return {"group": response} return {"group": response}
@ -107,7 +117,7 @@ async def add_group_permission(db: db_dependency, group_model: group_model_body_
group_model.permission_rel.append(perm_model) group_model.permission_rel.append(perm_model)
db.flush() db.flush()
response = IAMPutGroupPermissionResponse(group=GroupResponse(**group_model.__dict__), permissions=group_model.permission_rel) response = IAMPutGroupPermissionResponse(group=GroupSchema(**group_model.__dict__), permissions=group_model.permission_rel)
db.commit() db.commit()
return response return response
@ -122,7 +132,7 @@ async def add_group_user(db: db_dependency, group_model: group_model_body_depend
group_model.user_rel.append(user_model) group_model.user_rel.append(user_model)
db.flush() db.flush()
response = IAMPutGroupUserResponse(group=GroupResponse(**group_model.__dict__), users=group_model.user_rel) response = IAMPutGroupUserResponse(group=GroupSchema(**group_model.__dict__), users=group_model.user_rel)
db.commit() db.commit()
return response return response
@ -134,7 +144,7 @@ async def remove_group_permissions(db: db_dependency, group_model: group_model_b
group_model.permission_rel.remove(perm_model) group_model.permission_rel.remove(perm_model)
db.flush() db.flush()
response = IAMDeleteGroupPermissionResponse(group=GroupResponse(**group_model.__dict__), response = IAMDeleteGroupPermissionResponse(group=GroupSchema(**group_model.__dict__),
permissions=group_model.permission_rel) permissions=group_model.permission_rel)
db.commit() db.commit()
return response return response
@ -147,7 +157,7 @@ async def remove_group_user(db: db_dependency, group_model: group_model_body_dep
user_model.group_rel.remove(group_model) user_model.group_rel.remove(group_model)
db.flush() db.flush()
response = IAMDeleteGroupUserResponse(group=GroupResponse(**group_model.__dict__), users=group_model.user_rel) response = IAMDeleteGroupUserResponse(group=GroupSchema(**group_model.__dict__), users=group_model.user_rel)
db.commit() db.commit()
return response return response
@ -169,7 +179,7 @@ async def create_new_permission(db: db_dependency, su: super_admin_dependency, r
if isinstance(e.orig, errors.UniqueViolation): if isinstance(e.orig, errors.UniqueViolation):
raise Conflict(message="Permission already exists") raise Conflict(message="Permission already exists")
db.flush() db.flush()
response = IAMPostPermissionResponse(permission=PermissionResponse(**perm_model.__dict__)) response = IAMPostPermissionResponse(permission=PermissionSchema(**perm_model.__dict__))
db.commit() db.commit()
return response return response

View file

@ -1,9 +1,10 @@
""" """
Pydantic models for the IAM module Pydantic models for the IAM module
Models: Models follow the nomenclature of:
- List: Description - Sub-models: "<Resource><Opt:>Schema"
- Models: Description - Mixins: "<Attribute>Mixin"
- Models: "<Module><Method><Resource><Opt:Resource><Direction>" ie "IAMGetGroupPermissionsResponse"
""" """
from typing import Optional from typing import Optional
@ -14,20 +15,20 @@ from src.schemas import CustomBaseModel
from user.schemas import UserIDMixin from user.schemas import UserIDMixin
class UserResponse(CustomBaseModel): class UserSchema(CustomBaseModel):
id: int id: int
first_name: str first_name: str
last_name: str last_name: str
email: EmailStr email: EmailStr
class PermissionResponse(CustomBaseModel): class PermissionSchema(CustomBaseModel):
model_config = ConfigDict(from_attributes=True, extra="ignore") model_config = ConfigDict(from_attributes=True, extra="ignore")
service_name: str service_name: str
resource: str resource: str
action: str action: str
class GroupResponse(CustomBaseModel): class GroupSchema(CustomBaseModel):
id: int id: int
name: str name: str
@ -38,47 +39,47 @@ class PermIDMixin(CustomBaseModel):
permission_id: int permission_id: int
class IAMGetGroupPermissionsResponse(CustomBaseModel): class IAMGetGroupPermissionsResponse(CustomBaseModel):
permissions: list[PermissionResponse] permissions: list[PermissionSchema]
class IAMGetGroupUsersResponse(CustomBaseModel): class IAMGetGroupUsersResponse(CustomBaseModel):
users : list[UserResponse] users : list[UserSchema]
class IAMPostGroupRequest(OrgIDMixin): class IAMPostGroupRequest(OrgIDMixin):
name: str name: str
class IAMPostGroupResponse(CustomBaseModel): class IAMPostGroupResponse(CustomBaseModel):
group: GroupResponse group: GroupSchema
class IAMPutGroupPermissionRequest(GroupIDMixin, PermIDMixin): class IAMPutGroupPermissionRequest(GroupIDMixin, PermIDMixin):
pass pass
class IAMPutGroupPermissionResponse(CustomBaseModel): class IAMPutGroupPermissionResponse(CustomBaseModel):
group: GroupResponse group: GroupSchema
permissions: list[PermissionResponse] permissions: list[PermissionSchema]
class IAMPutGroupUserRequest(GroupIDMixin, UserIDMixin): class IAMPutGroupUserRequest(GroupIDMixin, UserIDMixin):
pass pass
class IAMPutGroupUserResponse(CustomBaseModel): class IAMPutGroupUserResponse(CustomBaseModel):
group: GroupResponse group: GroupSchema
users: list[UserResponse] users: list[UserSchema]
class IAMDeleteGroupPermissionRequest(GroupIDMixin, PermIDMixin): class IAMDeleteGroupPermissionRequest(GroupIDMixin, PermIDMixin):
pass pass
class IAMDeleteGroupPermissionResponse(CustomBaseModel): class IAMDeleteGroupPermissionResponse(CustomBaseModel):
group: GroupResponse group: GroupSchema
permissions: list[PermissionResponse] permissions: list[PermissionSchema]
class IAMDeleteGroupUserRequest(GroupIDMixin, UserIDMixin): class IAMDeleteGroupUserRequest(GroupIDMixin, UserIDMixin):
pass pass
class IAMDeleteGroupUserResponse(CustomBaseModel): class IAMDeleteGroupUserResponse(CustomBaseModel):
group: GroupResponse group: GroupSchema
users: list[UserResponse] users: list[UserSchema]
class IAMGetPermissionsResponse(CustomBaseModel): class IAMGetPermissionsResponse(CustomBaseModel):
permissions: list[PermissionResponse] permissions: list[PermissionSchema]
class IAMPostPermissionRequest(CustomBaseModel): class IAMPostPermissionRequest(CustomBaseModel):
service_id: int service_id: int
@ -86,7 +87,7 @@ class IAMPostPermissionRequest(CustomBaseModel):
action: str action: str
class IAMPostPermissionResponse(CustomBaseModel): class IAMPostPermissionResponse(CustomBaseModel):
permission: PermissionResponse permission: PermissionSchema
class IAMDeletePermissionRequest(PermIDMixin): class IAMDeletePermissionRequest(PermIDMixin):
pass pass
@ -97,4 +98,4 @@ class IAMGetPermissionsSearchRequest(CustomBaseModel):
action: Optional[str] = None action: Optional[str] = None
class IAMGetPermissionsSearchResponse(CustomBaseModel): class IAMGetPermissionsSearchResponse(CustomBaseModel):
permissions: list[PermissionResponse] permissions: list[PermissionSchema]

View file

@ -1,7 +1,8 @@
""" """
Module specific business logic for <this module> Business logic reusable functions related to IAM
Exports service_key_dependency Exports:
- service_key_dependency: bool: verifies request headers contain the correct api key for the service
""" """
from typing import Annotated from typing import Annotated

View file

@ -1,11 +1,3 @@
""" """
Non-business logic reusable functions and classes for <this module> Non-business logic reusable functions and classes for the IAM module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,7 +1,3 @@
""" """
Configurations for organisation module Configurations for the organisation module
Configurations:
- List: Description
- Configs: Description
""" """

View file

@ -1,5 +1,5 @@
""" """
Constants and error codes for organisation module Constants for the organisation module
Classes: Classes:
- Status(StrEnum): PARTIAL, SUBMITTED, REMEDIATION, APPROVED, REJECTED, REMOVED - Status(StrEnum): PARTIAL, SUBMITTED, REMEDIATION, APPROVED, REJECTED, REMOVED

View file

@ -1,13 +1,9 @@
""" """
Router dependencies for organisation module Dependencies related to the organisation module
Classes: Exports:
- List: Description - org_model_query_dependency: org_model: Gets org model from db, if it exists. Uses org_id from query param. Also verifies if the org has been approved.
- Classes: Description - org_model_body_dependency: org_model: Gets org model from db, if it exists. Uses org_id from request body. Also verifies if the org has been approved.
Functions:
- List: Description
- Functions: Description
""" """
from typing import Annotated, Optional from typing import Annotated, Optional
@ -26,7 +22,7 @@ def get_org_model(db, request: Request, org_id: int):
if org_model is None: if org_model is None:
raise OrgNotFoundException(org_id) raise OrgNotFoundException(org_id)
pre_approval_endpoints = ["PATCH/org/status", "PATCH/org/questionnaire", "GET/org/id"] pre_approval_endpoints = ["PATCH/org/status", "PATCH/org/questionnaire", "GET/org/id", "GET/org/contact", "PATCH/org/contact"]
current_request = f"{request.method}{request.url.path}" current_request = f"{request.method}{request.url.path}"
if current_request not in pre_approval_endpoints and org_model.status != OrgStatus.APPROVED: if current_request not in pre_approval_endpoints and org_model.status != OrgStatus.APPROVED:
raise AwaitingApprovalException(org_id) raise AwaitingApprovalException(org_id)

View file

@ -1,9 +1,9 @@
""" """
Module specific exceptions for organisation module Exceptions related to the organisation module
Exceptions: Exceptions:
- List: Description - OrgNotFoundException: Takes an optional org_id int
- Exceptions: Description - AwaitingApprovalException: Takes an optional org_id int
""" """
from typing import Optional from typing import Optional

View file

@ -2,9 +2,16 @@
Database models for organisation module Database models for organisation module
Models: Models:
- Organisation: id[pk], name, status, intake_questionnaire, - Organisation:
billing_contact_id[fk], security_contact_id[fk], owner_contact_id[fk] - id[PK], name, status, intake_questionnaire, root_user_id[FK], billing_contact_id[FK], security_contact_id[FK], owner_contact_id[FK]
- OrgUsers: org_id[fk][cpk], user_id[fk][cpk], is_admin - user_rel: ORM relationship to User via OrgUsers relationship table
- group_rel: ORM relationship to Group, backprops Group.org_rel
- root_user_rel: ORM relationship to User with root_user_id FK
- root_user_email: Calc property root_user_rel.email
- billing_contact_rel: ORM relationship to Contact with billing_contact FK
- security_contact_rel: ORM relationship to Contact with security_contact FK
- owner_contact_rel: ORM relationship to Contact with owner_contact FK
- OrgUsers: org_id[FK][PK], user_id[FK][PK]
""" """
from sqlalchemy import Column, Integer, String, ForeignKey, JSON from sqlalchemy import Column, Integer, String, ForeignKey, JSON
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View file

@ -2,15 +2,18 @@
Router endpoints for organisation module Router endpoints for organisation module
Endpoints: Endpoints:
- [get]/id/{org_id} - Retrieves an organisation by its ID - [GET](/org/id): [root user]: Get details about an organisation(id)
- [post]/ - Creates a new organisation - [POST](/org/): [oidc claim]: Creates an organisation, adds the current user as a user and sets them to be the root user
- [patch]/{org_id}/questionnaire - Updates the questionnaire data for an organisation (can be partial or final submission) - [PATCH](/org/questionnaire): [root user]: Updates the org's intake questionnaire and optionally be submitted for review
- [patch]/{org_id}/status - Updates the status of an organisation - [PATCH](/org/status): [super admin]: Allows a super admin to update an org(id) status(Status enum)
- [patch]/{org_id}/contact - Assigns a contact to an organisation (as billing, security, or owner) - [GET](/org/users): [root user]: Gets a list of the org(id) users(email)
- [get]/{org_id}/users - Retrieves all users associated with an organisation - [POST](/org/users): [root user]: Adds a new user(id) to the org(id)
- [post]/{org_id}/users - Adds a new user to an organisation - [DELETE](/org/): [super admin]: Deletes an organisation(id)
- [delete]/{org_id} - Deletes an organisation by ID - [PATCH](/org/root_user): [super admin]: Updates an org(id) root user(id)
- [get]/{org_id}/contact/{contact_type} - Retrieves the contact of a specific type (owner, billing, security) for an organisation - [GET](/org/groups): [root user]: Gets a list of the org(id) groups(name)
- [DELETE](/org/user): [root user]: Removes a user(id) from an org(id)
- [GET](/org/contact): [root user]: Gets the (contact_type) contact for an org(id)
- [PATCH](/org/contact): [root user]: Updates the (contact_type) contact for an org(id). Any number of details can be changed.
""" """
from typing import Annotated, Optional from typing import Annotated, Optional

View file

@ -1,9 +1,10 @@
""" """
Pydantic models for organisation module Pydantic models for organisation module
Models: Models follow the nomenclature of:
- List: Description - Sub-models: "<Resource><Opt:>Schema"
- Models: Description - Mixins: "<Attribute>Mixin"
- Models: "<Module><Method><Resource><Opt:Resource><Direction>" ie "OrgPostOrgRequest"
""" """
from typing import Optional from typing import Optional
@ -16,7 +17,7 @@ from src.user.schemas import UserIDMixin
from src.organisation.constants import Status, ContactType from src.organisation.constants import Status, ContactType
class OrgQuestionnaire(CustomBaseModel): class Questionnaire(CustomBaseModel):
question_one: str question_one: str
question_two: str question_two: str
question_three: str question_three: str
@ -27,10 +28,10 @@ class OrgIDMixin(CustomBaseModel):
class OrgPostOrgRequest(CustomBaseModel): class OrgPostOrgRequest(CustomBaseModel):
name: str name: str
intake_questionnaire: Optional[OrgQuestionnaire] = None intake_questionnaire: Optional[Questionnaire] = None
class OrgPatchQuestionnaireRequest(OrgIDMixin): class OrgPatchQuestionnaireRequest(OrgIDMixin):
intake_questionnaire: OrgQuestionnaire intake_questionnaire: Questionnaire
partial: bool partial: bool
class OrgPatchStatusRequest(OrgIDMixin): class OrgPatchStatusRequest(OrgIDMixin):

View file

@ -1,11 +1,3 @@
""" """
Module specific business logic for organisation module Reusable business logic functions for the organisation module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -1,11 +1,3 @@
""" """
Non-business logic reusable functions and classes for organisation module Non-business logic reusable functions and classes for the organisation module
Classes:
- List: Description
- Classes: Description
Functions:
- List: Description
- Functions: Description
""" """

View file

@ -17,7 +17,7 @@ from src.service.models import Service
from src.service.utils import generate_api_key from src.service.utils import generate_api_key
from src.service.dependencies import service_model_body_dependency from src.service.dependencies import service_model_body_dependency
from src.service.schemas import ServiceGetServiceResponse, ServicePostServiceRequest, ServicePostServiceResponse, \ from src.service.schemas import ServiceGetServiceResponse, ServicePostServiceRequest, ServicePostServiceResponse, \
ServiceWithKeyResponse, ServicePatchKeyResponse, ServicePatchKeyRequest, ServiceDeleteServiceRequest ServiceWithKeySchema, ServicePatchKeyResponse, ServicePatchKeyRequest, ServiceDeleteServiceRequest
router = APIRouter( router = APIRouter(
tags=["Service"], tags=["Service"],
@ -42,7 +42,7 @@ async def register_service(db: db_dependency, su: super_admin_dependency, servic
if isinstance(e.orig, UniqueViolation): if isinstance(e.orig, UniqueViolation):
raise Conflict(message="Service with this name already exists") raise Conflict(message="Service with this name already exists")
db.commit() db.commit()
response = ServiceWithKeyResponse(**service_model.__dict__) response = ServiceWithKeySchema(**service_model.__dict__)
db.commit() db.commit()
return {"service": response} return {"service": response}
@ -52,7 +52,7 @@ async def regenerate_api_key(db: db_dependency, su: super_admin_dependency, serv
service_model.api_key = key service_model.api_key = key
db.flush() db.flush()
response = ServiceWithKeyResponse(**service_model.__dict__) response = ServiceWithKeySchema(**service_model.__dict__)
db.commit() db.commit()
return {"service": response} return {"service": response}

View file

@ -12,29 +12,29 @@ from src.schemas import CustomBaseModel
class ServiceIDMixin(CustomBaseModel): class ServiceIDMixin(CustomBaseModel):
service_id: int service_id: int
class ServiceResponse(CustomBaseModel): class ServiceSchema(CustomBaseModel):
model_config = ConfigDict(from_attributes=True, extra="ignore") model_config = ConfigDict(from_attributes=True, extra="ignore")
id: int id: int
name: str name: str
class ServiceWithKeyResponse(ServiceResponse): class ServiceWithKeySchema(ServiceSchema):
api_key: str api_key: str
class ServiceGetServiceResponse(CustomBaseModel): class ServiceGetServiceResponse(CustomBaseModel):
services: list[ServiceResponse] services: list[ServiceSchema]
class ServicePostServiceRequest(CustomBaseModel): class ServicePostServiceRequest(CustomBaseModel):
name: str name: str
class ServicePostServiceResponse(CustomBaseModel): class ServicePostServiceResponse(CustomBaseModel):
service: ServiceWithKeyResponse service: ServiceWithKeySchema
class ServicePatchKeyRequest(ServiceIDMixin): class ServicePatchKeyRequest(ServiceIDMixin):
pass pass
class ServicePatchKeyResponse(CustomBaseModel): class ServicePatchKeyResponse(CustomBaseModel):
service: ServiceWithKeyResponse service: ServiceWithKeySchema
class ServiceDeleteServiceRequest(ServiceIDMixin): class ServiceDeleteServiceRequest(ServiceIDMixin):
pass pass