feat: sua added to group invitations
All checks were successful
ci / lint_and_test (push) Successful in 13s
All checks were successful
ci / lint_and_test (push) Successful in 13s
Issue: #23
This commit is contained in:
parent
7809df4c5a
commit
768a3881ef
4 changed files with 110 additions and 3 deletions
|
|
@ -16,9 +16,11 @@ Endpoints:
|
||||||
- [GET](/iam/permissions/search): [root user]: Returns a list of permissions matching a filter(service|resource|action)
|
- [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, BackgroundTasks
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
|
from src.iam.exceptions import GroupNotFoundException
|
||||||
|
from src.organisation.exceptions import OrgNotFoundException
|
||||||
from src.service.exceptions import ServiceNotFoundException
|
from src.service.exceptions import ServiceNotFoundException
|
||||||
from src.exceptions import ConflictException
|
from src.exceptions import ConflictException
|
||||||
from src.database import db_dependency
|
from src.database import db_dependency
|
||||||
|
|
@ -34,11 +36,12 @@ from src.user.models import User
|
||||||
from src.user.dependencies import (
|
from src.user.dependencies import (
|
||||||
user_model_body_dependency,
|
user_model_body_dependency,
|
||||||
user_model_query_dependency,
|
user_model_query_dependency,
|
||||||
|
user_model_claims_dependency,
|
||||||
)
|
)
|
||||||
from src.organisation.models import Organisation as Org
|
from src.organisation.models import Organisation as Org
|
||||||
from src.service.models import Service
|
from src.service.models import Service
|
||||||
|
|
||||||
from src.iam.service import service_key_dependency
|
from src.iam.service import service_key_dependency, send_user_group_invitation
|
||||||
from src.iam.models import (
|
from src.iam.models import (
|
||||||
Permission as Perm,
|
Permission as Perm,
|
||||||
GroupPermissions as GPerms,
|
GroupPermissions as GPerms,
|
||||||
|
|
@ -68,7 +71,10 @@ from src.iam.schemas import (
|
||||||
IAMPostPermissionResponse,
|
IAMPostPermissionResponse,
|
||||||
IAMGetPermissionsSearchRequest,
|
IAMGetPermissionsSearchRequest,
|
||||||
IAMGetPermissionsSearchResponse,
|
IAMGetPermissionsSearchResponse,
|
||||||
|
IAMPutGroupInvitationRequest,
|
||||||
|
IAMPutGroupInvitationAcceptRequest,
|
||||||
)
|
)
|
||||||
|
from src.utils import decode_jwt
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
tags=["IAM"],
|
tags=["IAM"],
|
||||||
|
|
@ -315,3 +321,68 @@ async def post_permissions(
|
||||||
permission_models = permission_query.all()
|
permission_models = permission_query.all()
|
||||||
|
|
||||||
return {"permissions": permission_models}
|
return {"permissions": permission_models}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/group/user/invitation",
|
||||||
|
summary="Send an email invitation for non-org member to join a group",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def invitation(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
|
org_model: org_model_root_claim_body_dependency,
|
||||||
|
group_model: group_model_body_dependency,
|
||||||
|
request_model: IAMPutGroupInvitationRequest,
|
||||||
|
):
|
||||||
|
org_id = org_model.id
|
||||||
|
org_name = org_model.name
|
||||||
|
user_email = request_model.user_email
|
||||||
|
group_id = group_model.id
|
||||||
|
group_name = group_model.name
|
||||||
|
|
||||||
|
background_tasks.add_task(
|
||||||
|
send_user_group_invitation,
|
||||||
|
org_id=org_id,
|
||||||
|
org_name=org_name,
|
||||||
|
user_email=user_email,
|
||||||
|
group_id=group_id,
|
||||||
|
group_name=group_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return "Invitation sent"
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/group/user//invitation/accept",
|
||||||
|
summary="Accept email invitation to join an org's group",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
async def accept_invitation(
|
||||||
|
db: db_dependency,
|
||||||
|
user_model: user_model_claims_dependency,
|
||||||
|
request_model: IAMPutGroupInvitationAcceptRequest,
|
||||||
|
):
|
||||||
|
email_claims = await decode_jwt(request_model.jwt)
|
||||||
|
claimed_email = email_claims["email"]
|
||||||
|
|
||||||
|
if user_model.email != claimed_email:
|
||||||
|
raise UnauthorizedException("The logged in user and email do not match.")
|
||||||
|
|
||||||
|
org_model = db.get(Org, email_claims["org_id"])
|
||||||
|
if org_model is None:
|
||||||
|
raise OrgNotFoundException()
|
||||||
|
|
||||||
|
group_model = db.get(Group, email_claims["group_id"])
|
||||||
|
if group_model is None:
|
||||||
|
raise GroupNotFoundException()
|
||||||
|
|
||||||
|
if group_model not in org_model.group_rel:
|
||||||
|
raise UnauthorizedException("Group and org do not match.")
|
||||||
|
|
||||||
|
if user_model in group_model.user_rel:
|
||||||
|
raise ConflictException(message="User already in group.")
|
||||||
|
|
||||||
|
group_model.user_rel.append(user_model)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return "Invitation accepted"
|
||||||
|
|
|
||||||
|
|
@ -108,3 +108,11 @@ class IAMGetPermissionsSearchRequest(OrgIDMixin):
|
||||||
|
|
||||||
class IAMGetPermissionsSearchResponse(CustomBaseModel):
|
class IAMGetPermissionsSearchResponse(CustomBaseModel):
|
||||||
permissions: list[PermissionSchema]
|
permissions: list[PermissionSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class IAMPutGroupInvitationRequest(OrgIDMixin, GroupIDMixin):
|
||||||
|
user_email: EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
class IAMPutGroupInvitationAcceptRequest(CustomBaseModel):
|
||||||
|
jwt: str
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@ Exports:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from src.service.models import Service
|
from src.service.models import Service
|
||||||
from src.database import db_dependency
|
from src.database import db_dependency
|
||||||
from src.schemas import ResourceName
|
from src.schemas import ResourceName
|
||||||
from src.auth.exceptions import UnauthorizedException
|
from src.auth.exceptions import UnauthorizedException
|
||||||
|
from src.utils import send_email, generate_jwt
|
||||||
|
|
||||||
|
|
||||||
from fastapi import Request, Depends
|
from fastapi import Request, Depends
|
||||||
|
|
||||||
|
|
@ -33,3 +36,28 @@ def valid_service_key(db: db_dependency, request: Request, rn: ResourceName) ->
|
||||||
|
|
||||||
|
|
||||||
service_key_dependency = Annotated[bool, Depends(valid_service_key)]
|
service_key_dependency = Annotated[bool, Depends(valid_service_key)]
|
||||||
|
|
||||||
|
|
||||||
|
async def send_user_group_invitation(
|
||||||
|
user_email: str, org_name: str, org_id: int, group_id: int, group_name: str
|
||||||
|
):
|
||||||
|
expiry_delta = timedelta(hours=24)
|
||||||
|
expiry = datetime.now(timezone.utc) + expiry_delta
|
||||||
|
claims = {
|
||||||
|
"email": user_email,
|
||||||
|
"org_id": org_id,
|
||||||
|
"group_id": group_id,
|
||||||
|
"group_name": group_name,
|
||||||
|
"exp": expiry,
|
||||||
|
"type": "group_invite",
|
||||||
|
}
|
||||||
|
|
||||||
|
token = await generate_jwt(claims)
|
||||||
|
subject = f"You have been invited to join a group of {org_name}"
|
||||||
|
body = f"You have been invited to join {group_name}.\nClick the link to accept.\nfrontend.capture/send/to/endpoint/{token}"
|
||||||
|
|
||||||
|
await send_email(
|
||||||
|
recipient=user_email,
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ async def accept_invitation(
|
||||||
request_model: UserPostInvitationAcceptRequest,
|
request_model: UserPostInvitationAcceptRequest,
|
||||||
):
|
):
|
||||||
email_claims = await decode_jwt(request_model.jwt)
|
email_claims = await decode_jwt(request_model.jwt)
|
||||||
claimed_email = email_claims["user_email"]
|
claimed_email = email_claims["email"]
|
||||||
|
|
||||||
if user_model.email != claimed_email:
|
if user_model.email != claimed_email:
|
||||||
raise UnauthorizedException("The logged in user and email do not match.")
|
raise UnauthorizedException("The logged in user and email do not match.")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue