From 1cd00ca02e6c0bb114556abd1bb922b750366e8a Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Fri, 5 Nov 2021 11:43:17 -0700 Subject: [PATCH 1/4] pull out helper methods for organization_invite_request_details --- .../organization_invite_request_details.py | 77 +++++----------- src/sentry/models/organizationmember.py | 10 ++ src/sentry/utils/audit.py | 18 +++- src/sentry/utils/members.py | 92 +++++++++++++++++++ 4 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 src/sentry/utils/members.py diff --git a/src/sentry/api/endpoints/organization_invite_request_details.py b/src/sentry/api/endpoints/organization_invite_request_details.py index 9bdfc69aadaf2b..73e491f15c1767 100644 --- a/src/sentry/api/endpoints/organization_invite_request_details.py +++ b/src/sentry/api/endpoints/organization_invite_request_details.py @@ -1,23 +1,22 @@ -from django.conf import settings -from django.db.models import Q from rest_framework import serializers, status from rest_framework.response import Response -from sentry import features, roles +from sentry import roles from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.api.serializers.models.organization_member import OrganizationMemberWithTeamsSerializer -from sentry.models import AuditLogEntryEvent, InviteStatus, OrganizationMember -from sentry.signals import member_invited +from sentry.models import OrganizationMember +from sentry.utils.audit import get_api_key_for_audit_log +from sentry.utils.members import ( + approve_member_invitation, + reject_member_invitation, + validate_invitation, +) from .organization_member_details import get_allowed_roles from .organization_member_index import OrganizationMemberSerializer, save_team_assignments -ERR_CANNOT_INVITE = "Your organization is not allowed to invite members." -ERR_INSUFFICIENT_ROLE = "You do not have permission to invite that role." -ERR_JOIN_REQUESTS_DISABLED = "Your organization does not allow requests to join." - class ApproveInviteRequestSerializer(serializers.Serializer): approve = serializers.BooleanField(required=True, write_only=True) @@ -28,18 +27,8 @@ def validate_approve(self, approve): member = self.context["member"] allowed_roles = self.context["allowed_roles"] - if not features.has("organizations:invite-members", organization, actor=request.user): - raise serializers.ValidationError(ERR_CANNOT_INVITE) - - if ( - organization.get_option("sentry:join_requests") is False - and member.invite_status == InviteStatus.REQUESTED_TO_JOIN.value - ): - raise serializers.ValidationError(ERR_JOIN_REQUESTS_DISABLED) - - # members cannot invite roles higher than their own - if member.role not in {r.id for r in allowed_roles}: - raise serializers.ValidationError(ERR_INSUFFICIENT_ROLE) + # will raise validation errors + validate_invitation(member, organization, request.user, allowed_roles) return approve @@ -57,12 +46,8 @@ class OrganizationInviteRequestDetailsEndpoint(OrganizationEndpoint): def _get_member(self, organization, member_id): try: - return OrganizationMember.objects.get( - Q(invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value) - | Q(invite_status=InviteStatus.REQUESTED_TO_JOIN.value), - organization=organization, - user__isnull=True, - id=member_id, + return OrganizationMember.objects.get_member_invite_query(member_id).get( + organization=organization ) except ValueError: raise OrganizationMember.DoesNotExist() @@ -135,26 +120,13 @@ def put(self, request, organization, member_id): result = serializer.validated_data if result.get("approve") and not member.invite_approved: - member.approve_invite() - member.save() - - if settings.SENTRY_ENABLE_INVITES: - member.send_invite_email() - member_invited.send_robust( - member=member, - user=request.user, - sender=self, - referrer=request.data.get("referrer"), - ) - - self.create_audit_entry( - request=request, - organization_id=organization.id, - target_object=member.id, - data=member.get_audit_log_data(), - event=AuditLogEntryEvent.MEMBER_INVITE - if settings.SENTRY_ENABLE_INVITES - else AuditLogEntryEvent.MEMBER_ADD, + api_key = get_api_key_for_audit_log(request) + approve_member_invitation( + member, + request.user, + api_key, + request.META["REMOTE_ADDR"], + request.data.get("referrer"), ) return Response( @@ -180,14 +152,7 @@ def delete(self, request, organization, member_id): except OrganizationMember.DoesNotExist: raise ResourceDoesNotExist - member.delete() - - self.create_audit_entry( - request=request, - organization_id=organization.id, - target_object=member.id, - data=member.get_audit_log_data(), - event=AuditLogEntryEvent.INVITE_REQUEST_REMOVE, - ) + api_key = get_api_key_for_audit_log(request) + reject_member_invitation(member, request.user, api_key, request.META["REMOTE_ADDR"]) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/sentry/models/organizationmember.py b/src/sentry/models/organizationmember.py index fb8be98cab87ae..79b065fe04b321 100644 --- a/src/sentry/models/organizationmember.py +++ b/src/sentry/models/organizationmember.py @@ -64,6 +64,16 @@ def get_for_integration(self, integration: "Integration", actor: "User") -> Quer organization__organizationintegration__integration=integration, ).select_related("organization") + def get_member_invite_query(self, id: int) -> QuerySet: + return OrganizationMember.objects.filter( + invite_status__in=[ + InviteStatus.REQUESTED_TO_BE_INVITED.value, + InviteStatus.REQUESTED_TO_JOIN.value, + ], + user__isnull=True, + id=id, + ) + class OrganizationMember(Model): """ diff --git a/src/sentry/utils/audit.py b/src/sentry/utils/audit.py index 326a9b2ac23bac..8887dee84357ff 100644 --- a/src/sentry/utils/audit.py +++ b/src/sentry/utils/audit.py @@ -13,14 +13,18 @@ def create_audit_entry(request, transaction_id=None, logger=None, **kwargs): user = kwargs.pop("actor", request.user if request.user.is_authenticated else None) - api_key = ( - request.auth if hasattr(request, "auth") and isinstance(request.auth, ApiKey) else None - ) + api_key = get_api_key_for_audit_log(request) - entry = AuditLogEntry( - actor=user, actor_key=api_key, ip_address=request.META["REMOTE_ADDR"], **kwargs + return create_audit_entry_from_user( + user, api_key, request.META["REMOTE_ADDR"], transaction_id, logger, **kwargs ) + +def create_audit_entry_from_user( + user, api_key=None, ip_address=None, transaction_id=None, logger=None, **kwargs +): + entry = AuditLogEntry(actor=user, actor_key=api_key, ip_address=ip_address, **kwargs) + # Only create a real AuditLogEntry record if we are passing an event type # otherwise, we want to still log to our actual logging if entry.event is not None: @@ -55,6 +59,10 @@ def create_audit_entry(request, transaction_id=None, logger=None, **kwargs): return entry +def get_api_key_for_audit_log(request): + return request.auth if hasattr(request, "auth") and isinstance(request.auth, ApiKey) else None + + def create_org_delete_log(entry): delete_log = DeletedOrganization() organization = Organization.objects.get(id=entry.target_object) diff --git a/src/sentry/utils/members.py b/src/sentry/utils/members.py new file mode 100644 index 00000000000000..808833d13be42f --- /dev/null +++ b/src/sentry/utils/members.py @@ -0,0 +1,92 @@ +from django.conf import settings +from rest_framework import serializers + +from sentry import features, roles +from sentry.models import AuditLogEntryEvent, InviteStatus +from sentry.signals import member_invited +from sentry.utils.audit import create_audit_entry_from_user + +ERR_CANNOT_INVITE = "Your organization is not allowed to invite members." +ERR_JOIN_REQUESTS_DISABLED = "Your organization does not allow requests to join." + + +def validate_invitation(member, organization, user_to_approve, allowed_roles): + """" + Validates whether an org has the optionss to invite members, handle join requests,\ + and that the member role doens't exceed the allowed roles to invite + """ + if not features.has("organizations:invite-members", organization, actor=user_to_approve): + raise serializers.ValidationError(ERR_CANNOT_INVITE) + + if ( + organization.get_option("sentry:join_requests") is False + and member.invite_status == InviteStatus.REQUESTED_TO_JOIN.value + ): + raise serializers.ValidationError(ERR_JOIN_REQUESTS_DISABLED) + + # members cannot invite roles higher than their own + if member.role not in {r.id for r in allowed_roles}: + raise serializers.ValidationError( + f"You do not have permission approve a member invitation with the role {member.role}." + ) + + +def approve_member_invitation( + member, user_to_approve, api_key=None, ip_address=None, referrer=None +): + """ + Approve a member invite/join request and send an audit log entry + """ + member.approve_invite() + member.save() + + if settings.SENTRY_ENABLE_INVITES: + member.send_invite_email() + member_invited.send_robust( + member=member, + user=user_to_approve, + sender=approve_member_invitation, + referrer=referrer, + ) + + create_audit_entry_from_user( + user_to_approve, + api_key, + ip_address, + organization_id=member.organization_id, + target_object=member.id, + data=member.get_audit_log_data(), + event=AuditLogEntryEvent.MEMBER_INVITE + if settings.SENTRY_ENABLE_INVITES + else AuditLogEntryEvent.MEMBER_ADD, + ) + + +def reject_member_invitation( + member, + user_to_approve, + api_key=None, + ip_address=None, +): + """ + Reject a member invite/jin request and send an audit log entry + """ + member.delete() + + create_audit_entry_from_user( + user_to_approve, + api_key, + ip_address, + organization_id=member.organization_id, + target_object=member.id, + data=member.get_audit_log_data(), + event=AuditLogEntryEvent.INVITE_REQUEST_REMOVE, + ) + + +def get_allowed_roles_for_member(member): + """ + Return a list of roles which that member could invite + Must check if member member has member:admin first before checking + """ + return [r for r in roles.get_all() if r.priority <= roles.get(member.role).priority] From d5ffbcf4b2252e20d7f94af631b7e32aa19cf7fd Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Fri, 5 Nov 2021 13:34:02 -0700 Subject: [PATCH 2/4] fix typo --- src/sentry/utils/members.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sentry/utils/members.py b/src/sentry/utils/members.py index 808833d13be42f..ca95c160ef0271 100644 --- a/src/sentry/utils/members.py +++ b/src/sentry/utils/members.py @@ -11,9 +11,9 @@ def validate_invitation(member, organization, user_to_approve, allowed_roles): - """" - Validates whether an org has the optionss to invite members, handle join requests,\ - and that the member role doens't exceed the allowed roles to invite + """ + Validates whether an org has the options to invite members, handle join requests, + and that the member role doesn't exceed the allowed roles to invite. """ if not features.has("organizations:invite-members", organization, actor=user_to_approve): raise serializers.ValidationError(ERR_CANNOT_INVITE) From 6bf33a9cadc94deadc1af87339b6e4f57283c615 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Fri, 5 Nov 2021 15:12:58 -0700 Subject: [PATCH 3/4] move logic to organizationmember file --- .../organization_invite_request_details.py | 19 ++-- src/sentry/exceptions.py | 4 + src/sentry/models/organizationmember.py | 94 ++++++++++++++++++- src/sentry/utils/members.py | 92 ------------------ .../sentry/models/test_organizationmember.py | 94 ++++++++++++++++++- 5 files changed, 197 insertions(+), 106 deletions(-) delete mode 100644 src/sentry/utils/members.py diff --git a/src/sentry/api/endpoints/organization_invite_request_details.py b/src/sentry/api/endpoints/organization_invite_request_details.py index 73e491f15c1767..c91b189077d645 100644 --- a/src/sentry/api/endpoints/organization_invite_request_details.py +++ b/src/sentry/api/endpoints/organization_invite_request_details.py @@ -6,13 +6,8 @@ from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.api.serializers.models.organization_member import OrganizationMemberWithTeamsSerializer -from sentry.models import OrganizationMember +from sentry.models import OrganizationMember, UnableToAcceptMemberInvitationException from sentry.utils.audit import get_api_key_for_audit_log -from sentry.utils.members import ( - approve_member_invitation, - reject_member_invitation, - validate_invitation, -) from .organization_member_details import get_allowed_roles from .organization_member_index import OrganizationMemberSerializer, save_team_assignments @@ -23,12 +18,13 @@ class ApproveInviteRequestSerializer(serializers.Serializer): def validate_approve(self, approve): request = self.context["request"] - organization = self.context["organization"] member = self.context["member"] allowed_roles = self.context["allowed_roles"] - # will raise validation errors - validate_invitation(member, organization, request.user, allowed_roles) + try: + member.validate_invitation(request.user, allowed_roles) + except UnableToAcceptMemberInvitationException as err: + raise serializers.ValidationError(str(err)) return approve @@ -121,8 +117,7 @@ def put(self, request, organization, member_id): if result.get("approve") and not member.invite_approved: api_key = get_api_key_for_audit_log(request) - approve_member_invitation( - member, + member.approve_member_invitation( request.user, api_key, request.META["REMOTE_ADDR"], @@ -153,6 +148,6 @@ def delete(self, request, organization, member_id): raise ResourceDoesNotExist api_key = get_api_key_for_audit_log(request) - reject_member_invitation(member, request.user, api_key, request.META["REMOTE_ADDR"]) + member.reject_member_invitation(request.user, api_key, request.META["REMOTE_ADDR"]) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/src/sentry/exceptions.py b/src/sentry/exceptions.py index eb6b468cc1f0cd..2ca44e1c9cb6f3 100644 --- a/src/sentry/exceptions.py +++ b/src/sentry/exceptions.py @@ -65,3 +65,7 @@ class ApiTokenLimitError(Exception): class InvalidSearchQuery(Exception): pass + + +class UnableToAcceptMemberInvitationException(Exception): + pass diff --git a/src/sentry/models/organizationmember.py b/src/sentry/models/organizationmember.py index 79b065fe04b321..e23590853d84a6 100644 --- a/src/sentry/models/organizationmember.py +++ b/src/sentry/models/organizationmember.py @@ -15,11 +15,13 @@ from structlog import get_logger from bitfield import BitField -from sentry import roles +from sentry import features, roles from sentry.constants import ALERTS_MEMBER_WRITE_DEFAULT, EVENTS_MEMBER_ADMIN_DEFAULT from sentry.db.models import BoundedPositiveIntegerField, FlexibleForeignKey, Model, sane_repr from sentry.db.models.manager import BaseManager +from sentry.exceptions import UnableToAcceptMemberInvitationException from sentry.models.team import TeamStatus +from sentry.signals import member_invited from sentry.utils.http import absolute_uri if TYPE_CHECKING: @@ -42,6 +44,10 @@ class InviteStatus(Enum): } +ERR_CANNOT_INVITE = "Your organization is not allowed to invite members." +ERR_JOIN_REQUESTS_DISABLED = "Your organization does not allow requests to join." + + class OrganizationMemberManager(BaseManager): def get_contactable_members_for_org(self, organization_id: int) -> QuerySet: """Get a list of members we can contact for an organization through email.""" @@ -370,3 +376,89 @@ def get_scopes(self): scopes = frozenset(s for s in scopes if s not in disabled_scopes) return scopes + + def validate_invitation(self, user_to_approve, allowed_roles): + """ + Validates whether an org has the options to invite members, handle join requests, + and that the member role doesn't exceed the allowed roles to invite. + """ + organization = self.organization + if not features.has("organizations:invite-members", organization, actor=user_to_approve): + raise UnableToAcceptMemberInvitationException(ERR_CANNOT_INVITE) + + if ( + organization.get_option("sentry:join_requests") is False + and self.invite_status == InviteStatus.REQUESTED_TO_JOIN.value + ): + raise UnableToAcceptMemberInvitationException(ERR_JOIN_REQUESTS_DISABLED) + + # members cannot invite roles higher than their own + if self.role not in {r.id for r in allowed_roles}: + raise UnableToAcceptMemberInvitationException( + f"You do not have permission approve a member invitation with the role {self.role}." + ) + return True + + def approve_member_invitation( + self, user_to_approve, api_key=None, ip_address=None, referrer=None + ): + """ + Approve a member invite/join request and send an audit log entry + """ + from sentry.models.auditlogentry import AuditLogEntryEvent + from sentry.utils.audit import create_audit_entry_from_user + + self.approve_invite() + self.save() + + if settings.SENTRY_ENABLE_INVITES: + self.send_invite_email() + member_invited.send_robust( + member=self, + user=user_to_approve, + sender=self.approve_member_invitation, + referrer=referrer, + ) + + create_audit_entry_from_user( + user_to_approve, + api_key, + ip_address, + organization_id=self.organization_id, + target_object=self.id, + data=self.get_audit_log_data(), + event=AuditLogEntryEvent.MEMBER_INVITE + if settings.SENTRY_ENABLE_INVITES + else AuditLogEntryEvent.MEMBER_ADD, + ) + + def reject_member_invitation( + self, + user_to_approve, + api_key=None, + ip_address=None, + ): + """ + Reject a member invite/jin request and send an audit log entry + """ + from sentry.models.auditlogentry import AuditLogEntryEvent + from sentry.utils.audit import create_audit_entry_from_user + + self.delete() + + create_audit_entry_from_user( + user_to_approve, + api_key, + ip_address, + organization_id=self.organization_id, + target_object=self.id, + data=self.get_audit_log_data(), + event=AuditLogEntryEvent.INVITE_REQUEST_REMOVE, + ) + + def get_allowed_roles_to_invite(self): + """ + Return a list of roles which that member could invite + Must check if member member has member:admin first before checking + """ + return [r for r in roles.get_all() if r.priority <= roles.get(self.role).priority] diff --git a/src/sentry/utils/members.py b/src/sentry/utils/members.py deleted file mode 100644 index ca95c160ef0271..00000000000000 --- a/src/sentry/utils/members.py +++ /dev/null @@ -1,92 +0,0 @@ -from django.conf import settings -from rest_framework import serializers - -from sentry import features, roles -from sentry.models import AuditLogEntryEvent, InviteStatus -from sentry.signals import member_invited -from sentry.utils.audit import create_audit_entry_from_user - -ERR_CANNOT_INVITE = "Your organization is not allowed to invite members." -ERR_JOIN_REQUESTS_DISABLED = "Your organization does not allow requests to join." - - -def validate_invitation(member, organization, user_to_approve, allowed_roles): - """ - Validates whether an org has the options to invite members, handle join requests, - and that the member role doesn't exceed the allowed roles to invite. - """ - if not features.has("organizations:invite-members", organization, actor=user_to_approve): - raise serializers.ValidationError(ERR_CANNOT_INVITE) - - if ( - organization.get_option("sentry:join_requests") is False - and member.invite_status == InviteStatus.REQUESTED_TO_JOIN.value - ): - raise serializers.ValidationError(ERR_JOIN_REQUESTS_DISABLED) - - # members cannot invite roles higher than their own - if member.role not in {r.id for r in allowed_roles}: - raise serializers.ValidationError( - f"You do not have permission approve a member invitation with the role {member.role}." - ) - - -def approve_member_invitation( - member, user_to_approve, api_key=None, ip_address=None, referrer=None -): - """ - Approve a member invite/join request and send an audit log entry - """ - member.approve_invite() - member.save() - - if settings.SENTRY_ENABLE_INVITES: - member.send_invite_email() - member_invited.send_robust( - member=member, - user=user_to_approve, - sender=approve_member_invitation, - referrer=referrer, - ) - - create_audit_entry_from_user( - user_to_approve, - api_key, - ip_address, - organization_id=member.organization_id, - target_object=member.id, - data=member.get_audit_log_data(), - event=AuditLogEntryEvent.MEMBER_INVITE - if settings.SENTRY_ENABLE_INVITES - else AuditLogEntryEvent.MEMBER_ADD, - ) - - -def reject_member_invitation( - member, - user_to_approve, - api_key=None, - ip_address=None, -): - """ - Reject a member invite/jin request and send an audit log entry - """ - member.delete() - - create_audit_entry_from_user( - user_to_approve, - api_key, - ip_address, - organization_id=member.organization_id, - target_object=member.id, - data=member.get_audit_log_data(), - event=AuditLogEntryEvent.INVITE_REQUEST_REMOVE, - ) - - -def get_allowed_roles_for_member(member): - """ - Return a list of roles which that member could invite - Must check if member member has member:admin first before checking - """ - return [r for r in roles.get_all() if r.priority <= roles.get(member.role).priority] diff --git a/tests/sentry/models/test_organizationmember.py b/tests/sentry/models/test_organizationmember.py index b4355daf426bff..fe75d41b10445b 100644 --- a/tests/sentry/models/test_organizationmember.py +++ b/tests/sentry/models/test_organizationmember.py @@ -1,12 +1,16 @@ from datetime import timedelta from unittest.mock import patch +import pytest from django.core import mail from django.utils import timezone +from sentry import roles from sentry.auth import manager -from sentry.models import INVITE_DAYS_VALID, InviteStatus, OrganizationMember +from sentry.exceptions import UnableToAcceptMemberInvitationException +from sentry.models import INVITE_DAYS_VALID, InviteStatus, OrganizationMember, OrganizationOption from sentry.testutils import TestCase +from sentry.testutils.helpers import with_feature class OrganizationMemberTest(TestCase): @@ -245,3 +249,91 @@ def test_get_contactable_members_for_org(self): results = OrganizationMember.objects.get_contactable_members_for_org(organization.id) assert results.count() == 1 assert results[0].user_id == member.user_id + + def test_validate_invitation_success(self): + member = self.create_member( + organization=self.organization, + invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value, + email="hello@sentry.io", + role="member", + ) + user = self.create_user() + assert member.validate_invitation(user, [roles.get("member")]) + + @with_feature({"organizations:invite-members": False}) + def test_validate_invitation_lack_feature(self): + member = self.create_member( + organization=self.organization, + invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value, + email="hello@sentry.io", + role="member", + ) + user = self.create_user() + with pytest.raises( + UnableToAcceptMemberInvitationException, + match="Your organization is not allowed to invite members.", + ): + member.validate_invitation(user, [roles.get("member")]) + + def test_validate_invitation_no_join_requests(self): + OrganizationOption.objects.create( + organization_id=self.organization.id, key="sentry:join_requests", value=False + ) + + member = self.create_member( + organization=self.organization, + invite_status=InviteStatus.REQUESTED_TO_JOIN.value, + email="hello@sentry.io", + role="member", + ) + user = self.create_user() + with pytest.raises( + UnableToAcceptMemberInvitationException, + match="Your organization does not allow requests to join.", + ): + member.validate_invitation(user, [roles.get("member")]) + + def test_validate_invitation_outside_allowed_role(self): + member = self.create_member( + organization=self.organization, + invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value, + email="hello@sentry.io", + role="admin", + ) + user = self.create_user() + with pytest.raises( + UnableToAcceptMemberInvitationException, + match="You do not have permission approve a member invitation with the role admin.", + ): + member.validate_invitation(user, [roles.get("member")]) + + def test_approve_member_invitation(self): + member = self.create_member( + organization=self.organization, + invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value, + email="hello@sentry.io", + role="member", + ) + user = self.create_user() + member.approve_member_invitation(user) + assert member.invite_status == InviteStatus.APPROVED.value + + def test_reject_member_invitation(self): + member = self.create_member( + organization=self.organization, + invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value, + email="hello@sentry.io", + role="member", + ) + user = self.create_user() + member.reject_member_invitation(user) + assert not OrganizationMember.objects.filter(id=member.id).exists() + + def test_get_allowed_roles_to_invite(self): + member = OrganizationMember.objects.get(user=self.user, organization=self.organization) + member.update(role="manager") + assert member.get_allowed_roles_to_invite() == [ + roles.get("member"), + roles.get("admin"), + roles.get("manager"), + ] From 981bc47c6248ba86e387fbbb5c0bcfba15184623 Mon Sep 17 00:00:00 2001 From: Stephen Cefali Date: Fri, 5 Nov 2021 15:33:19 -0700 Subject: [PATCH 4/4] fix import --- .../api/endpoints/organization_invite_request_details.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sentry/api/endpoints/organization_invite_request_details.py b/src/sentry/api/endpoints/organization_invite_request_details.py index c91b189077d645..c5c6280e13d826 100644 --- a/src/sentry/api/endpoints/organization_invite_request_details.py +++ b/src/sentry/api/endpoints/organization_invite_request_details.py @@ -6,7 +6,8 @@ from sentry.api.exceptions import ResourceDoesNotExist from sentry.api.serializers import serialize from sentry.api.serializers.models.organization_member import OrganizationMemberWithTeamsSerializer -from sentry.models import OrganizationMember, UnableToAcceptMemberInvitationException +from sentry.exceptions import UnableToAcceptMemberInvitationException +from sentry.models import OrganizationMember from sentry.utils.audit import get_api_key_for_audit_log from .organization_member_details import get_allowed_roles