def run_test( self, activity, expected_username, expected_action, expected_comment, expected_recipient, ): incident = activity.incident context = build_activity_context(activity, expected_recipient) assert context['user_name'] == expected_username assert context['action'] == '%s on incident %s (#%s)' % ( expected_action, activity.incident.title, activity.incident.identifier, ) assert context['link'] == absolute_uri( reverse( 'sentry-incident', kwargs={ 'organization_slug': incident.organization.slug, 'incident_id': incident.identifier, }, )) + '?referrer=incident_activity_email' assert context['comment'] == expected_comment assert context['unsubscribe_link'] == generate_signed_link( expected_recipient, 'sentry-account-email-unsubscribe-incident', kwargs={'incident_id': incident.id}, )
def build_activity_context(activity, user): if activity.type == IncidentActivityType.COMMENT.value: action = "left a comment" else: action = "changed status from %s to %s" % ( IncidentStatus(int(activity.previous_value)).name.lower(), IncidentStatus(int(activity.value)).name.lower(), ) incident = activity.incident action = "%s on incident %s (#%s)" % (action, incident.title, incident.identifier) return { "user_name": activity.user.name if activity.user else "Sentry", "action": action, "link": absolute_uri( reverse( "sentry-incident", kwargs={ "organization_slug": incident.organization.slug, "incident_id": incident.identifier, }, )) + "?" + urlencode({"referrer": "incident_activity_email"}), "comment": activity.comment, "unsubscribe_link": generate_signed_link(user, "sentry-account-email-unsubscribe-incident", kwargs={"incident_id": incident.id}), }
def run_test( self, activity, expected_username, expected_action, expected_comment, expected_recipient ): incident = activity.incident context = build_activity_context(activity, expected_recipient) assert context["user_name"] == expected_username assert context["action"] == "%s on incident %s (#%s)" % ( expected_action, activity.incident.title, activity.incident.identifier, ) assert ( context["link"] == absolute_uri( reverse( "sentry-incident", kwargs={ "organization_slug": incident.organization.slug, "incident_id": incident.identifier, }, ) ) + "?referrer=incident_activity_email" ) assert context["comment"] == expected_comment assert context["unsubscribe_link"] == generate_signed_link( expected_recipient, "sentry-account-email-unsubscribe-incident", kwargs={"incident_id": incident.id}, )
def test_process(self): instance = self.create_instance() path = generate_signed_link(user=self.user, viewname=self.view_name, args=[instance.id]) resp = self.client.post(path, data={"op": "unsubscribe"}) assert resp.status_code == 302 self.assert_unsubscribed(instance, self.user)
def test_no_access(self): user = self.create_user("*****@*****.**") instance = self.create_instance() path = generate_signed_link(user=user, viewname=self.view_name, args=[instance.id]) resp = self.client.get(path) assert resp.status_code == 404
def build_activity_context(activity, user): if activity.type == IncidentActivityType.COMMENT.value: action = 'left a comment' else: action = 'changed status from %s to %s' % ( IncidentStatus(int(activity.previous_value)).name.lower(), IncidentStatus(int(activity.value)).name.lower(), ) incident = activity.incident action = '%s on incident %s (#%s)' % (action, incident.title, incident.identifier) return { 'user_name': activity.user.name if activity.user else 'Sentry', 'action': action, 'link': absolute_uri(reverse( 'sentry-incident', kwargs={ 'organization_slug': incident.organization.slug, 'incident_id': incident.identifier, }, )) + '?' + urlencode({'referrer': 'incident_activity_email'}), 'comment': activity.comment, 'unsubscribe_link': generate_signed_link( user, 'sentry-account-email-unsubscribe-incident', kwargs={'incident_id': incident.id}, ), }
def test_invalid_issue(self): path = generate_signed_link(user=self.user, viewname=self.view_name, args=[13413434]) resp = self.client.get(path) assert resp.status_code == 404
def add_unsubscribe_link(self, context, user_id, project, referrer): context["unsubscribe_link"] = generate_signed_link( user_id, "sentry-account-email-unsubscribe-project", referrer, kwargs={"project_id": project.id}, )
def add_unsubscribe_link(self, context, user_id, project): context['unsubscribe_link'] = generate_signed_link( user_id, 'sentry-account-email-unsubscribe-project', kwargs={ 'project_id': project.id, })
def test_sso_auth_required_signed_link(self): user = self.create_user("*****@*****.**", is_superuser=False) organization = self.create_organization(name="foo") team = self.create_team(name="bar", organization=organization) project = self.create_project(name="baz", organization=organization, teams=[team]) member = self.create_member(user=user, organization=organization, teams=[team]) setattr(member.flags, "sso:linked", True) member.save() self.store_event(data={}, project_id=project.id) auth_provider = AuthProvider.objects.create( organization=organization, provider="dummy", flags=0 ) AuthIdentity.objects.create(auth_provider=auth_provider, user=user) self.login_as(user) unsigned_link = reverse( "sentry-api-0-project-fix-processing-issues", kwargs={"project_slug": project.slug, "organization_slug": organization.slug}, ) resp = self.client.get(unsigned_link) assert resp.status_code == 401, (resp.status_code, resp.content) signed_link = generate_signed_link( user, "sentry-api-0-project-fix-processing-issues", kwargs={"project_slug": project.slug, "organization_slug": organization.slug}, ) resp = self.client.get(signed_link) assert resp.status_code == 200
def add_unsubscribe_link(self, context, user_id, project): context['unsubscribe_link'] = generate_signed_link( user_id, 'sentry-account-email-unsubscribe-project', kwargs={ 'project_id': project.id, } )
def test_renders(self): instance = self.create_instance() path = generate_signed_link(user=self.user, viewname=self.view_name, args=[instance.id]) resp = self.client.get(path) assert resp.status_code == 200
def get_unsubscribe_link(user_id: int, resource_id: int, key: str = "issue", referrer: Optional[str] = None) -> str: return generate_signed_link( user_id, f"sentry-account-email-unsubscribe-{key}", referrer, kwargs={f"{key}_id": resource_id}, )
def send(self): if not self.should_email(): return participants = self.get_participants() if not participants: return activity = self.activity project = self.project group = self.group context = self.get_base_context() context.update(self.get_context()) template = self.get_template() html_template = self.get_html_template() email_type = self.get_email_type() headers = self.get_headers() for user, reason in participants.items(): if group: context.update( { 'reason': GroupSubscriptionReason.descriptions.get( reason, "are subscribed to this issue", ), 'unsubscribe_link': generate_signed_link( user.id, 'sentry-account-email-unsubscribe-issue', kwargs={'issue_id': group.id}, ), } ) user_context = self.get_user_context(user) if user_context: user_context.update(context) else: user_context = context msg = MessageBuilder( subject=self.get_subject_with_prefix(), template=template, html_template=html_template, headers=headers, type=email_type, context=user_context, reference=activity, reply_reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def test_invalid_issue(self): path = generate_signed_link( user=self.user, viewname='sentry-account-email-unsubscribe-issue', args=[13413434], ) resp = self.client.get(path) assert resp.status_code == 404
def test_renders(self): group = self.create_group() path = generate_signed_link( user=self.user, viewname='sentry-account-email-unsubscribe-issue', args=[group.id], ) resp = self.client.get(path) assert resp.status_code == 200
def test_no_access(self): user = self.create_user('*****@*****.**') group = self.create_group() path = generate_signed_link( user=user, viewname='sentry-account-email-unsubscribe-issue', args=[group.id], ) resp = self.client.get(path) assert resp.status_code == 404
def test_sso_auth_required_signed_link(self): user = self.create_user('*****@*****.**', is_superuser=False) organization = self.create_organization(name='foo') team = self.create_team(name='bar', organization=organization) project = self.create_project( name='baz', organization=organization, teams=[team]) member = self.create_member( user=user, organization=organization, teams=[team]) setattr(member.flags, 'sso:linked', True) member.save() self.store_event( data={}, project_id=project.id, ) auth_provider = AuthProvider.objects.create( organization=organization, provider='dummy', flags=0, ) AuthIdentity.objects.create( auth_provider=auth_provider, user=user, ) self.login_as(user) unsigned_link = reverse( 'sentry-api-0-project-fix-processing-issues', kwargs={ 'project_slug': project.slug, 'organization_slug': organization.slug, } ) resp = self.client.get(unsigned_link) assert resp.status_code == 401, (resp.status_code, resp.content) signed_link = generate_signed_link( user, 'sentry-api-0-project-fix-processing-issues', kwargs={ 'project_slug': project.slug, 'organization_slug': organization.slug, } ) resp = self.client.get(signed_link) assert resp.status_code == 200
def test_sso_auth_required_signed_link(self): user = self.create_user('*****@*****.**', is_superuser=False) organization = self.create_organization(name='foo') team = self.create_team(name='bar', organization=organization) project = self.create_project( name='baz', organization=organization, team=team) member = self.create_member( user=user, organization=organization, teams=[team]) setattr(member.flags, 'sso:linked', True) member.save() group = self.create_group(project=project) self.create_event(group=group) auth_provider = AuthProvider.objects.create( organization=organization, provider='dummy', flags=0, ) AuthIdentity.objects.create( auth_provider=auth_provider, user=user, ) self.login_as(user) unsigned_link = reverse( 'sentry-api-0-project-fix-processing-issues', kwargs={ 'project_slug': project.slug, 'organization_slug': organization.slug, } ) resp = self.client.get(unsigned_link) assert resp.status_code == 401, (resp.status_code, resp.content) signed_link = generate_signed_link( user, 'sentry-api-0-project-fix-processing-issues', kwargs={ 'project_slug': project.slug, 'organization_slug': organization.slug, } ) resp = self.client.get(signed_link) assert resp.status_code == 200
def get(self, request, project): """ List a project's processing issues. """ num_issues = ProcessingIssue.objects.filter( project=project ).count() last_seen = ProcessingIssue.objects.filter( project=project ).order_by('-datetime').first() resolveable_issues, has_more = ProcessingIssue.objects \ .find_resolved(project_id=project.id) reprocessing_issues = ReprocessingReport.objects \ .filter(project_id=project.id).count() signed_link = None if num_issues > 0: signed_link = generate_signed_link( request.user, 'sentry-api-0-project-fix-processing-issues', kwargs={ 'project_slug': project.slug, 'organization_slug': project.organization.slug, } ) data = { 'hasIssues': num_issues > 0, 'numIssues': num_issues, 'lastSeen': last_seen and serialize(last_seen.datetime) or None, 'resolveableIssues': len(resolveable_issues), 'hasMoreResolveableIssues': has_more, 'issuesProcessing': reprocessing_issues, 'signedLink': signed_link } if request.GET.get('detailed') == '1': q = ProcessingIssue.objects.with_num_events().filter( project=project ).order_by('type', 'datetime') data['issues'] = [serialize(x, request.user) for x in q] return Response(serialize(data, request.user))
def send(self): if not self.should_email(): return users = self.get_participants() if not users: return activity = self.activity project = self.project group = self.group context = self.get_base_context() context.update(self.get_context()) subject_prefix = self._get_subject_prefix() subject = (u'{}{}'.format( subject_prefix, self.get_subject(), )).encode('utf-8') template = self.get_template() html_template = self.get_html_template() email_type = self.get_email_type() headers = self.get_headers() for user in users: if group: context['unsubscribe_link'] = generate_signed_link( user.id, 'sentry-account-email-unsubscribe-issue', kwargs={'issue_id': group.id}, ) msg = MessageBuilder( subject=subject, template=template, html_template=html_template, headers=headers, type=email_type, context=context, reference=activity, reply_reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def test_sso_auth_required_signed_link(self): unsigned_link = reverse( "sentry-api-0-project-fix-processing-issues", kwargs={"project_slug": self.project.slug, "organization_slug": self.organization.slug}, ) resp = self.client.get(unsigned_link) assert resp.status_code == 401, (resp.status_code, resp.content) signed_link = generate_signed_link( self.user, "sentry-api-0-project-fix-processing-issues", kwargs={"project_slug": self.project.slug, "organization_slug": self.organization.slug}, ) resp = self.client.get(signed_link) assert resp.status_code == 200
def test_process(self): group = self.create_group() path = generate_signed_link( user=self.user, viewname='sentry-account-email-unsubscribe-issue', args=[group.id], ) resp = self.client.post(path, data={'op': 'unsubscribe'}) assert resp.status_code == 302 assert GroupSubscription.objects.filter( user=self.user, group=group, is_active=False, ).exists()
def test_link_signing(self): rf = RequestFactory() url = linksign.generate_signed_link(self.user, "sentry") assert url.startswith("http://") req = rf.get("/" + url.split("/", 3)[-1]) signed_user = linksign.process_signature(req) assert signed_user assert signed_user.id == self.user.id req = rf.get("/what" + url.split("/", 3)[-1]) signed_user = linksign.process_signature(req) assert signed_user is None req = rf.get("/" + url.split("/", 3)[-1] + "garbage") signed_user = linksign.process_signature(req) assert signed_user is None rf.defaults["SERVER_NAME"] = "something-else" req = rf.get("/" + url.split("/", 3)[-1]) signed_user = linksign.process_signature(req) assert signed_user is None
def test_link_signing(self): rf = RequestFactory() url = linksign.generate_signed_link(self.user, 'sentry') assert url.startswith('http://') req = rf.get('/' + url.split('/', 3)[-1]) signed_user = linksign.process_signature(req) assert signed_user assert signed_user.id == self.user.id req = rf.get('/what' + url.split('/', 3)[-1]) signed_user = linksign.process_signature(req) assert signed_user is None req = rf.get('/' + url.split('/', 3)[-1] + 'garbage') signed_user = linksign.process_signature(req) assert signed_user is None rf.defaults['SERVER_NAME'] = 'something-else' req = rf.get('/' + url.split('/', 3)[-1]) signed_user = linksign.process_signature(req) assert signed_user is None
def handle_user_report(self, payload, project, **kwargs): metrics.incr("mail_adapter.handle_user_report") group = Group.objects.get(id=payload["report"]["issue"]["id"]) participants = GroupSubscription.objects.get_participants(group=group) if not participants: return org = group.organization enhanced_privacy = org.flags.enhanced_privacy context = { "project": project, "project_link": absolute_uri(u"/{}/{}/".format(project.organization.slug, project.slug)), "issue_link": absolute_uri(u"/{}/{}/issues/{}/".format( project.organization.slug, project.slug, payload["report"]["issue"]["id"])), # TODO(dcramer): we dont have permalinks to feedback yet "link": absolute_uri(u"/{}/{}/issues/{}/feedback/".format( project.organization.slug, project.slug, payload["report"]["issue"]["id"])), "group": group, "report": payload["report"], "enhanced_privacy": enhanced_privacy, } subject_prefix = self._build_subject_prefix(project) subject = force_text(u"{}{} - New Feedback from {}".format( subject_prefix, group.qualified_short_id, payload["report"]["name"])) headers = {"X-Sentry-Project": project.slug} # TODO(dcramer): this is copypasta'd from activity notifications # and while it'd be nice to re-use all of that, they are currently # coupled to <Activity> instances which makes this tough for user, reason in participants.items(): context.update({ "reason": GroupSubscriptionReason.descriptions.get( reason, "are subscribed to this issue"), "unsubscribe_link": generate_signed_link( user.id, "sentry-account-email-unsubscribe-issue", kwargs={"issue_id": group.id}, ), }) msg = MessageBuilder( subject=subject, template="sentry/emails/activity/new-user-feedback.txt", html_template="sentry/emails/activity/new-user-feedback.html", headers=headers, type="notify.user-report", context=context, reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def generate_unsubscribe_link(self, user_id): return generate_signed_link( user_id, "sentry-account-email-unsubscribe-project", kwargs={"project_id": self.project.id}, )
def handle_user_report(self, payload, project, **kwargs): from sentry.models import Group, GroupSubscription, GroupSubscriptionReason group = Group.objects.get(id=payload['report']['issue']['id']) participants = GroupSubscription.objects.get_participants(group=group) if not participants: return context = { 'project': project, 'project_link': absolute_uri(u'/{}/{}/'.format( project.organization.slug, project.slug, )), 'issue_link': absolute_uri(u'/{}/{}/issues/{}/'.format( project.organization.slug, project.slug, payload['report']['issue']['id'], )), # TODO(dcramer): we dont have permalinks to feedback yet 'link': absolute_uri(u'/{}/{}/issues/{}/feedback/'.format( project.organization.slug, project.slug, payload['report']['issue']['id'], )), 'group': group, 'report': payload['report'], } subject_prefix = self.get_option('subject_prefix', project) or self._subject_prefix() subject_prefix = force_text(subject_prefix) subject = force_text(u'{}{} - New Feedback from {}'.format( subject_prefix, group.qualified_short_id, payload['report']['name'], )) headers = { 'X-Sentry-Project': project.slug, } # TODO(dcramer): this is copypasta'd from activity notifications # and while it'd be nice to re-use all of that, they are currently # coupled to <Activity> instances which makes this tough for user, reason in participants.items(): context.update({ 'reason': GroupSubscriptionReason.descriptions.get( reason, "are subscribed to this issue", ), 'unsubscribe_link': generate_signed_link( user.id, 'sentry-account-email-unsubscribe-issue', kwargs={'issue_id': group.id}, ), }) msg = MessageBuilder( subject=subject, template='sentry/emails/activity/new-user-feedback.txt', html_template='sentry/emails/activity/new-user-feedback.html', headers=headers, type='notify.user-report', context=context, reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def get_processing_issues(user, projects, include_detailed_issues=False): """ Given a list of projects, returns a list containing stats about processing issues for those projects :param include_detailed_issues: Include specific details on each processing issue :return: A list of dicts, with each dict containing keys: - 'hasIssues': Whether the project has any processing issues - 'numIssues': How many processing issues the project has - 'lastSeen': The date a processing issue was last seen - 'resolveableIssues': How many Raw Events have no remaining issues and can be resolved automatically - 'hasMoreResolveableIssues': Whether there are any Raw Events that have no remaining issues and can be resolved automatically 'issuesProcessing': How many ReprocessingReports exist for this Project 'signedLink': Signed link that takes the user to the reprocessing page for this project 'project': Slug for the project """ project_agg_results = { result["project"]: result for result in ProcessingIssue.objects.filter( project__in=projects).values("project").annotate( num_issues=Count("id"), last_seen=Max("datetime")) } project_reprocessing_issues = { result["project"]: result["reprocessing_issues"] for result in ReprocessingReport.objects.filter( project__in=projects).values("project").annotate( reprocessing_issues=Count("id")) } resolved_qs = ProcessingIssue.objects.find_resolved_queryset( [p.id for p in projects]) project_resolveable = { result["project"]: result["count"] for result in resolved_qs.values("project").annotate(count=Count("id")) } if include_detailed_issues: project_issues = defaultdict(list) for proc_issue in (ProcessingIssue.objects.with_num_events().filter( project__in=projects).order_by("type", "datetime")): project_issues[proc_issue.project_id].append(proc_issue) project_results = [] for project in projects: agg_results = project_agg_results.get(project.id, {}) num_issues = agg_results.get("num_issues", 0) signed_link = None if num_issues > 0: signed_link = generate_signed_link( user, "sentry-api-0-project-fix-processing-issues", kwargs={ "project_slug": project.slug, "organization_slug": project.organization.slug, }, ) last_seen = agg_results.get("last_seen") data = { "hasIssues": num_issues > 0, "numIssues": num_issues, "lastSeen": last_seen and serialize(last_seen) or None, "resolveableIssues": project_resolveable.get(project.id, 0), # XXX: Due to a bug in `find_resolved`, this was always returning # False. It's unused in our frontend, so just defaulting to False # so that we don't break any other consumers that expect this value. "hasMoreResolveableIssues": False, "issuesProcessing": project_reprocessing_issues.get(project.id, 0), "signedLink": signed_link, "project": project.slug, } if include_detailed_issues: issues = project_issues[project.id] data["issues"] = [serialize(issue, user) for issue in issues] project_results.append(data) return project_results
def get_processing_issues(user, projects, include_detailed_issues=False): """ Given a list of projects, returns a list containing stats about processing issues for those projects :param include_detailed_issues: Include specific details on each processing issue :return: A list of dicts, with each dict containing keys: - 'hasIssues': Whether the project has any processing issues - 'numIssues': How many processing issues the project has - 'lastSeen': The date a processing issue was last seen - 'resolveableIssues': How many Raw Events have no remaining issues and can be resolved automatically - 'hasMoreResolveableIssues': Whether there are any Raw Events that have no remaining issues and can be resolved automatically 'issuesProcessing': How many ReprocessingReports exist for this Project 'signedLink': Signed link that takes the user to the reprocessing page for this project 'project': Slug for the project """ project_agg_results = { result['project']: result for result in ProcessingIssue.objects.filter( project__in=projects, ).values('project').annotate( num_issues=Count('id'), last_seen=Max('datetime'), ) } project_reprocessing_issues = { result['project']: result['reprocessing_issues'] for result in ReprocessingReport.objects.filter( project__in=projects, ).values('project').annotate(reprocessing_issues=Count('id')) } resolved_qs = ProcessingIssue.objects.find_resolved_queryset([p.id for p in projects]) project_resolveable = { result['project']: result['count'] for result in resolved_qs.values('project').annotate(count=Count('id')) } if include_detailed_issues: project_issues = defaultdict(list) for proc_issue in ProcessingIssue.objects.with_num_events().filter( project__in=projects, ).order_by('type', 'datetime'): project_issues[proc_issue.project_id].append(proc_issue) project_results = [] for project in projects: agg_results = project_agg_results.get(project.id, {}) num_issues = agg_results.get('num_issues', 0) signed_link = None if num_issues > 0: signed_link = generate_signed_link( user, 'sentry-api-0-project-fix-processing-issues', kwargs={ 'project_slug': project.slug, 'organization_slug': project.organization.slug, } ) last_seen = agg_results.get('last_seen') data = { 'hasIssues': num_issues > 0, 'numIssues': num_issues, 'lastSeen': last_seen and serialize(last_seen) or None, 'resolveableIssues': project_resolveable.get(project.id, 0), # XXX: Due to a bug in `find_resolved`, this was always returning # False. It's unused in our frontend, so just defaulting to False # so that we don't break any other consumers that expect this value. 'hasMoreResolveableIssues': False, 'issuesProcessing': project_reprocessing_issues.get(project.id, 0), 'signedLink': signed_link, 'project': project.slug, } if include_detailed_issues: issues = project_issues[project.id] data['issues'] = [serialize(issue, user) for issue in issues] project_results.append(data) return project_results
def handle_user_report(self, payload, project, **kwargs): from sentry.models import Group, GroupSubscription, GroupSubscriptionReason group = Group.objects.get(id=payload['report']['issue']['id']) participants = GroupSubscription.objects.get_participants(group=group) if not participants: return context = { 'project': project, 'project_link': absolute_uri('/{}/{}/'.format( project.organization.slug, project.slug, )), 'issue_link': absolute_uri('/{}/{}/issues/{}/'.format( project.organization.slug, project.slug, payload['report']['issue']['id'], )), # TODO(dcramer): we dont have permalinks to feedback yet 'link': absolute_uri('/{}/{}/issues/{}/feedback/'.format( project.organization.slug, project.slug, payload['report']['issue']['id'], )), 'group': group, 'report': payload['report'], } subject_prefix = self.get_option('subject_prefix', project) or self._subject_prefix() subject_prefix = force_text(subject_prefix) subject = force_text(u'{}{} - New Feedback from {}'.format( subject_prefix, group.qualified_short_id, payload['report']['name'], )) headers = { 'X-Sentry-Project': project.slug, } # TODO(dcramer): this is copypasta'd from activity notifications # and while it'd be nice to re-use all of that, they are currently # coupled to <Activity> instances which makes this tough for user, reason in participants.items(): context.update({ 'reason': GroupSubscriptionReason.descriptions.get( reason, "are subscribed to this issue", ), 'unsubscribe_link': generate_signed_link( user.id, 'sentry-account-email-unsubscribe-issue', kwargs={'issue_id': group.id}, ), }) msg = MessageBuilder( subject=subject, template='sentry/emails/activity/new-user-feedback.txt', html_template='sentry/emails/activity/new-user-feedback.html', headers=headers, type='notify.user-report', context=context, reference=group, ) msg.add_users([user.id], project=project) msg.send_async()
def get_unsubscribe_link(user_id: int, group_id: int) -> str: return generate_signed_link( user_id, "sentry-account-email-unsubscribe-issue", kwargs={"issue_id": group_id}, )