def test_accept_invite(self, get_audit, create_audit): om = OrganizationMember.objects.get(id=self.member.id) assert om.email == self.member.email helper = ApiInviteHelper(self.request, self.member.id, None) helper.accept_invite() om = OrganizationMember.objects.get(id=self.member.id) assert om.email is None assert om.user.id == self.user.id
def test_accept_invite_with_SSO(self, mock_provider, get_audit, create_audit): self.auth_provider.flags.allow_unlinked = True mock_provider.get.return_value = self.auth_provider om = OrganizationMember.objects.get(id=self.member.id) assert om.email == self.member.email helper = ApiInviteHelper(self.request, self.member.id, None) helper.accept_invite() om = OrganizationMember.objects.get(id=self.member.id) assert om.email is None assert om.user.id == self.user.id
def test_accept_invite_with_required_SSO(self, mock_provider, get_audit, create_audit): self.auth_provider.flags.allow_unlinked = False mock_provider.get.return_value = self.auth_provider om = OrganizationMember.objects.get(id=self.member.id) assert om.email == self.member.email helper = ApiInviteHelper(self.request, self.member.id, None) helper.accept_invite() # Invite cannot be accepted without AuthIdentity if SSO is required om = OrganizationMember.objects.get(id=self.member.id) assert om.email is not None assert om.user is None
def _handle_new_membership( self, auth_identity: AuthIdentity) -> Optional[OrganizationMember]: user = auth_identity.user # If the user is either currently *pending* invite acceptance (as indicated # from the pending-invite cookie) OR an existing invite exists on this # organziation for the email provided by the identity provider. invite_helper = ApiInviteHelper.from_cookie_or_email( request=self.request, organization=self.organization, email=user.email) # If we are able to accept an existing invite for the user for this # organization, do so, otherwise handle new membership if invite_helper: if invite_helper.invite_approved: return invite_helper.accept_invite(user) # It's possible the user has an _invite request_ that hasn't been approved yet, # and is able to join the organization without an invite through the SSO flow. # In that case, delete the invite request and create a new membership. invite_helper.handle_invite_not_approved() flags = OrganizationMember.flags["sso:linked"] # if the org doesn't have the ability to add members then anyone who got added # this way should be disabled until the org upgrades if not features.has("organizations:invite-members", self.organization): flags = flags | OrganizationMember.flags["member-limit:restricted"] # Otherwise create a new membership om = OrganizationMember.objects.create( organization=self.organization, role=self.organization.default_role, user=user, flags=flags, ) default_teams = self.auth_provider.default_teams.all() for team in default_teams: OrganizationMemberTeam.objects.create(team=team, organizationmember=om) AuditLogEntry.objects.create( organization=self.organization, actor=user, ip_address=self.request.META["REMOTE_ADDR"], target_object=om.id, target_user=om.user, event=AuditLogEntryEvent.MEMBER_ADD, data=om.get_audit_log_data(), ) return om
def handle_new_membership(auth_provider, organization, request, auth_identity): user = auth_identity.user # If the user is either currently *pending* invite acceptance (as indicated # from the pending-invite cookie) OR an existing invite exists on this # organziation for the email provided by the identity provider. invite_helper = ApiInviteHelper.from_cookie_or_email( request=request, organization=organization, email=user.email ) # If we are able to accept an existing invite for the user for this # organization, do so, otherwise handle new membership if invite_helper: if invite_helper.invite_approved: invite_helper.accept_invite(user) return # It's possible the user has an _invite request_ that hasn't been approved yet, # and is able to join the organization without an invite through the SSO flow. # In that case, delete the invite request and create a new membership. invite_helper.handle_invite_not_approved() # Otherwise create a new membership om = OrganizationMember.objects.create( organization=organization, role=organization.default_role, user=user, flags=OrganizationMember.flags["sso:linked"], ) default_teams = auth_provider.default_teams.all() for team in default_teams: OrganizationMemberTeam.objects.create(team=team, organizationmember=om) AuditLogEntry.objects.create( organization=organization, actor=user, ip_address=request.META["REMOTE_ADDR"], target_object=om.id, target_user=om.user, event=AuditLogEntryEvent.MEMBER_ADD, data=om.get_audit_log_data(), ) return om
def post(self, request, user, interface_id): """ Enroll in authenticator interface ````````````````````````````````` :pparam string user_id: user id or "me" for current user :pparam string interface_id: interface id :auth: required """ if ratelimiter.is_limited( f"auth:authenticator-enroll:{request.user.id}:{interface_id}", limit=10, window=86400, # 10 per day should be fine ): return HttpResponse( "You have made too many authenticator enrollment attempts. Please try again later.", content_type="text/plain", status=429, ) # Using `request.user` here because superuser should not be able to set a user's 2fa # start activation serializer_cls = serializer_map.get(interface_id, None) if serializer_cls is None: return Response(status=status.HTTP_404_NOT_FOUND) serializer = serializer_cls(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) interface = Authenticator.objects.get_interface( request.user, interface_id) # Not all interfaces allow multi enrollment # # This is probably un-needed because we catch # `Authenticator.AlreadyEnrolled` when attempting to enroll if interface.is_enrolled() and not interface.allow_multi_enrollment: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) try: interface.secret = request.data["secret"] except KeyError: pass context = {} # Need to update interface with phone number before validating OTP if "phone" in request.data: interface.phone_number = serializer.data["phone"] # Disregarding value of 'otp', if no OTP was provided, # send text message to phone number with OTP if "otp" not in request.data: if interface.send_text(for_enrollment=True, request=request._request): return Response(status=status.HTTP_204_NO_CONTENT) else: # Error sending text message return Response( SEND_SMS_ERR, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Attempt to validate OTP if "otp" in request.data and not interface.validate_otp( serializer.data["otp"]): return Response(INVALID_OTP_ERR, status=status.HTTP_400_BAD_REQUEST) # Try u2f enrollment if interface_id == "u2f": # What happens when this fails? interface.try_enroll( serializer.data["challenge"], serializer.data["response"], serializer.data["deviceName"], ) context.update({"device_name": serializer.data["deviceName"]}) try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) context.update({"authenticator": interface.authenticator}) capture_security_activity( account=request.user, type="mfa-added", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context=context, send_email=True, ) request.user.clear_lost_passwords() request.user.refresh_session_nonce(self.request) request.user.save() Authenticator.objects.auto_add_recovery_codes(request.user) response = Response(status=status.HTTP_204_NO_CONTENT) # If there is a pending organization invite accept after the # authenticator has been configured. invite_helper = ApiInviteHelper.from_cookie(request=request, instance=self, logger=logger) if invite_helper and invite_helper.valid_request: invite_helper.accept_invite() remove_invite_cookie(request, response) return response
def get_helper(self, request, member_id, token): return ApiInviteHelper(request=request, member_id=member_id, instance=self, token=token)
def handle_basic_auth(self, request, **kwargs): can_register = self.can_register(request) op = request.POST.get("op") organization = kwargs.pop("organization", None) if not op: # Detect that we are on the register page by url /register/ and # then activate the register tab by default. if "/register" in request.path_info and can_register: op = "register" elif request.GET.get("op") == "sso": op = "sso" login_form = self.get_login_form(request) if can_register: register_form = self.get_register_form( request, initial={"username": request.session.get("invite_email", "")} ) else: register_form = None if can_register and register_form.is_valid(): user = register_form.save() user.send_confirm_emails(is_new_user=True) user_signup.send_robust( sender=self, user=user, source="register-form", referrer="in-app" ) # HACK: grab whatever the first backend is and assume it works user.backend = settings.AUTHENTICATION_BACKENDS[0] auth.login(request, user, organization_id=organization.id if organization else None) # can_register should only allow a single registration request.session.pop("can_register", None) request.session.pop("invite_email", None) # In single org mode, associate the user to the orgnaization if settings.SENTRY_SINGLE_ORGANIZATION: organization = Organization.get_default() OrganizationMember.objects.create( organization=organization, role=organization.default_role, user=user ) # Attempt to directly accept any pending invites invite_helper = ApiInviteHelper.from_cookie(request=request, instance=self) if invite_helper and invite_helper.valid_request: invite_helper.accept_invite() response = self.redirect_to_org(request) remove_invite_cookie(request, response) return response return self.redirect(auth.get_login_redirect(request)) elif request.method == "POST": from sentry.app import ratelimiter from sentry.utils.hashlib import md5_text login_attempt = ( op == "login" and request.POST.get("username") and request.POST.get("password") ) if login_attempt and ratelimiter.is_limited( u"auth:login:username:{}".format( md5_text(login_form.clean_username(request.POST["username"])).hexdigest() ), limit=10, window=60, # 10 per minute should be enough for anyone ): login_form.errors["__all__"] = [ u"You have made too many login attempts. Please try again later." ] metrics.incr( "login.attempt", instance="rate_limited", skip_internal=True, sample_rate=1.0 ) elif login_form.is_valid(): user = login_form.get_user() auth.login(request, user, organization_id=organization.id if organization else None) metrics.incr( "login.attempt", instance="success", skip_internal=True, sample_rate=1.0 ) if not user.is_active: return self.redirect(reverse("sentry-reactivate-account")) return self.redirect(auth.get_login_redirect(request)) else: metrics.incr( "login.attempt", instance="failure", skip_internal=True, sample_rate=1.0 ) context = { "op": op or "login", "server_hostname": get_server_hostname(), "login_form": login_form, "organization": organization, "register_form": register_form, "CAN_REGISTER": can_register, "join_request_link": self.get_join_request_link(organization), } context.update(additional_context.run_callbacks(request)) return self.respond_login(request, context, **kwargs)
def post(self, request, user, interface_id): """ Enroll in authenticator interface ````````````````````````````````` :pparam string user_id: user id or "me" for current user :pparam string interface_id: interface id :auth: required """ # Using `request.user` here because superuser should not be able to set a user's 2fa # start activation serializer_cls = serializer_map.get(interface_id, None) if serializer_cls is None: return Response(status=status.HTTP_404_NOT_FOUND) serializer = serializer_cls(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) interface = Authenticator.objects.get_interface( request.user, interface_id) # Not all interfaces allow multi enrollment # # This is probably un-needed because we catch # `Authenticator.AlreadyEnrolled` when attempting to enroll if interface.is_enrolled and not interface.allow_multi_enrollment: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) try: interface.secret = request.data["secret"] except KeyError: pass context = {} # Need to update interface with phone number before validating OTP if "phone" in request.data: interface.phone_number = serializer.data["phone"] # Disregarding value of 'otp', if no OTP was provided, # send text message to phone number with OTP if "otp" not in request.data: if interface.send_text(for_enrollment=True, request=request._request): return Response(status=status.HTTP_204_NO_CONTENT) else: # Error sending text message return Response( SEND_SMS_ERR, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # Attempt to validate OTP if "otp" in request.data and not interface.validate_otp( serializer.data["otp"]): return Response(INVALID_OTP_ERR, status=status.HTTP_400_BAD_REQUEST) # Try u2f enrollment if interface_id == "u2f": # What happens when this fails? interface.try_enroll( serializer.data["challenge"], serializer.data["response"], serializer.data["deviceName"], ) context.update({"device_name": serializer.data["deviceName"]}) try: interface.enroll(request.user) except Authenticator.AlreadyEnrolled: return Response(ALREADY_ENROLLED_ERR, status=status.HTTP_400_BAD_REQUEST) else: context.update({"authenticator": interface.authenticator}) capture_security_activity( account=request.user, type="mfa-added", actor=request.user, ip_address=request.META["REMOTE_ADDR"], context=context, send_email=True, ) request.user.clear_lost_passwords() request.user.refresh_session_nonce(self.request) request.user.save() Authenticator.objects.auto_add_recovery_codes(request.user) # Try to accept an org invite pending 2FA enrollment member_id = serializer.data.get("memberId") token = serializer.data.get("token") if member_id and token: try: helper = ApiInviteHelper( instance=self, request=request, member_id=member_id, token=token, logger=logger, ) except OrganizationMember.DoesNotExist: logger.error("Failed to accept pending org invite", exc_info=True) else: if helper.valid_request: helper.accept_invite() response = Response(status=status.HTTP_204_NO_CONTENT) helper.remove_invite_cookie(response) return response return Response(status=status.HTTP_204_NO_CONTENT)