def post(self, request, organization): serializer = OrganizationMemberSerializer( data={ "email": request.data.get("userName"), "role": roles.get(organization.default_role).id, }, context={ "organization": organization, "allowed_roles": [roles.get(organization.default_role)], "allow_existing_invite_request": True, }, ) if not serializer.is_valid(): if "email" in serializer.errors and any( ("is already a member" in error) for error in serializer.errors["email"]): # we include conflict logic in the serializer, check to see if that was # our error and if so, return a 409 so the scim IDP knows how to handle raise ConflictError(detail=SCIM_409_USER_EXISTS) return Response(serializer.errors, status=400) result = serializer.validated_data with transaction.atomic(): member = OrganizationMember( organization=organization, email=result["email"], role=result["role"], inviter=request.user, ) # TODO: are invite tokens needed for SAML orgs? if settings.SENTRY_ENABLE_INVITES: member.token = member.generate_token() member.save() 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, ) if settings.SENTRY_ENABLE_INVITES and result.get("sendInvite"): member.send_invite_email() member_invited.send_robust( member=member, user=request.user, sender=self, referrer=request.data.get("referrer"), ) context = serialize( member, serializer=_scim_member_serializer_with_expansion(organization), ) return Response(context, status=201)
def post(self, request, organization): """ Add a Member to Organization ```````````````````````````` Invite a member to the organization. :pparam string organization_slug: the slug of the organization the member will belong to :param string email: the email address to invite :param string role: the role of the new member :param array teams: the slugs of the teams the member should belong to. :auth: required """ # TODO: If the member already exists, should this still update the role and team? # For now, it doesn't, but simply returns the existing object if not features.has('organizations:invite-members', organization, actor=request.user): return Response( { 'organization': 'Your organization is not allowed to invite members' }, status=403) serializer = OrganizationMemberSerializer(data=request.DATA) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.object _, allowed_roles = get_allowed_roles(request, organization) # ensure listed teams are real teams teams = list( Team.objects.filter( organization=organization, status=TeamStatus.VISIBLE, slug__in=result['teams'], )) if len(set(result['teams'])) != len(teams): return Response({'teams': 'Invalid team'}, 400) if not result['role'] in {r.id for r in allowed_roles}: return Response( {'role': 'You do not have permission to invite that role.'}, 403) # This is needed because `email` field is case sensitive, but from a user perspective, # Sentry treats email as case-insensitive ([email protected] equals [email protected]). existing = OrganizationMember.objects.filter( organization=organization, user__email__iexact=result['email'], user__is_active=True, ).exists() if existing: return Response( {'email': 'The user %s is already a member' % result['email']}, 409) om = OrganizationMember(organization=organization, email=result['email'], role=result['role']) if settings.SENTRY_ENABLE_INVITES: om.token = om.generate_token() try: with transaction.atomic(): om.save() except IntegrityError: return Response( {'email': 'The user %s is already a member' % result['email']}, 409) lock = locks.get(u'org:member:{}'.format(om.id), duration=5) with TimedRetryPolicy(10)(lock.acquire): self.save_team_assignments(om, teams) if settings.SENTRY_ENABLE_INVITES: om.send_invite_email() member_invited.send_robust(member=om, user=request.user, sender=self, referrer=request.DATA.get('referrer')) self.create_audit_entry( request=request, organization_id=organization.id, target_object=om.id, data=om.get_audit_log_data(), event=AuditLogEntryEvent.MEMBER_INVITE if settings.SENTRY_ENABLE_INVITES else AuditLogEntryEvent.MEMBER_ADD, ) return Response(serialize(om), status=201)
def post(self, request, organization): """ Add a Member to Organization ```````````````````````````` Invite a member to the organization. :pparam string organization_slug: the slug of the organization the member will belong to :param string email: the email address to invite :param string role: the role of the new member :param array teams: the slugs of the teams the member should belong to. :auth: required """ if not features.has("organizations:invite-members", organization, actor=request.user): return Response( { "organization": "Your organization is not allowed to invite members" }, status=403) _, allowed_roles = get_allowed_roles(request, organization) serializer = OrganizationMemberSerializer( data=request.data, context={ "organization": organization, "allowed_roles": allowed_roles, "allow_existing_invite_request": True, }, ) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data with transaction.atomic(): # remove any invitation requests for this email before inviting OrganizationMember.objects.filter( Q(invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value) | Q(invite_status=InviteStatus.REQUESTED_TO_JOIN.value), email=result["email"], organization=organization, ).delete() om = OrganizationMember( organization=organization, email=result["email"], role=result["role"], inviter=request.user, ) if settings.SENTRY_ENABLE_INVITES: om.token = om.generate_token() om.save() if result["teams"]: lock = locks.get(u"org:member:{}".format(om.id), duration=5) with TimedRetryPolicy(10)(lock.acquire): save_team_assignments(om, result["teams"]) if settings.SENTRY_ENABLE_INVITES and result.get("sendInvite"): om.send_invite_email() member_invited.send_robust(member=om, user=request.user, sender=self, referrer=request.data.get("referrer")) self.create_audit_entry( request=request, organization_id=organization.id, target_object=om.id, data=om.get_audit_log_data(), event=AuditLogEntryEvent.MEMBER_INVITE if settings.SENTRY_ENABLE_INVITES else AuditLogEntryEvent.MEMBER_ADD, ) return Response(serialize(om), status=201)
def post(self, request, organization): """ Add a Member to Organization ```````````````````````````` Invite a member to the organization. :pparam string organization_slug: the slug of the organization the member will belong to :param string email: the email address to invite :param string role: the role of the new member :param array teams: the slugs of the teams the member should belong to. :auth: required """ # TODO: If the member already exists, should this still update the role and team? # For now, it doesn't, but simply returns the existing object if not features.has('organizations:invite-members', organization, actor=request.user): return Response( {'organization': 'Your organization is not allowed to invite members'}, status=403) serializer = OrganizationMemberSerializer(data=request.DATA) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.object _, allowed_roles = get_allowed_roles(request, organization) # ensure listed teams are real teams teams = list(Team.objects.filter( organization=organization, status=TeamStatus.VISIBLE, slug__in=result['teams'], )) if len(set(result['teams'])) != len(teams): return Response({'teams': 'Invalid team'}, 400) if not result['role'] in {r.id for r in allowed_roles}: return Response({'role': 'You do not have permission to invite that role.'}, 403) # This is needed because `email` field is case sensitive, but from a user perspective, # Sentry treats email as case-insensitive ([email protected] equals [email protected]). existing = OrganizationMember.objects.filter( organization=organization, user__email__iexact=result['email'], user__is_active=True, ).exists() if existing: return Response({'email': 'The user %s is already a member' % result['email']}, 409) om = OrganizationMember( organization=organization, email=result['email'], role=result['role']) if settings.SENTRY_ENABLE_INVITES: om.token = om.generate_token() try: with transaction.atomic(): om.save() except IntegrityError: return Response({'email': 'The user %s is already a member' % result['email']}, 409) lock = locks.get(u'org:member:{}'.format(om.id), duration=5) with TimedRetryPolicy(10)(lock.acquire): self.save_team_assignments(om, teams) if settings.SENTRY_ENABLE_INVITES: om.send_invite_email() member_invited.send_robust(member=om, user=request.user, sender=self, referrer=request.DATA.get('referrer')) self.create_audit_entry( request=request, organization_id=organization.id, target_object=om.id, data=om.get_audit_log_data(), event=AuditLogEntryEvent.MEMBER_INVITE if settings.SENTRY_ENABLE_INVITES else AuditLogEntryEvent.MEMBER_ADD, ) return Response(serialize(om), status=201)
def delete( self, request: Request, organization: Organization, member: OrganizationMember, ) -> Response: """ Remove an organization member. """ if request.user.is_authenticated and not is_active_superuser(request): try: acting_member = OrganizationMember.objects.get( organization=organization, user=request.user) except OrganizationMember.DoesNotExist: return Response({"detail": ERR_INSUFFICIENT_ROLE}, status=400) else: if acting_member != member: if not request.access.has_scope("member:admin"): return Response({"detail": ERR_INSUFFICIENT_SCOPE}, status=400) elif not roles.can_manage(acting_member.role, member.role): return Response({"detail": ERR_INSUFFICIENT_ROLE}, status=400) # TODO(dcramer): do we even need this check? elif not request.access.has_scope("member:admin"): return Response({"detail": ERR_INSUFFICIENT_SCOPE}, status=400) if member.is_only_owner(): return Response({"detail": ERR_ONLY_OWNER}, status=403) audit_data = member.get_audit_log_data() with transaction.atomic(): AuthIdentity.objects.filter( user=member.user, auth_provider__organization=organization).delete() # Delete instances of `UserOption` that are scoped to the projects within the # organization when corresponding member is removed from org proj_list = Project.objects.filter( organization=organization).values_list("id", flat=True) uo_list = UserOption.objects.filter(user=member.user, project_id__in=proj_list, key="mail:email") for uo in uo_list: uo.delete() member.delete() self.create_audit_entry( request=request, organization=organization, target_object=member.id, target_user=member.user, event=AuditLogEntryEvent.MEMBER_REMOVE, data=audit_data, ) return Response(status=204)
def put( self, request: Request, organization: Organization, member: OrganizationMember, ) -> Response: serializer = OrganizationMemberSerializer(data=request.data, partial=True) if not serializer.is_valid(): return Response(status=400) try: auth_provider = AuthProvider.objects.get(organization=organization) auth_provider = auth_provider.get_provider() except AuthProvider.DoesNotExist: auth_provider = None allowed_roles = None result = serializer.validated_data # XXX(dcramer): if/when this expands beyond reinvite we need to check # access level if result.get("reinvite"): if member.is_pending: if ratelimits.for_organization_member_invite( organization=organization, email=member.email, user=request.user, auth=request.auth, ): metrics.incr( "member-invite.attempt", instance="rate_limited", skip_internal=True, sample_rate=1.0, ) return Response({"detail": ERR_RATE_LIMITED}, status=429) if result.get("regenerate"): if request.access.has_scope("member:admin"): member.regenerate_token() member.save() else: return Response({"detail": ERR_INSUFFICIENT_SCOPE}, status=400) if member.token_expired: return Response({"detail": ERR_EXPIRED}, status=400) member.send_invite_email() elif auth_provider and not getattr(member.flags, "sso:linked"): member.send_sso_link_email(request.user, auth_provider) else: # TODO(dcramer): proper error message return Response({"detail": ERR_UNINVITABLE}, status=400) if "teams" in result: # dupe code from member_index # ensure listed teams are real teams teams = list( Team.objects.filter(organization=organization, status=TeamStatus.VISIBLE, slug__in=result["teams"])) if len(set(result["teams"])) != len(teams): return Response({"teams": "Invalid team"}, status=400) with transaction.atomic(): # teams may be empty OrganizationMemberTeam.objects.filter( organizationmember=member).delete() OrganizationMemberTeam.objects.bulk_create([ OrganizationMemberTeam(team=team, organizationmember=member) for team in teams ]) if result.get("role"): _, allowed_roles = get_allowed_roles(request, organization) allowed_role_ids = {r.id for r in allowed_roles} # A user cannot promote others above themselves if result["role"] not in allowed_role_ids: return Response( { "role": "You do not have permission to assign the given role." }, status=403) # A user cannot demote a superior if member.role not in allowed_role_ids: return Response( { "role": "You do not have permission to assign a role to the given user." }, status=403, ) if member.user == request.user and (result["role"] != member.role): return Response( {"detail": "You cannot make changes to your own role."}, status=400) member.update(role=result["role"]) self.create_audit_entry( request=request, organization=organization, target_object=member.id, target_user=member.user, event=AuditLogEntryEvent.MEMBER_EDIT, data=member.get_audit_log_data(), ) context = self._serialize_member(member, request, allowed_roles) return Response(context)