Skip to content

Commit

Permalink
feat(onboarding): backend to allow members to delete sent invites (#7…
Browse files Browse the repository at this point in the history
…6734)

If members have permission to invite members to an organization (the
org's `disable_member_invite` flag is `false`), we also want them to be
able to remove invitations that they sent.
  • Loading branch information
ameliahsu committed Aug 29, 2024
1 parent 6d8b040 commit 35d4c22
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 0 deletions.
35 changes: 35 additions & 0 deletions src/sentry/api/endpoints/organization_member/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
ERR_NO_AUTH = "You cannot remove this member with an unauthenticated API request."
ERR_INSUFFICIENT_ROLE = "You cannot remove a member who has more access than you."
ERR_INSUFFICIENT_SCOPE = "You are missing the member:admin scope."
ERR_MEMBER_INVITE = "Your role cannot remove an invitation that was sent by someone else."
ERR_ONLY_OWNER = "You cannot remove the only remaining owner of the organization."
ERR_UNINVITABLE = "You cannot send an invitation to a user who is already a full member."
ERR_EXPIRED = "You cannot resend an expired invitation without regenerating the token."
Expand Down Expand Up @@ -363,6 +364,32 @@ def _change_org_role(member: OrganizationMember, role: str) -> None:
tags={"target_org_role": role, "count": omt_update_count},
)

def _handle_deletion_by_member(
self,
request: Request,
organization: Organization,
member: OrganizationMember,
acting_member: OrganizationMember,
) -> Response:
# Members can only delete invitations
if not member.inviter_id:
return Response({"detail": ERR_INSUFFICIENT_SCOPE}, status=400)
# Members can only delete invitations that they sent
if member.inviter_id != acting_member.user_id:
return Response({"detail": ERR_MEMBER_INVITE}, status=400)

audit_data = member.get_audit_log_data()
member.delete()
self.create_audit_entry(
request=request,
organization=organization,
target_object=member.id,
target_user_id=member.user_id,
event=audit_log.get_event_id("MEMBER_REMOVE"),
data=audit_data,
)
return Response(status=204)

@extend_schema(
operation_id="Delete an Organization Member",
parameters=[
Expand Down Expand Up @@ -398,6 +425,14 @@ def delete(
else:
if acting_member != member:
if not request.access.has_scope("member:admin"):
if (
features.has("organizations:members-invite-teammates", organization)
and not organization.flags.disable_member_invite
and request.access.has_scope("member:invite")
):
return self._handle_deletion_by_member(
request, organization, member, acting_member
)
return Response({"detail": ERR_INSUFFICIENT_SCOPE}, status=400)
else:
can_manage = roles.can_manage(acting_member.role, member.role)
Expand Down
53 changes: 53 additions & 0 deletions tests/sentry/api/endpoints/test_organization_member_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,18 @@ def test_can_promote_team_member_to_role_where_team_roles_enabled(self, mock_get
class DeleteOrganizationMemberTest(OrganizationMemberTestBase):
method = "delete"

def setUp(self):
super().setUp()

self.curr_user = self.create_user("member@example.com")
self.curr_member = self.create_member(
organization=self.organization, role="member", user=self.curr_user
)
self.other_user = self.create_user("other@example.com")
self.other_member = self.create_member(
organization=self.organization, role="member", user=self.other_user
)

def test_simple(self):
member = self.create_user("bar@example.com")
member_om = self.create_member(organization=self.organization, user=member, role="member")
Expand Down Expand Up @@ -820,6 +832,47 @@ def test_cannot_delete_partnership_member(self):

self.get_error_response(self.organization.slug, member_om.id, status_code=403)

@with_feature("organizations:members-invite-teammates")
def test_member_delete_pending_invite(self):
curr_invite = self.create_member(
organization=self.organization,
user=None,
email="member_invite@example.com",
role="member",
inviter_id=self.curr_user.id,
)
other_invite = self.create_member(
organization=self.organization,
user=None,
email="other_invite@example.com",
role="member",
inviter_id=self.other_user.id,
)

self.login_as(self.curr_user)

self.organization.flags.disable_member_invite = True
self.organization.save()
self.get_error_response(self.organization.slug, curr_invite.id, status_code=400)
self.get_error_response(self.organization.slug, curr_invite.id, status_code=400)

self.organization.flags.disable_member_invite = False
self.organization.save()
self.get_success_response(self.organization.slug, curr_invite.id)
self.get_error_response(self.organization.slug, other_invite.id, status_code=400)

@with_feature("organizations:members-invite-teammates")
def test_member_cannot_delete_members(self):
self.login_as(self.curr_user)

self.organization.flags.disable_member_invite = True
self.organization.save()
self.get_error_response(self.organization.slug, self.other_member.id, status_code=400)

self.organization.flags.disable_member_invite = False
self.organization.save()
self.get_error_response(self.organization.slug, self.other_member.id, status_code=400)

@override_settings(SENTRY_SELF_HOSTED=False)
@override_options({"superuser.read-write.ga-rollout": True})
def test_cannot_delete_as_superuser_read(self):
Expand Down

0 comments on commit 35d4c22

Please sign in to comment.