diff --git a/src/sentry/api/endpoints/accept_organization_invite.py b/src/sentry/api/endpoints/accept_organization_invite.py index a5d736df30f3ec..7de4cd6cd34f28 100644 --- a/src/sentry/api/endpoints/accept_organization_invite.py +++ b/src/sentry/api/endpoints/accept_organization_invite.py @@ -3,6 +3,7 @@ import logging from collections.abc import Mapping +from django.contrib.auth.models import AnonymousUser from django.http import HttpRequest from django.urls import reverse from rest_framework import status @@ -31,7 +32,7 @@ def handle_empty_organization_id_or_slug( - member_id: int, user_id: int, request: HttpRequest + member_id: int, user_id: int | None, request: HttpRequest ) -> RpcUserInviteContext | None: member_mapping: OrganizationMemberMapping | None = None member_mappings: Mapping[int, OrganizationMemberMapping] = { @@ -73,7 +74,7 @@ def handle_empty_organization_id_or_slug( def get_invite_state( member_id: int, organization_id_or_slug: int | str | None, - user_id: int, + user_id: int | None, request: HttpRequest, ) -> RpcUserInviteContext | None: @@ -110,6 +111,16 @@ class AcceptOrganizationInvite(Endpoint): def respond_invalid() -> Response: return Response(status=status.HTTP_400_BAD_REQUEST, data={"details": "Invalid invite code"}) + @staticmethod + def respond_unauthorized(email: str) -> Response: + return Response( + status=status.HTTP_401_UNAUTHORIZED, + data={ + "details": "Active session account is not authorized to accept invite", + "user_email": email, + }, + ) + def get_helper( self, request: Request, token: str, invite_context: RpcUserOrganizationContext ) -> ApiInviteHelper: @@ -126,7 +137,7 @@ def get( invite_context = get_invite_state( member_id=int(member_id), organization_id_or_slug=organization_id_or_slug, - user_id=request.user.id, + user_id=None, # NOTE (mifu67): we want to get the invite context using the member ID only request=request, ) if invite_context is None: @@ -137,6 +148,15 @@ def get( organization_member = invite_context.member organization = invite_context.organization + # NOTE (mifu67): we get the invite context without passing in the user_id, so the + # invite context is generated solely using the passed member_id. Then, compare the + # email of the current session user against the invite context member email. If + # they're different, raise a 401 Unauthorized error, which causes the frontend to + # render a view prompting the user to log out and try again. + if not isinstance(request.user, AnonymousUser): + if organization_member.email != request.user.email: + return self.respond_unauthorized(request.user.email) + if ( not helper.member_pending or not helper.valid_token @@ -158,7 +178,7 @@ def get( "needsSso": auth_provider is not None, "hasAuthProvider": auth_provider is not None, "requireSso": auth_provider is not None and not auth_provider.flags.allow_unlinked, - # If they're already a member of the organization its likely + # If they're already a member of the organization it's likely # they're using a shared account and either previewing this invite # or are incorrectly expecting this to create a new account. "existingMember": helper.member_already_exists,