def for_organization_member_invite(organization, email, user=None, auth=None): """ Rate limit logic for triggering a user invite email, which should also be applied for generating a brand new member invite when possible. """ if not features.has("organizations:invite-members-rate-limits", organization, actor=user): return False limits = ( ratelimiter.is_limited( u"members:invite-by-user:{}".format( md5_text(user.id if user and user.is_authenticated() else six.text_type(auth)) ), # 100 invites from a user per day limit=100, window=3600 * 24, ) if (user or auth) else None, ratelimiter.is_limited( u"members:invite-by-org:{}".format(md5_text(organization.id)), # 100 invites from an org per day limit=100, window=3600 * 24, ), ratelimiter.is_limited( u"members:org-invite-to-email:{}-{}".format( organization.id, md5_text(email.lower()).hexdigest() ), # 10 invites per email per org per day limit=10, window=3600 * 24, ), ) return any(limits)
def for_organization_member_invite(organization, email, user=None, auth=None, config=None): """ Rate limit logic for triggering a user invite email, which should also be applied for generating a brand new member invite when possible. """ if config is None: config = DEFAULT_CONFIG if not features.has("organizations:invite-members-rate-limits", organization, actor=user): return False limits = ( ratelimiter.is_limited( u"members:invite-by-user:{}".format( md5_text(user.id if user and user.is_authenticated() else six. text_type(auth)).hexdigest()), ** config["members:invite-by-user"]) if (user or auth) else None, ratelimiter.is_limited( u"members:invite-by-org:{}".format( md5_text(organization.id).hexdigest()), **config["members:invite-by-org"]), ratelimiter.is_limited( u"members:org-invite-to-email:{}-{}".format( organization.id, md5_text(email.lower()).hexdigest()), **config["members:org-invite-to-email"]), ) return any(limits)
def start_confirm_email(request): from sentry.app import ratelimiter if ratelimiter.is_limited( 'auth:confirm-email:{}'.format(request.user.id), limit=10, window=60, # 10 per minute should be enough for anyone ): return HttpResponse( 'You have made too many email confirmation requests. Please try again later.', content_type='text/plain', status=429, ) has_unverified_emails = request.user.has_unverified_emails() if has_unverified_emails: request.user.send_confirm_emails() unverified_emails = [ e.email for e in request.user.get_unverified_emails() ] msg = _('A verification email has been sent to %s.') % ( ', ').join(unverified_emails) else: msg = _( 'Your email (%s) has already been verified.') % request.user.email messages.add_message(request, messages.SUCCESS, msg) return HttpResponseRedirect(reverse('sentry-account-settings-emails'))
def post(self, request, organization): assignment = experiments.get(org=organization, experiment_name=JOIN_REQUEST_EXPERIMENT) if assignment != 1: return Response(status=403) if organization.get_option("sentry:join_requests") is False: return Response( {"detail": "Your organization does not allow join requests."}, status=403) # users can already join organizations with SSO enabled without an invite # so no need to allow requests to join as well if AuthProvider.objects.filter(organization=organization).exists(): return Response(status=403) ip_address = request.META["REMOTE_ADDR"] if ratelimiter.is_limited( u"org-join-request:ip:{}".format(ip_address), limit=5, window=60 # 5 per minute ): return Response({"detail": "Rate limit exceeded."}, status=429) serializer = JoinRequestSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data email = result["email"] create_organization_join_request(organization, email, ip_address) return Response(status=204)
def recover(request): from sentry.app import ratelimiter if request.method == 'POST' and ratelimiter.is_limited( 'accounts:recover:{}'.format(request.META['REMOTE_ADDR']), limit=5, window=60, # 5 per minute should be enough for anyone ): return HttpResponse( 'You have made too many password recovery attempts. Please try again later.', content_type='text/plain', status=429, ) form = RecoverPasswordForm(request.POST or None) if form.is_valid(): password_hash = send_password_recovery_mail(request, form.cleaned_data['user']) return render_to_response('sentry/account/recover/sent.html', { 'email': password_hash.user.email, }, request) context = { 'form': form, } return render_to_response('sentry/account/recover/index.html', context, request)
def post_process(self, event, **kwargs): # TODO(dcramer): we currently only support authenticated events, as the # value of anonymous errors/crashes/etc is much less meaningful in the # context of Segment # we avoid instantiating interfaces here as they're only going to be # used if there's a User present user_interface = event.data.get('sentry.interfaces.User') if not user_interface: return # we currently only support errors if event.get_event_type() != 'error': return user_id = user_interface.get('id') if not user_id: return write_key = self.get_option('write_key', event.project) if not write_key: return rl_key = 'segment:{}'.format(md5_text(write_key).hexdigest()) # limit segment to 50 requests/second if ratelimiter.is_limited(rl_key, limit=50, window=1): return payload = self.get_event_payload(event) session = http.build_session() session.post(self.endpoint, json=payload, auth=(write_key, ''))
def post(self, request, organization): ip_address = request.META["REMOTE_ADDR"] if ratelimiter.is_limited( u"org-join-request:ip:{}".format(ip_address), limit=5, window=60 # 5 per minute ): return Response({"detail": "Rate limit exceeded."}, status=429) serializer = JoinRequestSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data email = result["email"] assignment = experiments.get(org=organization, experiment_name=JOIN_REQUEST_EXPERIMENT) if assignment != 1: return Response(status=403) create_organization_join_request(organization, email, ip_address) return Response(status=204)
def _is_ip_rate_limited(self): limit = options.get("auth.ip-rate-limit") if not limit: return False ip_address = self.request.META["REMOTE_ADDR"] return ratelimiter.is_limited("auth:ip:{}".format(ip_address), limit)
def post(self, request, organization=None, *args, **kwargs): """ Process a login request via username/password. SSO login is handled elsewhere. """ login_form = AuthenticationForm(request, request.data) # Rate limit logins is_limited = ratelimiter.is_limited( "auth:login:username:{}".format( md5_text( login_form.clean_username( request.data.get("username"))).hexdigest()), limit=10, window=60, # 10 per minute should be enough for anyone ) if is_limited: errors = {"__all__": [login_form.error_messages["rate_limited"]]} metrics.incr("login.attempt", instance="rate_limited", skip_internal=True, sample_rate=1.0) return self.respond_with_error(errors) if not login_form.is_valid(): metrics.incr("login.attempt", instance="failure", skip_internal=True, sample_rate=1.0) return self.respond_with_error(login_form.errors) 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 Response({ "nextUri": "/auth/reactivate/", "user": serialize(user, user, DetailedUserSerializer()), }) active_org = self.get_active_organization(request) redirect_url = auth.get_org_redirect_url(request, active_org) return Response({ "nextUri": auth.get_login_redirect(request, redirect_url), "user": serialize(user, user, DetailedUserSerializer()), })
def start_confirm_email(request): from sentry.app import ratelimiter if ratelimiter.is_limited( 'auth:confirm-email:{}'.format(request.user.id), limit=10, window=60, # 10 per minute should be enough for anyone ): return HttpResponse( 'You have made too many email confirmation requests. Please try again later.', content_type='text/plain', status=429, ) if 'primary-email' in request.POST: email = request.POST.get('email') try: email_to_send = UserEmail.objects.get(user=request.user, email=email) except UserEmail.DoesNotExist: msg = _('There was an error confirming your email.') level = messages.ERROR else: request.user.send_confirm_email_singular(email_to_send) msg = _('A verification email has been sent to %s.') % (email) level = messages.SUCCESS messages.add_message(request, level, msg) return HttpResponseRedirect(reverse('sentry-account-settings')) elif request.user.has_unverified_emails(): request.user.send_confirm_emails() unverified_emails = [e.email for e in request.user.get_unverified_emails()] msg = _('A verification email has been sent to %s.') % (', ').join(unverified_emails) else: msg = _('Your email (%s) has already been verified.') % request.user.email messages.add_message(request, messages.SUCCESS, msg) return HttpResponseRedirect(reverse('sentry-account-settings-emails'))
def post(self, request, organization=None, *args, **kwargs): """ Process a login request via username/password. SSO login is handled elsewhere. """ login_form = AuthenticationForm(request, request.DATA) # Rate limit logins is_limited = ratelimiter.is_limited( u'auth:login:username:{}'.format( md5_text(request.DATA.get('username').lower()).hexdigest()), limit=10, window=60, # 10 per minute should be enough for anyone ) if is_limited: errors = {'__all__': [login_form.error_messages['rate_limited']]} metrics.incr('login.attempt', instance='rate_limited', skip_internal=True, sample_rate=1.0) return self.respond_with_error(errors) if not login_form.is_valid(): metrics.incr('login.attempt', instance='failure', skip_internal=True, sample_rate=1.0) return self.respond_with_error(login_form.errors) 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 Response({ 'nextUri': '/auth/reactivate/', 'user': serialize(user, user, DetailedUserSerializer()), }) active_org = self.get_active_organization(request) redirect_url = auth.get_org_redirect_url(request, active_org) return Response({ 'nextUri': auth.get_login_redirect(request, redirect_url), 'user': serialize(user, user, DetailedUserSerializer()), })
def get(self, request, organization, event_id): """ Resolve a Event ID `````````````````` This resolves a event ID to the project slug and internal issue ID and internal event ID. :pparam string organization_slug: the slug of the organization the event ID should be looked up in. :param string event_id: the event ID to look up. :auth: required """ # Largely copied from ProjectGroupIndexEndpoint if len(event_id) != 32: return Response({"detail": "Event ID must be 32 characters."}, status=400) # Limit to 100req/s if ratelimiter.is_limited( u"api:event-id-lookup:{}".format( md5_text(request.user.id if request.user and request.user. is_authenticated() else "").hexdigest()), limit=100, window=1, ): return Response( { "detail": "You are attempting to use this endpoint too quickly. Limit is 100 requests/second." }, status=429, ) project_slugs_by_id = dict( Project.objects.filter(organization=organization).values_list( "id", "slug")) try: snuba_filter = eventstore.Filter( conditions=[["event.type", "!=", "transaction"]], project_ids=project_slugs_by_id.keys(), event_ids=[event_id], ) event = eventstore.get_events(filter=snuba_filter, limit=1)[0] except IndexError: raise ResourceDoesNotExist() else: return Response({ "organizationSlug": organization.slug, "projectSlug": project_slugs_by_id[event.project_id], "groupId": six.text_type(event.group_id), "eventId": six.text_type(event.event_id), "event": serialize(event, request.user), })
def _is_ip_rate_limited(self): limit = options.get('auth.ip-rate-limit') if not limit: return False ip_address = self.request.META['REMOTE_ADDR'] return ratelimiter.is_limited( 'auth:ip:{}'.format(ip_address), limit, )
def _is_ip_rate_limited(self): limit = options.get('auth.ip-rate-limit') if not limit: return False ip_address = self.request.META['REMOTE_ADDR'] return ratelimiter.is_limited( u'auth:ip:{}'.format(ip_address), limit, )
def _is_user_rate_limited(self): limit = options.get("auth.user-rate-limit") if not limit: return False username = self.cleaned_data.get("username") if not username: return False return ratelimiter.is_limited(f"auth:username:{username}", limit)
def should_notify(self, group, event): if group.is_muted(): return False project = group.project rate_limited = ratelimiter.is_limited(project=project, key=self.get_conf_key(), limit=15) if rate_limited: self.logger.info("Notification for project %s dropped due to rate limiting", project.id) return not rate_limited
def post(self, request): """ Create a New Organization ````````````````````````` Create a new organization owned by the request's user. To create an organization only the name is required. :param string name: the human readable name for the new organization. :param string slug: the unique URL slug for this organization. If this is not provided a slug is automatically generated based on the name. :auth: required, user-context-needed """ if not request.user.is_authenticated(): return Response({"detail": "This endpoint requires user info"}, status=401) if not features.has("organizations:create", actor=request.user): return Response({"detail": "Organizations are not allowed to be created by this user."}, status=401) limit = options.get("api.rate-limit.org-create") if limit and ratelimiter.is_limited(u"org-create:{}".format(request.user.id), limit=5, window=3600): return Response({"detail": "You are attempting to create too many organizations too quickly."}, status=429) serializer = OrganizationSerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object try: with transaction.atomic(): org = Organization.objects.create(name=result["name"], slug=result.get("slug")) except IntegrityError: return Response({"detail": "An organization with this slug already exists."}, status=409) om = OrganizationMember.objects.create(organization=org, user=request.user, role=roles.get_top_dog().id) if result.get("defaultTeam"): team = org.team_set.create(name=org.name) OrganizationMemberTeam.objects.create(team=team, organizationmember=om, is_active=True) self.create_audit_entry( request=request, organization=org, target_object=org.id, event=AuditLogEntryEvent.ORG_ADD, data=org.get_audit_log_data(), ) return Response(serialize(org, request.user), status=201) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def _is_user_rate_limited(self): limit = options.get('auth.user-rate-limit') if not limit: return False username = self.cleaned_data.get('username') if not username: return False return ratelimiter.is_limited( u'auth:username:{}'.format(username), limit, )
def post(self, request, user): """ Sends a confirmation email to user `````````````````````````````````` :auth required: """ from sentry.app import ratelimiter if ratelimiter.is_limited( "auth:confirm-email:{}".format(user.id), limit=10, window=60, # 10 per minute should be enough for anyone ): return self.respond( { "detail": "You have made too many email confirmation requests. Please try again later." }, status=status.HTTP_429_TOO_MANY_REQUESTS, ) serializer = EmailSerializer(data=request.data) if not serializer.is_valid(): return InvalidEmailResponse() # If email is specified then try to only send one confirmation email try: email_to_send = UserEmail.objects.get( user=user, email=serializer.validated_data["email"].lower().strip()) except UserEmail.DoesNotExist: return InvalidEmailResponse() else: if email_to_send.is_verified: return self.respond({"detail": "Email is already verified"}, status=status.HTTP_400_BAD_REQUEST) user.send_confirm_email_singular(email_to_send) logger.info( "user.email.start_confirm", extra={ "user_id": user.id, "ip_address": request.META["REMOTE_ADDR"], "email": email_to_send, }, ) return self.respond(status=status.HTTP_204_NO_CONTENT)
def recover(request): from sentry.app import ratelimiter extra = { 'ip_address': request.META['REMOTE_ADDR'], 'user_agent': request.META.get('HTTP_USER_AGENT'), } if request.method == 'POST' and ratelimiter.is_limited( u'accounts:recover:{}'.format(extra['ip_address']), limit=5, window=60, # 5 per minute should be enough for anyone ): logger.warning('recover.rate-limited', extra=extra) return HttpResponse( 'You have made too many password recovery attempts. Please try again later.', content_type='text/plain', status=429, ) prefill = {'user': request.GET.get('email')} form = RecoverPasswordForm(request.POST or None, initial=prefill) extra['user_recovered'] = form.data.get('user') if form.is_valid(): email = form.cleaned_data['user'] if email: password_hash = LostPasswordHash.for_user(email) password_hash.send_email(request) extra['passwordhash_id'] = password_hash.id extra['user_id'] = password_hash.user_id logger.info('recover.sent', extra=extra) tpl = 'sentry/account/recover/sent.html' context = {'email': email} return render_to_response(tpl, context, request) if form._errors: logger.warning('recover.error', extra=extra) tpl = 'sentry/account/recover/index.html' context = {'form': form} return render_to_response(tpl, context, request)
def recover(request): from sentry.app import ratelimiter extra = { "ip_address": request.META["REMOTE_ADDR"], "user_agent": request.META.get("HTTP_USER_AGENT"), } if request.method == "POST" and ratelimiter.is_limited( "accounts:recover:{}".format(extra["ip_address"]), limit=5, window=60, # 5 per minute should be enough for anyone ): logger.warning("recover.rate-limited", extra=extra) return HttpResponse( "You have made too many password recovery attempts. Please try again later.", content_type="text/plain", status=429, ) prefill = {"user": request.GET.get("email")} form = RecoverPasswordForm(request.POST or None, initial=prefill) extra["user_recovered"] = form.data.get("user") if form.is_valid(): email = form.cleaned_data["user"] if email: password_hash = LostPasswordHash.for_user(email) password_hash.send_email(request) extra["passwordhash_id"] = password_hash.id extra["user_id"] = password_hash.user_id logger.info("recover.sent", extra=extra) context = {"email": email} return render_to_response(get_template("recover", "sent"), context, request) if form._errors: logger.warning("recover.error", extra=extra) context = {"form": form} return render_to_response(get_template("recover", "index"), context, request)
def start_confirm_email(request): from sentry.app import ratelimiter if ratelimiter.is_limited( f"auth:confirm-email:{request.user.id}", limit=10, window=60, # 10 per minute should be enough for anyone ): return HttpResponse( "You have made too many email confirmation requests. Please try again later.", content_type="text/plain", status=429, ) if "primary-email" in request.POST: email = request.POST.get("email") try: email_to_send = UserEmail.objects.get(user=request.user, email=email) except UserEmail.DoesNotExist: msg = _("There was an error confirming your email.") level = messages.ERROR else: request.user.send_confirm_email_singular(email_to_send) msg = _("A verification email has been sent to %s.") % (email) level = messages.SUCCESS messages.add_message(request, level, msg) return HttpResponseRedirect(reverse("sentry-account-settings")) elif request.user.has_unverified_emails(): request.user.send_confirm_emails() unverified_emails = [ e.email for e in request.user.get_unverified_emails() ] msg = _("A verification email has been sent to %s.") % ( ", ").join(unverified_emails) for email in unverified_emails: logger.info( "user.email.start_confirm", extra={ "user_id": request.user.id, "ip_address": request.META["REMOTE_ADDR"], "email": email, }, ) else: msg = _( "Your email (%s) has already been verified.") % request.user.email messages.add_message(request, messages.SUCCESS, msg) return HttpResponseRedirect(reverse("sentry-account-settings-emails"))
def wrapper(*args, **kwargs): try: if ratelimiter.is_limited( u"rate_limit_endpoint:{}".format(md5_text(function).hexdigest()), limit=limit, window=window, ): return Response( {"detail": "You are attempting to use this endpoint too quickly."}, status=429, ) else: return function(*args, **kwargs) except Exception: raise
def wrapper(self, request, *args, **kwargs): ip = request.META["REMOTE_ADDR"] if ratelimiter.is_limited( "rate_limit_endpoint:{}:{}".format(md5_text(function).hexdigest(), ip), limit=limit, window=window, ): return Response( { "detail": f"You are attempting to use this endpoint too quickly. Limit is {limit}/{window}s" }, status=429, ) else: return function(self, request, *args, **kwargs)
def should_notify(self, group, event): if group.is_muted(): return False project = group.project rate_limited = ratelimiter.is_limited( project=project, key=self.get_conf_key(), limit=10, ) if rate_limited: self.logger.info('notification.rate_limited', extra={'project_id': project.id}) return not rate_limited
def wrapper(self: Any, request: Request, *args: Any, **kwargs: Any) -> Response: if ratelimiter.is_limited( build_rate_limit_key(function, request), limit=limit, window=window, ): return Response( { "detail": f"You are attempting to use this endpoint too quickly. Limit is {limit}/{window}s" }, status=429, ) else: return function(self, request, *args, **kwargs)
def start_confirm_email(request): from sentry.app import ratelimiter if ratelimiter.is_limited( u'auth:confirm-email:{}'.format(request.user.id), limit=10, window=60, # 10 per minute should be enough for anyone ): return HttpResponse( 'You have made too many email confirmation requests. Please try again later.', content_type='text/plain', status=429, ) if 'primary-email' in request.POST: email = request.POST.get('email') try: email_to_send = UserEmail.objects.get(user=request.user, email=email) except UserEmail.DoesNotExist: msg = _('There was an error confirming your email.') level = messages.ERROR else: request.user.send_confirm_email_singular(email_to_send) msg = _('A verification email has been sent to %s.') % (email) level = messages.SUCCESS messages.add_message(request, level, msg) return HttpResponseRedirect(reverse('sentry-account-settings')) elif request.user.has_unverified_emails(): request.user.send_confirm_emails() unverified_emails = [ e.email for e in request.user.get_unverified_emails() ] msg = _('A verification email has been sent to %s.') % ( ', ').join(unverified_emails) for email in unverified_emails: logger.info('user.email.start_confirm', extra={ 'user_id': request.user.id, 'ip_address': request.META['REMOTE_ADDR'], 'email': email, }) else: msg = _( 'Your email (%s) has already been verified.') % request.user.email messages.add_message(request, messages.SUCCESS, msg) return HttpResponseRedirect(reverse('sentry-account-settings-emails'))
def should_notify(self, group, event): if group.is_muted(): return False project = group.project rate_limited = ratelimiter.is_limited( project=project, key=self.get_conf_key(), limit=10, ) if rate_limited: logger = logging.getLogger('sentry.plugins.{0}'.format(self.get_conf_key())) logger.info('Notification for project %s dropped due to rate limiting', project.id) return not rate_limited
def should_notify(self, group, event): if group.is_muted(): return False project = group.project rate_limited = ratelimiter.is_limited( project=project, key=self.get_conf_key(), limit=15, ) if rate_limited: logger = logging.getLogger('sentry.plugins.{0}'.format(self.get_conf_key())) logger.info('Notification for project %s dropped due to rate limiting', project.id) return not rate_limited
def should_notify(self, group, event): if group.is_ignored(): return False project = group.project rate_limited = ratelimiter.is_limited( project=project, key=self.get_conf_key(), limit=10, ) if rate_limited: self.logger.info('notification.rate_limited', extra={'project_id': project.id}) return not rate_limited
def post(self, request, user): """ Sends a confirmation email to user `````````````````````````````````` :auth required: """ from sentry.app import ratelimiter if ratelimiter.is_limited( 'auth:confirm-email:{}'.format(user.id), limit=10, window=60, # 10 per minute should be enough for anyone ): return self.respond({'detail': 'You have made too many email confirmation requests. Please try again later.', }, status=status.HTTP_429_TOO_MANY_REQUESTS) serializer = EmailSerializer(data=request.DATA) if not serializer.is_valid(): return InvalidEmailResponse() # If email is specified then try to only send one confirmation email try: email_to_send = UserEmail.objects.get( user=user, email=serializer.object['email'].lower().strip()) except UserEmail.DoesNotExist: return InvalidEmailResponse() else: if email_to_send.is_verified: return self.respond({'detail': 'Email is already verified'}, status=status.HTTP_400_BAD_REQUEST) user.send_confirm_email_singular(email_to_send) logger.info( 'user.email.start_confirm', extra={ 'user_id': user.id, 'ip_address': request.META['REMOTE_ADDR'], 'email': email_to_send, } ) return self.respond(status=status.HTTP_204_NO_CONTENT)
def should_notify(self, group, event): if group.is_muted(): return False project = group.project rate_limited = ratelimiter.is_limited( project=project, key=self.get_conf_key(), limit=10, ) if rate_limited: self.logger.info( 'Notification for project %s dropped due to rate limiting', project.id) return not rate_limited
def should_notify(self, group, event): if group.is_muted(): return False project = group.project send_to = self.get_sendable_users(project) if not send_to: return False rate_limited = ratelimiter.is_limited( project=project, key=self.get_conf_key(), limit=15, ) if rate_limited: logger = logging.getLogger('sentry.plugins.{0}'.format(self.get_conf_key())) logger.info('Notification dropped due to rate limiting') return not rate_limited
def post(self, request, organization): variant = experiments.get(org=organization, experiment_name="ImprovedInvitesExperiment") if variant not in ("all", "join_request"): return Response(status=403) if organization.get_option("sentry:join_requests") is False: return Response( {"detail": "Your organization does not allow join requests."}, status=403) # users can already join organizations with SSO enabled without an invite # so they should join that way and not through a request to the admins if AuthProvider.objects.filter(organization=organization).exists(): return Response(status=403) ip_address = request.META["REMOTE_ADDR"] if ratelimiter.is_limited( u"org-join-request:ip:{}".format(ip_address), limit=5, window=86400, # 5 per day, 60 x 60 x 24 ): return Response({"detail": "Rate limit exceeded."}, status=429) serializer = JoinRequestSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data email = result["email"] member = create_organization_join_request(organization, email, ip_address) if member: send_invite_request_notification_email.delay(member.id) join_request_created.send_robust(sender=self, member=member) return Response(status=204)
def post(self, request: Request, organization) -> Response: if organization.get_option("sentry:join_requests") is False: return Response( {"detail": "Your organization does not allow join requests."}, status=403) # users can already join organizations with SSO enabled without an invite # so they should join that way and not through a request to the admins if AuthProvider.objects.filter(organization=organization).exists(): return Response(status=403) ip_address = request.META["REMOTE_ADDR"] if ratelimiter.is_limited( f"org-join-request:ip:{ip_address}", limit=5, window=86400, # 5 per day, 60 x 60 x 24 ): return Response({"detail": "Rate limit exceeded."}, status=429) serializer = JoinRequestSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data email = result["email"] member = create_organization_join_request(organization, email, ip_address) if member: async_send_notification(JoinRequestNotification, member, request.user) # legacy analytics join_request_created.send_robust(sender=self, member=member) return Response(status=204)
def post_process(self, event, **kwargs): token = self.get_option('token', event.project) index = self.get_option('index', event.project) instance = self.get_option('instance', event.project) if not (token and index and instance): return if not instance.endswith('/services/collector'): instance = instance.rstrip('/') + '/services/collector' source = self.get_option('source', event.project) or 'sentry' rl_key = 'splunk:{}'.format(md5_text(token).hexdigest()) # limit splunk to 50 requests/second if ratelimiter.is_limited(rl_key, limit=50, window=1): return payload = { 'time': int(event.datetime.strftime('%s')), 'source': source, 'index': index, 'event': self.get_event_payload(event), } host = self.get_host_for_splunk(event) if host: payload['host'] = host session = http.build_session() session.post( instance, json=payload, # Splunk cloud instances certifcates dont play nicely verify=False, headers={ 'Authorization': 'Splunk {}'.format(token) }, ).raise_for_status()
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 handle(self, request: Request) -> Response: user = auth.get_pending_2fa_user(request) if user is None: return HttpResponseRedirect(auth.get_login_url()) interfaces = Authenticator.objects.all_interfaces_for_user(user) # If for whatever reason we ended up here but the user has no 2FA # enabled, we just continue successfully. if not interfaces: return self.perform_signin(request, user) challenge = activation = None interface = self.negotiate_interface(request, interfaces) if request.method == "POST" and ratelimiter.is_limited( f"auth-2fa:user:{user.id}", limit=5, window=60): # TODO: Maybe email the account owner or do something to notify someone # This would probably be good for them to know. return HttpResponse( "You have made too many 2FA attempts. Please try again later.", content_type="text/plain", status=429, ) # check if webauthn-login feature flag is enabled for frontend webauthn_signin_ff = self._check_can_webauthn_signin( user, request.user) if request.method == "GET": if interface.type == U2fInterface.type: activation = interface.activate(request, webauthn_signin_ff) else: activation = interface.activate(request) if activation is not None and activation.type == "challenge": challenge = activation.challenge if webauthn_signin_ff and interface.type == U2fInterface.type: activation.challenge = {} activation.challenge[ "webAuthnAuthenticationData"] = b64encode(challenge) elif "challenge" in request.POST: challenge = json.loads(request.POST["challenge"]) form = TwoFactorForm() # If an OTP response was supplied, we try to make it pass. otp = request.POST.get("otp") if otp: used_interface = self.validate_otp(otp, interface, interfaces) if used_interface is not None: return self.perform_signin(request, user, used_interface) self.fail_signin(request, user, form) # If a challenge and response exists, validate if challenge: response = request.POST.get("response") if response: response = json.loads(response) if interface.validate_response(request, challenge, response, webauthn_signin_ff): return self.perform_signin(request, user, interface) self.fail_signin(request, user, form) return render_to_response( [ "sentry/twofactor_%s.html" % interface.interface_id, "sentry/twofactor.html" ], { "form": form, "interface": interface, "other_interfaces": self.get_other_interfaces(interface, interfaces), "activation": activation, "isWebauthnSigninFFEnabled": webauthn_signin_ff, }, request, status=200, )
def post_process(self, event, **kwargs): token = self.get_option('token', event.project) index = self.get_option('index', event.project) instance = self.get_option('instance', event.project) if not (token and index and instance): metrics.incr('integrations.splunk.forward-event.unconfigured', tags={ 'project_id': event.project_id, 'organization_id': event.project.organization_id, 'event_type': event.get_event_type(), }) return if not instance.endswith('/services/collector'): instance = instance.rstrip('/') + '/services/collector' source = self.get_option('source', event.project) or 'sentry' rl_key = 'splunk:{}'.format(md5_text(token).hexdigest()) # limit splunk to 50 requests/second if ratelimiter.is_limited(rl_key, limit=1000, window=1): metrics.incr('integrations.splunk.forward-event.rate-limited', tags={ 'project_id': event.project_id, 'organization_id': event.project.organization_id, 'event_type': event.get_event_type(), }) return payload = { 'time': int(event.datetime.strftime('%s')), 'source': source, 'index': index, 'event': self.get_event_payload(event), } host = self.get_host_for_splunk(event) if host: payload['host'] = host session = http.build_session() try: # https://docs.splunk.com/Documentation/Splunk/7.2.3/Data/TroubleshootHTTPEventCollector resp = session.post( instance, json=payload, # Splunk cloud instances certifcates dont play nicely verify=False, headers={ 'Authorization': 'Splunk {}'.format(token) }, timeout=5, ) if resp.status_code != 200: raise SplunkError.from_response(resp) except Exception as exc: metrics.incr('integrations.splunk.forward-event.error', tags={ 'project_id': event.project_id, 'organization_id': event.project.organization_id, 'event_type': event.get_event_type(), 'error_code': getattr(exc, 'code', None), }) raise metrics.incr('integrations.splunk.forward-event.success', tags={ 'project_id': event.project_id, 'organization_id': event.project.organization_id, 'event_type': event.get_event_type(), })
def __is_rate_limited(self, group, event): return ratelimiter.is_limited( project=group.project, key=self.get_conf_key(), limit=10, )
def handle_basic_auth(self, request, organization=None, *args, **kwargs): can_register = self.can_register( request, organization=organization, *args, **kwargs) op = request.POST.get('op') # Detect that we are on the register page by url /register/ and # then activate the register tab by default. if not op and '/register' in request.path_info and can_register: op = 'register' 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) # 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) 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(request.POST['username'].lower()).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.' ] elif login_form.is_valid(): user = login_form.get_user() auth.login( request, user, organization_id=organization.id if organization else None, ) if not user.is_active: return self.redirect(reverse('sentry-reactivate-account')) return self.redirect(auth.get_login_redirect(request)) context = { 'op': op or 'login', 'server_hostname': get_server_hostname(), 'login_form': login_form, 'organization': organization, 'register_form': register_form, 'CAN_REGISTER': can_register, } return self.respond_login(request, context, organization=organization, *args, **kwargs)
def post(self, request): """ Create a New Organization ````````````````````````` Create a new organization owned by the request's user. To create an organization only the name is required. :param string name: the human readable name for the new organization. :param string slug: the unique URL slug for this organization. If this is not provided a slug is automatically generated based on the name. :param bool agreeTerms: a boolean signaling you agree to the applicable terms of service and privacy policy. :auth: required, user-context-needed """ if not request.user.is_authenticated(): return Response({'detail': 'This endpoint requires user info'}, status=401) if not features.has('organizations:create', actor=request.user): return Response( { 'detail': 'Organizations are not allowed to be created by this user.' }, status=401 ) limit = options.get('api.rate-limit.org-create') if limit and ratelimiter.is_limited( u'org-create:{}'.format(request.user.id), limit=limit, window=3600, ): return Response( { 'detail': 'You are attempting to create too many organizations too quickly.' }, status=429 ) serializer = OrganizationSerializer(data=request.DATA) if serializer.is_valid(): result = serializer.object try: with transaction.atomic(): org = Organization.objects.create( name=result['name'], slug=result.get('slug'), ) om = OrganizationMember.objects.create( organization=org, user=request.user, role=roles.get_top_dog().id, ) if result.get('defaultTeam'): team = org.team_set.create( name=org.name, ) OrganizationMemberTeam.objects.create( team=team, organizationmember=om, is_active=True ) self.create_audit_entry( request=request, organization=org, target_object=org.id, event=AuditLogEntryEvent.ORG_ADD, data=org.get_audit_log_data(), ) analytics.record( 'organization.created', org, actor_id=request.user.id if request.user.is_authenticated() else None ) except IntegrityError: return Response( { 'detail': 'An organization with this slug already exists.' }, status=409, ) # failure on sending this signal is acceptable if result.get('agreeTerms'): terms_accepted.send_robust( user=request.user, organization=org, ip_address=request.META['REMOTE_ADDR'], sender=type(self), ) return Response(serialize(org, request.user), status=201) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)