diff --git a/src/exceptions.py b/src/exceptions.py index 8b3629c..5a6ed3a 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -27,3 +27,12 @@ class ConflictException(HTTPException): status_code=status.HTTP_409_CONFLICT, detail=detail, ) + + +class ForbiddenException(HTTPException): + def __init__(self, message: Optional[str] = None) -> None: + detail = "Forbidden" if not message else message + super().__init__( + status_code=status.HTTP_403_FORBIDDEN, + detail=detail, + ) diff --git a/src/iam/router.py b/src/iam/router.py index 07bd960..5a50f42 100644 --- a/src/iam/router.py +++ b/src/iam/router.py @@ -23,7 +23,7 @@ from src.iam.exceptions import GroupNotFoundException from src.organisation.exceptions import OrgNotFoundException from src.schemas import GroupSummary, OrgSummary from src.service.exceptions import ServiceNotFoundException -from src.exceptions import ConflictException +from src.exceptions import ConflictException, ForbiddenException from src.database import db_dependency from src.auth.exceptions import UnauthorizedException from src.auth.service import claims_dependency @@ -75,7 +75,7 @@ from src.iam.schemas import ( IAMPutGroupInvitationRequest, IAMPutGroupInvitationAcceptRequest, ) -from src.utils import decode_jwt +from src.utils import verify_email_token router = APIRouter( tags=["IAM"], @@ -211,6 +211,11 @@ async def add_group_user( if user_model in group_model.user_rel: raise ConflictException("User already in group") + if user_model not in org_model.user_rel: + raise ForbiddenException( + "Adding users directly can only be done with org members. Use email invitation instead." + ) + group_model.user_rel.append(user_model) db.flush() response = IAMPutGroupUserResponse( @@ -373,11 +378,9 @@ async def accept_invitation( 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.") + 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: diff --git a/src/user/router.py b/src/user/router.py index 2989785..9a3b78a 100644 --- a/src/user/router.py +++ b/src/user/router.py @@ -10,7 +10,6 @@ Endpoints: from fastapi import APIRouter, status, BackgroundTasks -from src.auth.exceptions import UnauthorizedException from src.organisation.exceptions import OrgNotFoundException from src.user.schemas import ( UserResponse, @@ -32,7 +31,7 @@ from src.auth.dependencies import ( ) from src.auth.service import claims_dependency from src.database import db_dependency -from src.utils import decode_jwt +from src.utils import verify_email_token router = APIRouter( prefix="/user", @@ -181,11 +180,9 @@ async def accept_invitation( user_model: user_model_claims_dependency, request_model: UserPostInvitationAcceptRequest, ): - 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.") + 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: diff --git a/src/utils.py b/src/utils.py index 0724011..5f1df8d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from joserfc import jwt, jwk, errors from src.auth.exceptions import UnauthorizedException @@ -21,6 +22,22 @@ async def decode_jwt(encoded): raise UnauthorizedException("Invalid JWS") +async def verify_email_token(user_model, token): + email_claims = await decode_jwt(token) + + claimed_email = email_claims["email"] + + expiry = datetime.fromtimestamp(email_claims["exp"], timezone.utc) + + if expiry < datetime.now(timezone.utc): + raise UnauthorizedException("Invitation expired.") + + if user_model.email != claimed_email: + raise UnauthorizedException("The logged in user and email do not match.") + + return email_claims + + async def send_email(recipient: str, subject: str, body: str): print(recipient) print(subject) diff --git a/test/test_iam.py b/test/test_iam.py index 0176f9d..7f99de9 100644 --- a/test/test_iam.py +++ b/test/test_iam.py @@ -4,7 +4,7 @@ import pytest from httpx import AsyncClient from src.user.models import User -from src.organisation.models import Organisation as Org +from src.organisation.models import Organisation as Org, OrgUsers from src.iam.models import Group from .conftest import generate_query_and_status @@ -468,6 +468,8 @@ async def test_put_group_user_success(default_client: AsyncClient, db_session): ) ) db_session.flush() + db_session.add(OrgUsers(user_id=2, org_id=1)) + db_session.flush() resp = await default_client.put( "/iam/group/user", json={"user_id": 2, "group_id": 1, "organisation_id": 1}