Compare commits
6 commits
61e186a727
...
bcdef91dd0
| Author | SHA1 | Date | |
|---|---|---|---|
| bcdef91dd0 | |||
| 8925280f96 | |||
| 38c26cca7b | |||
| 0a7f9092c7 | |||
| c268097306 | |||
| dd0478d5e7 |
12 changed files with 289 additions and 62 deletions
|
|
@ -2,18 +2,20 @@
|
|||
Router endpoints for IAM
|
||||
|
||||
Endpoints:
|
||||
- [POST](/iam/can_act_on_resource): [API key & user claim]: Service access point to verify user permissions
|
||||
- [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)
|
||||
- [POST](/api/v1/iam/can_act_on_resource): [API Key & User JWT]: Used for services to check user access permission
|
||||
- [GET](/api/v1/iam/group/permissions): [Root User]: Gets a list of permissions granted to a group
|
||||
- [GET](/api/v1/iam/group/users): [Root User]: Gets a list of users assigned to a group
|
||||
- [POST](/api/v1/iam/group): [Root User]: Creates a new group
|
||||
- [PUT](/api/v1/iam/group/permission): [Root User]: Grants a permission to a group
|
||||
- [PUT](/api/v1/iam/group/user): [Root User]: Directly adds a user to the group
|
||||
- [DELETE](/api/v1/iam/group/permissions): [Root User]: Removes a permission from the group
|
||||
- [DELETE](/api/v1/iam/group/user): [Root User]: Removes a user from the group
|
||||
- [GET](/api/v1/iam/permissions): [Root User]: Returns a full list of permissions
|
||||
- [POST](/api/v1/iam/permission): [Super Admin]: Creates a new permission for a service
|
||||
- [DELETE](/api/v1/iam/permission): [Super Admin]: Deletes a permission
|
||||
- [POST](/api/v1/iam/permissions/search): [Root User]: Search list of permissions
|
||||
- [PUT](/api/v1/iam/group/user/invitation): [Root User]: Send an email invitation for non-org member to join a group
|
||||
- [PUT](/api/v1/iam/group/user//invitation/accept): [User Claim & Invite JWT]: Accept email invitation to join an org's group
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, status, BackgroundTasks
|
||||
|
|
@ -281,7 +283,21 @@ async def add_group_permission(
|
|||
return response
|
||||
|
||||
|
||||
@router.put("/group/user", response_model=IAMPutGroupUserResponse)
|
||||
@router.put(
|
||||
path="/group/user",
|
||||
summary="Directly adds a user to the group",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=IAMPutGroupUserResponse,
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Group not in org | User not authenticated | User does not have permission"
|
||||
},
|
||||
status.HTTP_409_CONFLICT: {"description": "User is already in group"},
|
||||
status.HTTP_403_FORBIDDEN: {
|
||||
"description": "Only existing org members can be added directly."
|
||||
},
|
||||
},
|
||||
)
|
||||
async def add_group_user(
|
||||
db: db_dependency,
|
||||
group_model: group_model_body_dependency,
|
||||
|
|
@ -289,6 +305,11 @@ async def add_group_user(
|
|||
org_model: org_model_root_claim_body_dependency,
|
||||
request_model: IAMPutGroupUserRequest,
|
||||
):
|
||||
"""
|
||||
Directly adds an organisation member to a group.\n
|
||||
To add a non-member, use an email invitation instead.\n
|
||||
The user's email address must match the email on their OIDC profile.
|
||||
"""
|
||||
if group_model.org_id != org_model.id:
|
||||
raise UnauthorizedException("Group does not belong to this organization")
|
||||
|
||||
|
|
@ -309,13 +330,26 @@ async def add_group_user(
|
|||
return response
|
||||
|
||||
|
||||
@router.delete("/group/permissions")
|
||||
@router.delete(
|
||||
path="/group/permissions",
|
||||
summary="Removes a permission from the group",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=IAMDeleteGroupPermissionResponse,
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Group not in org | User not authenticated | User does not have permission"
|
||||
},
|
||||
},
|
||||
)
|
||||
async def remove_group_permissions(
|
||||
db: db_dependency,
|
||||
group_model: group_model_query_dependency,
|
||||
perm_model: perm_model_query_dependency,
|
||||
org_model: org_model_root_claim_query_dependency,
|
||||
):
|
||||
"""
|
||||
Removes a permission from the group.
|
||||
"""
|
||||
if group_model.org_id != org_model.id:
|
||||
raise UnauthorizedException("Group does not belong to this organization")
|
||||
|
||||
|
|
@ -329,13 +363,26 @@ async def remove_group_permissions(
|
|||
return response
|
||||
|
||||
|
||||
@router.delete("/group/user")
|
||||
@router.delete(
|
||||
path="/group/user",
|
||||
summary="Removes a user from the group",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=IAMDeleteGroupUserResponse,
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "Group not in org | User not authenticated | User does not have permission"
|
||||
},
|
||||
},
|
||||
)
|
||||
async def remove_group_user(
|
||||
db: db_dependency,
|
||||
group_model: group_model_query_dependency,
|
||||
user_model: user_model_query_dependency,
|
||||
org_model: org_model_root_claim_query_dependency,
|
||||
):
|
||||
"""
|
||||
Removes a user from the group.
|
||||
"""
|
||||
if group_model.org_id != org_model.id:
|
||||
raise UnauthorizedException("Group does not belong to this organization")
|
||||
|
||||
|
|
@ -349,21 +396,47 @@ async def remove_group_user(
|
|||
return response
|
||||
|
||||
|
||||
@router.get("/permissions", response_model=IAMGetPermissionsResponse)
|
||||
@router.get(
|
||||
path="/permissions",
|
||||
summary="Returns a full list of permissions",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=IAMGetPermissionsResponse,
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {
|
||||
"description": "User must be root user of an organisation."
|
||||
},
|
||||
},
|
||||
)
|
||||
async def get_permissions(
|
||||
db: db_dependency, org_model: org_model_root_claim_query_dependency
|
||||
):
|
||||
"""
|
||||
Returns a full list of permissions.
|
||||
"""
|
||||
permission_models = db.query(Perm).all()
|
||||
|
||||
return {"permissions": permission_models}
|
||||
|
||||
|
||||
@router.post("/permission", response_model=IAMPostPermissionResponse)
|
||||
@router.post(
|
||||
path="/permission",
|
||||
summary="Creates a new permission for a service",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
response_model=IAMPostPermissionResponse,
|
||||
responses={
|
||||
status.HTTP_401_UNAUTHORIZED: {"description": "Must be super user."},
|
||||
status.HTTP_404_NOT_FOUND: {"description": "Service does not exist"},
|
||||
status.HTTP_409_CONFLICT: {"description": "Permission already exists"},
|
||||
},
|
||||
)
|
||||
async def create_new_permission(
|
||||
db: db_dependency,
|
||||
su: super_admin_dependency,
|
||||
request_model: IAMPostPermissionRequest,
|
||||
):
|
||||
"""
|
||||
Allows a super admin to create a new IAM permission for a service.
|
||||
"""
|
||||
service_model = db.get(Service, request_model.service_id)
|
||||
if service_model is None:
|
||||
raise ServiceNotFoundException(service_id=request_model.service_id)
|
||||
|
|
@ -387,22 +460,40 @@ async def create_new_permission(
|
|||
return {"permission": response}
|
||||
|
||||
|
||||
@router.delete("/permission", status_code=status.HTTP_204_NO_CONTENT)
|
||||
@router.delete(
|
||||
path="/permission",
|
||||
summary="Deletes a permission",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
responses={},
|
||||
)
|
||||
async def delete_permission(
|
||||
db: db_dependency,
|
||||
su: super_admin_dependency,
|
||||
perm_model: perm_model_query_dependency,
|
||||
):
|
||||
"""
|
||||
Allows a super admin to remove a permission.
|
||||
"""
|
||||
db.delete(perm_model)
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.post("/permissions/search", response_model=IAMGetPermissionsSearchResponse)
|
||||
@router.post(
|
||||
path="/permissions/search",
|
||||
summary="Search list of permissions",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=IAMGetPermissionsSearchResponse,
|
||||
responses={},
|
||||
)
|
||||
async def post_permissions(
|
||||
db: db_dependency,
|
||||
org_model: org_model_root_claim_body_dependency,
|
||||
request_model: IAMGetPermissionsSearchRequest,
|
||||
):
|
||||
"""
|
||||
Returns a list of permissions filtered by the queries provided.\n
|
||||
If a query is null, it will be ignored.
|
||||
"""
|
||||
permission_query = db.query(Perm)
|
||||
|
||||
if request_model.service_id is not None:
|
||||
|
|
@ -424,9 +515,10 @@ async def post_permissions(
|
|||
|
||||
|
||||
@router.put(
|
||||
"/group/user/invitation",
|
||||
path="/group/user/invitation",
|
||||
summary="Send an email invitation for non-org member to join a group",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={},
|
||||
)
|
||||
async def invitation(
|
||||
background_tasks: BackgroundTasks,
|
||||
|
|
@ -434,11 +526,17 @@ async def invitation(
|
|||
group_model: group_model_body_dependency,
|
||||
request_model: IAMPutGroupInvitationRequest,
|
||||
):
|
||||
org_id = org_model.id
|
||||
org_name = org_model.name
|
||||
"""
|
||||
Sends an email invitation to join a group.\n
|
||||
This is intended for inviting no-members to a group, giving them permission to access org resources.\n
|
||||
i.e. Allowing somebody in a partner organisation to view metrics.\n
|
||||
Can also be used for inviting organisaion members if needed.
|
||||
"""
|
||||
org_id: int = org_model.id
|
||||
org_name: str = org_model.name
|
||||
user_email = request_model.user_email
|
||||
group_id = group_model.id
|
||||
group_name = group_model.name
|
||||
group_id: int = group_model.id
|
||||
group_name: str = group_model.name
|
||||
|
||||
background_tasks.add_task(
|
||||
send_user_group_invitation,
|
||||
|
|
@ -453,32 +551,42 @@ async def invitation(
|
|||
|
||||
|
||||
@router.put(
|
||||
"/group/user//invitation/accept",
|
||||
path="/group/user//invitation/accept",
|
||||
summary="Accept email invitation to join an org's group",
|
||||
status_code=status.HTTP_200_OK,
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {"description": "User|Org|Group not found"},
|
||||
status.HTTP_403_FORBIDDEN: {
|
||||
"description": "Group and organisation do not match"
|
||||
},
|
||||
status.HTTP_409_CONFLICT: {"description": "User is already in the group"},
|
||||
},
|
||||
)
|
||||
async def accept_invitation(
|
||||
db: db_dependency,
|
||||
user_model: user_model_claims_dependency,
|
||||
request_model: IAMPutGroupInvitationAcceptRequest,
|
||||
):
|
||||
"""
|
||||
Accepts an invitation to join an org's group
|
||||
"""
|
||||
email_claims = await verify_email_token(
|
||||
token=request_model.jwt, user_model=user_model
|
||||
)
|
||||
|
||||
org_model = db.get(Org, email_claims["org_id"])
|
||||
if org_model is None:
|
||||
raise OrgNotFoundException()
|
||||
raise OrgNotFoundException(email_claims["org_id"])
|
||||
|
||||
group_model = db.get(Group, email_claims["group_id"])
|
||||
if group_model is None:
|
||||
raise GroupNotFoundException()
|
||||
raise GroupNotFoundException(email_claims["group_id"])
|
||||
|
||||
if group_model not in org_model.group_rel:
|
||||
raise UnauthorizedException("Group and org do not match.")
|
||||
raise ForbiddenException("Group and org do not match.")
|
||||
|
||||
if user_model in group_model.user_rel:
|
||||
raise ConflictException(message="User already in group.")
|
||||
raise ConflictException("User already in group.")
|
||||
|
||||
group_model.user_rel.append(user_model)
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -34,13 +34,17 @@ tags_metadata = [
|
|||
"name": "User",
|
||||
"description": "User related operations, includes getting information about the current user",
|
||||
},
|
||||
{
|
||||
"name": "Organisation",
|
||||
"description": "Organisation related operations, includes getting lists of users etc associated with orgs",
|
||||
},
|
||||
{
|
||||
"name": "Service",
|
||||
"description": "Services related operations, includes registering services and reissuing API keys",
|
||||
},
|
||||
{
|
||||
"name": "Organisation",
|
||||
"description": "Organisation related operations, includes getting lists of users etc associated with orgs",
|
||||
"name": "IAM",
|
||||
"description": "Operations related to the role based identity and access management system. This includes management of groups, permissions, and related users.",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ Endpoints:
|
|||
- [PATCH](/org/contact): [root user]: Updates the (contact_type) contact for an org(id). Any number of details can be changed.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, status
|
||||
|
|
@ -29,6 +30,7 @@ from src.contact.models import Contact
|
|||
from src.contact.schemas import ContactAddress
|
||||
from src.contact.exceptions import ContactNotFoundException
|
||||
from src.database import db_dependency
|
||||
from src.organisation.schemas_questionnaires import QuestionnaireQuestionsVersion0
|
||||
from src.user.dependencies import (
|
||||
user_model_body_dependency,
|
||||
user_model_claims_dependency,
|
||||
|
|
@ -64,6 +66,7 @@ from src.organisation.schemas import (
|
|||
OrgPatchRootResponse,
|
||||
Questionnaire,
|
||||
OrgPatchContactResponse,
|
||||
QuestionnaireMetadata,
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
|
|
@ -88,7 +91,9 @@ router = APIRouter(
|
|||
},
|
||||
},
|
||||
)
|
||||
async def get_org_by_id(org_model: org_model_root_claim_query_dependency):
|
||||
async def get_org_by_id(
|
||||
db: db_dependency, org_model: org_model_root_claim_query_dependency
|
||||
):
|
||||
"""
|
||||
Returns organisation details including key member email addresses
|
||||
"""
|
||||
|
|
@ -143,10 +148,21 @@ async def create_org(
|
|||
ALl organisations are given the "partial" status on creation. See update_questionnaire() for more details.
|
||||
"""
|
||||
if request_model.intake_questionnaire:
|
||||
intake_questionnaire = request_model.intake_questionnaire.model_dump()
|
||||
questionnaire_questions = request_model.intake_questionnaire.model_dump()
|
||||
else:
|
||||
intake_questionnaire = None
|
||||
org_model = Org(name=request_model.name, intake_questionnaire=intake_questionnaire)
|
||||
questionnaire_questions = QuestionnaireQuestionsVersion0().model_dump()
|
||||
|
||||
questionnaire_metadata = QuestionnaireMetadata(version=0, submission_date=None)
|
||||
|
||||
intake_questionnaire = Questionnaire(
|
||||
metadata=questionnaire_metadata,
|
||||
questions=questionnaire_questions,
|
||||
)
|
||||
|
||||
org_model = Org(
|
||||
name=request_model.name,
|
||||
intake_questionnaire=intake_questionnaire.model_dump(mode="json"),
|
||||
)
|
||||
|
||||
org_model.status = "partial"
|
||||
|
||||
|
|
@ -204,18 +220,27 @@ async def update_questionnaire(
|
|||
final "are you sure" check before setting the org to be in "submitted" status, awaiting admin approval.
|
||||
"""
|
||||
update_data = request_model.intake_questionnaire.model_dump(exclude_none=True)
|
||||
questionnaire_model = Questionnaire(**org_model.intake_questionnaire)
|
||||
questionnaire = org_model.intake_questionnaire
|
||||
questions_model = QuestionnaireQuestionsVersion0(**questionnaire["questions"])
|
||||
for key, value in update_data.items():
|
||||
if hasattr(questionnaire_model, key):
|
||||
setattr(questionnaire_model, key, value)
|
||||
if hasattr(questions_model, key):
|
||||
setattr(questions_model, key, value)
|
||||
else:
|
||||
raise UnprocessableContentException("Invalid keys in update request")
|
||||
|
||||
metadata = QuestionnaireMetadata(version=questionnaire["metadata"]["version"])
|
||||
|
||||
# Allows for partially completed questionnaires to be saved without being submitted for review
|
||||
if not request_model.partial:
|
||||
org_model.status = "submitted"
|
||||
metadata.submission_date = datetime.now(timezone.utc)
|
||||
|
||||
org_model.intake_questionnaire = questionnaire_model.model_dump()
|
||||
questionnaire_model = Questionnaire(
|
||||
metadata=metadata,
|
||||
questions=questions_model,
|
||||
)
|
||||
|
||||
org_model.intake_questionnaire = questionnaire_model.model_dump(mode="json")
|
||||
db.flush()
|
||||
response = OrgPatchQuestionnaireResponse(**org_model.__dict__)
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Models follow the nomenclature of:
|
|||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import EmailStr, ConfigDict
|
||||
|
||||
|
|
@ -21,12 +22,20 @@ from src.schemas import (
|
|||
from src.contact.schemas import ContactModel
|
||||
|
||||
from src.organisation.constants import Status, ContactType
|
||||
from src.organisation.schemas_questionnaires import (
|
||||
QuestionnaireQuestionsVersion0 as CurrentQuestions,
|
||||
questionnaire_union,
|
||||
)
|
||||
|
||||
|
||||
class QuestionnaireMetadata(CustomBaseModel):
|
||||
version: int
|
||||
submission_date: Optional[datetime] = None
|
||||
|
||||
|
||||
class Questionnaire(CustomBaseModel):
|
||||
question_one: Optional[str] = None
|
||||
question_two: Optional[str] = None
|
||||
question_three: Optional[str] = None
|
||||
metadata: QuestionnaireMetadata
|
||||
questions: questionnaire_union
|
||||
|
||||
|
||||
class ContactSummary(CustomBaseModel):
|
||||
|
|
@ -47,7 +56,7 @@ class OrgSchema(OrgIDMixin):
|
|||
|
||||
class OrgPostOrgRequest(CustomBaseModel):
|
||||
name: str
|
||||
intake_questionnaire: Optional[Questionnaire] = None
|
||||
intake_questionnaire: Optional[CurrentQuestions] = None
|
||||
|
||||
|
||||
class OrgPostOrgResponse(CustomBaseModel):
|
||||
|
|
@ -57,7 +66,7 @@ class OrgPostOrgResponse(CustomBaseModel):
|
|||
|
||||
|
||||
class OrgPatchQuestionnaireRequest(OrgIDMixin):
|
||||
intake_questionnaire: Questionnaire
|
||||
intake_questionnaire: CurrentQuestions
|
||||
partial: bool
|
||||
|
||||
|
||||
|
|
|
|||
16
src/organisation/schemas_questionnaires.py
Normal file
16
src/organisation/schemas_questionnaires.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from typing import Optional
|
||||
|
||||
from src.schemas import CustomBaseModel
|
||||
|
||||
|
||||
class QuestionnaireQuestions(CustomBaseModel):
|
||||
pass
|
||||
|
||||
|
||||
class QuestionnaireQuestionsVersion0(QuestionnaireQuestions):
|
||||
question_one: Optional[str] = None
|
||||
question_two: Optional[str] = None
|
||||
question_three: Optional[str] = None
|
||||
|
||||
|
||||
questionnaire_union = QuestionnaireQuestionsVersion0 # | QuestionnaireQuestionsVersion1
|
||||
|
|
@ -17,6 +17,8 @@ from src.user.schemas import (
|
|||
UserPostInvitationRequest,
|
||||
UserPostInvitationAcceptRequest,
|
||||
UserGetSelfOrgsResponse,
|
||||
UserPostInvitationResponse,
|
||||
UserPostInvitationAcceptResponse,
|
||||
)
|
||||
from src.user.dependencies import (
|
||||
user_model_claims_dependency,
|
||||
|
|
@ -153,6 +155,7 @@ async def get_user_orgs(user_model: user_model_claims_dependency):
|
|||
"/invitation",
|
||||
summary="Send an email invitation for a user to join an org",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=UserPostInvitationResponse,
|
||||
)
|
||||
async def invitation(
|
||||
background_tasks: BackgroundTasks,
|
||||
|
|
@ -167,13 +170,19 @@ async def invitation(
|
|||
send_invitation, org_id=org_id, org_name=org_name, user_email=user_email
|
||||
)
|
||||
|
||||
return "Invitation sent"
|
||||
response = {
|
||||
"organisation": org_model,
|
||||
"invited_email": user_email,
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/invitation/accept",
|
||||
summary="Accept email invitation to join an org",
|
||||
status_code=status.HTTP_200_OK,
|
||||
response_model=UserPostInvitationAcceptResponse,
|
||||
)
|
||||
async def accept_invitation(
|
||||
db: db_dependency,
|
||||
|
|
@ -189,6 +198,13 @@ async def accept_invitation(
|
|||
raise OrgNotFoundException()
|
||||
|
||||
org_model.user_rel.append(user_model)
|
||||
db.flush()
|
||||
|
||||
response = {
|
||||
"organisation": org_model,
|
||||
"user": user_model,
|
||||
}
|
||||
|
||||
db.commit()
|
||||
|
||||
return "Invitation accepted"
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from typing import Optional
|
|||
from pydantic import EmailStr
|
||||
|
||||
from src.organisation.schemas import OrgSchema
|
||||
from src.schemas import CustomBaseModel, OrgIDMixin
|
||||
from src.schemas import CustomBaseModel, OrgIDMixin, OrgSummary, UserSummary
|
||||
|
||||
|
||||
class OIDCClaims(CustomBaseModel):
|
||||
|
|
@ -60,3 +60,13 @@ class UserPostInvitationAcceptRequest(CustomBaseModel):
|
|||
|
||||
class UserGetSelfOrgsResponse(CustomBaseModel):
|
||||
organisations: list[OrgSchema]
|
||||
|
||||
|
||||
class UserPostInvitationResponse(CustomBaseModel):
|
||||
organisation: OrgSummary
|
||||
invited_email: EmailStr
|
||||
|
||||
|
||||
class UserPostInvitationAcceptResponse(CustomBaseModel):
|
||||
organisation: OrgSummary
|
||||
user: UserSummary
|
||||
|
|
|
|||
|
|
@ -103,7 +103,10 @@ def _seed(db):
|
|||
owner_contact_id=2,
|
||||
security_contact_id=3,
|
||||
status="approved",
|
||||
intake_questionnaire={"question_two": "answer two"},
|
||||
intake_questionnaire={
|
||||
"metadata": {"version": 0, "submission_date": None},
|
||||
"questions": {"question_two": "answer two"},
|
||||
},
|
||||
)
|
||||
)
|
||||
db.add(Service(name="Test Service", api_key="123456789"))
|
||||
|
|
@ -167,11 +170,25 @@ def get_testable_routes():
|
|||
if method in {"HEAD", "OPTIONS"}:
|
||||
continue
|
||||
|
||||
routes.append((method, route.path, route.status_code, route.response_model))
|
||||
routes.append(
|
||||
(
|
||||
method,
|
||||
route.path,
|
||||
route.status_code,
|
||||
route.response_model,
|
||||
route.summary,
|
||||
)
|
||||
)
|
||||
|
||||
return routes
|
||||
|
||||
|
||||
# with open("endpoints.txt", "w") as f:
|
||||
# for ep in get_testable_routes():
|
||||
# f.write(f"[{ep[0]}]{ep[1]}({ep[2]}) -> {ep[2]}: {ep[3]}\n")
|
||||
# f.write(f"[{ep[0]}]({ep[1]}) -> {ep[2]}: {ep[3]}\n")
|
||||
#
|
||||
#
|
||||
### Docstring formatted output ###
|
||||
# with open("endpoints.txt", "w") as f:
|
||||
# for ep in get_testable_routes():
|
||||
# f.write(f"- [{ep[0]}]({ep[1]}): []: {ep[4]}\n")
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@ async def test_get_org_auth_root_su(default_client: AsyncClient, db_session):
|
|||
owner_contact_id=2,
|
||||
security_contact_id=3,
|
||||
status="approved",
|
||||
intake_questionnaire={},
|
||||
intake_questionnaire={
|
||||
"metadata": {"version": 0, "submission_date": None},
|
||||
"questions": {},
|
||||
},
|
||||
)
|
||||
)
|
||||
db_session.flush()
|
||||
|
|
|
|||
|
|
@ -587,7 +587,7 @@ async def test_post_perm_success(default_client: AsyncClient, db_session):
|
|||
"/iam/permission",
|
||||
json={"service_id": 1, "resource": "test_resource", "action": "create"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.status_code == 201
|
||||
|
||||
data = resp.json()
|
||||
|
||||
|
|
|
|||
|
|
@ -103,9 +103,13 @@ async def test_patch_org_questionnaire_partial_success(
|
|||
assert data["status"] == "partial"
|
||||
assert "intake_questionnaire" in data
|
||||
assert isinstance(data["intake_questionnaire"], dict)
|
||||
assert data["intake_questionnaire"]["question_one"] == "new answer one"
|
||||
assert data["intake_questionnaire"]["question_two"] == "answer two"
|
||||
assert data["intake_questionnaire"]["question_three"] is None
|
||||
metadata = data["intake_questionnaire"]["metadata"]
|
||||
assert metadata["version"] == 0
|
||||
assert metadata["submission_date"] is None
|
||||
questions = data["intake_questionnaire"]["questions"]
|
||||
assert questions["question_one"] == "new answer one"
|
||||
assert questions["question_two"] == "answer two"
|
||||
assert questions["question_three"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -172,9 +176,13 @@ async def test_patch_org_questionnaire_submit_success(
|
|||
assert data["status"] == "submitted"
|
||||
assert "intake_questionnaire" in data
|
||||
assert isinstance(data["intake_questionnaire"], dict)
|
||||
assert data["intake_questionnaire"]["question_one"] == "new answer one"
|
||||
assert data["intake_questionnaire"]["question_two"] == "answer two"
|
||||
assert data["intake_questionnaire"]["question_three"] is None
|
||||
metadata = data["intake_questionnaire"]["metadata"]
|
||||
assert metadata["version"] == 0
|
||||
assert metadata["submission_date"] is not None
|
||||
questions = data["intake_questionnaire"]["questions"]
|
||||
assert questions["question_one"] == "new answer one"
|
||||
assert questions["question_two"] == "answer two"
|
||||
assert questions["question_three"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
|
|||
|
|
@ -70,7 +70,15 @@ async def test_post_user_invitation_success(default_client: AsyncClient):
|
|||
resp = await default_client.post("/user/invitation", json=body)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == "Invitation sent"
|
||||
data = resp.json()
|
||||
assert "organisation" in data
|
||||
assert isinstance(data["organisation"], dict)
|
||||
assert data["organisation"]["id"] == 1
|
||||
assert data["organisation"]["name"] == "Test Org"
|
||||
|
||||
assert "invited_email" in data
|
||||
assert isinstance(data["invited_email"], str)
|
||||
assert data["invited_email"] == "admin@test.com"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -161,9 +169,12 @@ async def test_get_self_orgs_dynamic(default_client: AsyncClient):
|
|||
"security_contact": {"email": "security@test.org", "id": 3},
|
||||
"billing_contact": {"email": "billing@test.org", "id": 1},
|
||||
"intake_questionnaire": {
|
||||
"question_one": None,
|
||||
"question_three": None,
|
||||
"question_two": "answer two",
|
||||
"questions": {
|
||||
"question_one": None,
|
||||
"question_three": None,
|
||||
"question_two": "answer two",
|
||||
},
|
||||
"metadata": {"version": 0, "submission_date": None},
|
||||
},
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue