forked from sr2/cloud-api
parent
61e186a727
commit
dd0478d5e7
2 changed files with 138 additions and 30 deletions
|
|
@ -2,18 +2,20 @@
|
||||||
Router endpoints for IAM
|
Router endpoints for IAM
|
||||||
|
|
||||||
Endpoints:
|
Endpoints:
|
||||||
- [POST](/iam/can_act_on_resource): [API key & user claim]: Service access point to verify user permissions
|
- [POST](/api/v1/iam/can_act_on_resource): [API Key & User JWT]: Used for services to check user access permission
|
||||||
- [GET](/iam/group/permissions): [root user]: Gets list of perms(service, resource, action) the given group(id) has
|
- [GET](/api/v1/iam/group/permissions): [Root User]: Gets a list of permissions granted to a group
|
||||||
- [DELETE](/iam/group/permissions): [root user]: Removes a given perm(id) from the given group(id)
|
- [GET](/api/v1/iam/group/users): [Root User]: Gets a list of users assigned to a group
|
||||||
- [GET](/iam/group/users): [root user]: Gets a list of users(id, name, email) that are assigned to the given group(id)
|
- [POST](/api/v1/iam/group): [Root User]: Creates a new group
|
||||||
- [POST](/iam/group): [root user]: Creates a new group for the given org(id)
|
- [PUT](/api/v1/iam/group/permission): [Root User]: Grants a permission to a group
|
||||||
- [PUT](/iam/group/permission): [root user]: Assigns a perm(id) to the given group(id)
|
- [PUT](/api/v1/iam/group/user): [Root User]: Directly adds a user to the group
|
||||||
- [PUT](/iam/group/user): [root user]: Assigns a user(id) to a group(id)
|
- [DELETE](/api/v1/iam/group/permissions): [Root User]: Removes a permission from the group
|
||||||
- [DELETE](/iam/group/user): [root user]: Removes a user(id) from the given group(id)
|
- [DELETE](/api/v1/iam/group/user): [Root User]: Removes a user from the group
|
||||||
- [GET](/iam/permissions): [root user]: Gets a list of all permissions
|
- [GET](/api/v1/iam/permissions): [Root User]: Returns a full list of permissions
|
||||||
- [POST](/iam/permission): [super admin]: Creates a new permission
|
- [POST](/api/v1/iam/permission): [Super Admin]: Creates a new permission for a service
|
||||||
- [DELETE](/iam/permission): [super admin]: Removes a permission
|
- [DELETE](/api/v1/iam/permission): [Super Admin]: Deletes a permission
|
||||||
- [GET](/iam/permissions/search): [root user]: Returns a list of permissions matching a filter(service|resource|action)
|
- [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
|
from fastapi import APIRouter, status, BackgroundTasks
|
||||||
|
|
@ -281,7 +283,21 @@ async def add_group_permission(
|
||||||
return response
|
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(
|
async def add_group_user(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
group_model: group_model_body_dependency,
|
group_model: group_model_body_dependency,
|
||||||
|
|
@ -289,6 +305,11 @@ async def add_group_user(
|
||||||
org_model: org_model_root_claim_body_dependency,
|
org_model: org_model_root_claim_body_dependency,
|
||||||
request_model: IAMPutGroupUserRequest,
|
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:
|
if group_model.org_id != org_model.id:
|
||||||
raise UnauthorizedException("Group does not belong to this organization")
|
raise UnauthorizedException("Group does not belong to this organization")
|
||||||
|
|
||||||
|
|
@ -309,13 +330,26 @@ async def add_group_user(
|
||||||
return response
|
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(
|
async def remove_group_permissions(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
group_model: group_model_query_dependency,
|
group_model: group_model_query_dependency,
|
||||||
perm_model: perm_model_query_dependency,
|
perm_model: perm_model_query_dependency,
|
||||||
org_model: org_model_root_claim_query_dependency,
|
org_model: org_model_root_claim_query_dependency,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Removes a permission from the group.
|
||||||
|
"""
|
||||||
if group_model.org_id != org_model.id:
|
if group_model.org_id != org_model.id:
|
||||||
raise UnauthorizedException("Group does not belong to this organization")
|
raise UnauthorizedException("Group does not belong to this organization")
|
||||||
|
|
||||||
|
|
@ -329,13 +363,26 @@ async def remove_group_permissions(
|
||||||
return response
|
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(
|
async def remove_group_user(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
group_model: group_model_query_dependency,
|
group_model: group_model_query_dependency,
|
||||||
user_model: user_model_query_dependency,
|
user_model: user_model_query_dependency,
|
||||||
org_model: org_model_root_claim_query_dependency,
|
org_model: org_model_root_claim_query_dependency,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Removes a user from the group.
|
||||||
|
"""
|
||||||
if group_model.org_id != org_model.id:
|
if group_model.org_id != org_model.id:
|
||||||
raise UnauthorizedException("Group does not belong to this organization")
|
raise UnauthorizedException("Group does not belong to this organization")
|
||||||
|
|
||||||
|
|
@ -349,21 +396,47 @@ async def remove_group_user(
|
||||||
return response
|
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(
|
async def get_permissions(
|
||||||
db: db_dependency, org_model: org_model_root_claim_query_dependency
|
db: db_dependency, org_model: org_model_root_claim_query_dependency
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Returns a full list of permissions.
|
||||||
|
"""
|
||||||
permission_models = db.query(Perm).all()
|
permission_models = db.query(Perm).all()
|
||||||
|
|
||||||
return {"permissions": permission_models}
|
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(
|
async def create_new_permission(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
su: super_admin_dependency,
|
su: super_admin_dependency,
|
||||||
request_model: IAMPostPermissionRequest,
|
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)
|
service_model = db.get(Service, request_model.service_id)
|
||||||
if service_model is None:
|
if service_model is None:
|
||||||
raise ServiceNotFoundException(service_id=request_model.service_id)
|
raise ServiceNotFoundException(service_id=request_model.service_id)
|
||||||
|
|
@ -387,22 +460,40 @@ async def create_new_permission(
|
||||||
return {"permission": response}
|
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(
|
async def delete_permission(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
su: super_admin_dependency,
|
su: super_admin_dependency,
|
||||||
perm_model: perm_model_query_dependency,
|
perm_model: perm_model_query_dependency,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Allows a super admin to remove a permission.
|
||||||
|
"""
|
||||||
db.delete(perm_model)
|
db.delete(perm_model)
|
||||||
db.commit()
|
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(
|
async def post_permissions(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
org_model: org_model_root_claim_body_dependency,
|
org_model: org_model_root_claim_body_dependency,
|
||||||
request_model: IAMGetPermissionsSearchRequest,
|
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)
|
permission_query = db.query(Perm)
|
||||||
|
|
||||||
if request_model.service_id is not None:
|
if request_model.service_id is not None:
|
||||||
|
|
@ -424,9 +515,10 @@ async def post_permissions(
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/group/user/invitation",
|
path="/group/user/invitation",
|
||||||
summary="Send an email invitation for non-org member to join a group",
|
summary="Send an email invitation for non-org member to join a group",
|
||||||
status_code=status.HTTP_200_OK,
|
status_code=status.HTTP_200_OK,
|
||||||
|
responses={},
|
||||||
)
|
)
|
||||||
async def invitation(
|
async def invitation(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
|
|
@ -434,11 +526,17 @@ async def invitation(
|
||||||
group_model: group_model_body_dependency,
|
group_model: group_model_body_dependency,
|
||||||
request_model: IAMPutGroupInvitationRequest,
|
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
|
user_email = request_model.user_email
|
||||||
group_id = group_model.id
|
group_id: int = group_model.id
|
||||||
group_name = group_model.name
|
group_name: str = group_model.name
|
||||||
|
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
send_user_group_invitation,
|
send_user_group_invitation,
|
||||||
|
|
@ -453,32 +551,42 @@ async def invitation(
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/group/user//invitation/accept",
|
path="/group/user//invitation/accept",
|
||||||
summary="Accept email invitation to join an org's group",
|
summary="Accept email invitation to join an org's group",
|
||||||
status_code=status.HTTP_200_OK,
|
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(
|
async def accept_invitation(
|
||||||
db: db_dependency,
|
db: db_dependency,
|
||||||
user_model: user_model_claims_dependency,
|
user_model: user_model_claims_dependency,
|
||||||
request_model: IAMPutGroupInvitationAcceptRequest,
|
request_model: IAMPutGroupInvitationAcceptRequest,
|
||||||
):
|
):
|
||||||
|
"""
|
||||||
|
Accepts an invitation to join an org's group
|
||||||
|
"""
|
||||||
email_claims = await verify_email_token(
|
email_claims = await verify_email_token(
|
||||||
token=request_model.jwt, user_model=user_model
|
token=request_model.jwt, user_model=user_model
|
||||||
)
|
)
|
||||||
|
|
||||||
org_model = db.get(Org, email_claims["org_id"])
|
org_model = db.get(Org, email_claims["org_id"])
|
||||||
if org_model is None:
|
if org_model is None:
|
||||||
raise OrgNotFoundException()
|
raise OrgNotFoundException(email_claims["org_id"])
|
||||||
|
|
||||||
group_model = db.get(Group, email_claims["group_id"])
|
group_model = db.get(Group, email_claims["group_id"])
|
||||||
if group_model is None:
|
if group_model is None:
|
||||||
raise GroupNotFoundException()
|
raise GroupNotFoundException(email_claims["group_id"])
|
||||||
|
|
||||||
if group_model not in org_model.group_rel:
|
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:
|
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)
|
group_model.user_rel.append(user_model)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
|
||||||
|
|
@ -587,7 +587,7 @@ async def test_post_perm_success(default_client: AsyncClient, db_session):
|
||||||
"/iam/permission",
|
"/iam/permission",
|
||||||
json={"service_id": 1, "resource": "test_resource", "action": "create"},
|
json={"service_id": 1, "resource": "test_resource", "action": "create"},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 201
|
||||||
|
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue