diff --git a/CHANGES b/CHANGES index 4b3c22f51bb68e..8723a085ab6e0e 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,8 @@ Version 8.13 (Unreleased) ------------------------- +- Support for setting a custom security header for javascript fetching. + Version 8.12 ------------ diff --git a/src/sentry/lang/javascript/processor.py b/src/sentry/lang/javascript/processor.py index 781cc6a2107a89..f77ba841662b3a 100644 --- a/src/sentry/lang/javascript/processor.py +++ b/src/sentry/lang/javascript/processor.py @@ -353,7 +353,11 @@ def fetch_file(url, project=None, release=None, allow_scraping=True): if project and is_valid_origin(url, project=project): token = project.get_option('sentry:token') if token: - headers['X-Sentry-Token'] = token + token_header = project.get_option( + 'sentry:token_header', + 'X-Sentry-Token', + ) + headers[token_header] = token logger.debug('Fetching %r from the internet', url) diff --git a/src/sentry/templates/sentry/projects/manage.html b/src/sentry/templates/sentry/projects/manage.html index c2467240a28a6d..088a9d3556d910 100644 --- a/src/sentry/templates/sentry/projects/manage.html +++ b/src/sentry/templates/sentry/projects/manage.html @@ -84,6 +84,7 @@

{% trans "Client Security" %}

{% endwith %} {{ form.scrape_javascript|as_crispy_field }} {{ form.token|as_crispy_field }} + {{ form.token_header|as_crispy_field }} {{ form.blacklisted_ips|as_crispy_field }} diff --git a/src/sentry/web/frontend/project_settings.py b/src/sentry/web/frontend/project_settings.py index 35e5992997d29d..9f6fc66d67922d 100644 --- a/src/sentry/web/frontend/project_settings.py +++ b/src/sentry/web/frontend/project_settings.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +import re + from django import forms from django.contrib import messages from django.core.urlresolvers import reverse @@ -29,8 +31,19 @@ class EditProjectForm(forms.ModelForm): team = CustomTypedChoiceField(choices=(), coerce=int, required=False) origins = OriginsField(label=_('Allowed Domains'), required=False, help_text=_('Separate multiple entries with a newline.')) - token = forms.CharField(label=_('Security token'), required=True, - help_text=_('Outbound requests matching Allowed Domains will have the header "X-Sentry-Token: {token}" appended.')) + token = forms.CharField( + label=_('Security token'), + help_text=_('Outbound requests matching Allowed Domains will have the header "{token_header}: {token}" appended.'), + required=True, + ) + token_header = forms.CharField( + label=_('Security token header'), + help_text=_('Outbound requests matching Allowed Domains will have the header "{token_header}: {token}" appended.'), + widget=forms.TextInput(attrs={ + 'placeholder': _('X-Sentry-Token'), + }), + required=False, + ) resolve_age = RangeField(label=_('Auto resolve'), required=False, min_value=0, max_value=168, step_value=1, help_text=_('Automatically resolve an issue if it hasn\'t been seen for this amount of time.')) @@ -187,6 +200,24 @@ def clean_slug(self): 'using that slug' % other.name) return slug + def clean_token(self): + token = self.cleaned_data.get('token') + if not token: + return + token_re = r'^[-a-zA-Z0-9+/= ]{1,255}$' + if not re.match(token_re, token): + raise forms.ValidationError('Invalid security token, must be: %s' % token_re) + return token + + def clean_token_header(self): + token_header = self.cleaned_data.get('token_header') + if not token_header: + return + header_re = r'^[a-zA-Z0-9-]{1,20}$' + if not re.match(header_re, token_header): + raise forms.ValidationError('Invalid header value, must be: %s' % header_re) + return token_header + class ProjectSettingsView(ProjectView): required_scope = 'project:write' @@ -213,6 +244,7 @@ def get_form(self, request, project): initial={ 'origins': '\n'.join(project.get_option('sentry:origins', ['*'])), 'token': security_token, + 'token_header': project.get_option('sentry:token_header'), 'resolve_age': int(project.get_option('sentry:resolve_age', 0)), 'scrub_data': bool(project.get_option('sentry:scrub_data', True)), 'scrub_defaults': bool(project.get_option('sentry:scrub_defaults', True)), @@ -235,6 +267,7 @@ def handle(self, request, organization, team, project): for opt in ( 'origins', 'token', + 'token_header', 'resolve_age', 'scrub_data', 'scrub_defaults', diff --git a/tests/sentry/web/frontend/test_project_settings.py b/tests/sentry/web/frontend/test_project_settings.py index 0cc0be36989fc8..55e30e7ed67851 100644 --- a/tests/sentry/web/frontend/test_project_settings.py +++ b/tests/sentry/web/frontend/test_project_settings.py @@ -80,7 +80,8 @@ def test_valid_params(self): 'slug': self.project.slug, 'team': self.team.id, 'scrub_data': '1', - 'token': 'foobar', + 'token': 'Basic Zm9vOmJhcg==', + 'token_header': 'Authorization' }) assert resp.status_code == 302 self.assertEquals(resp['Location'], 'http://testserver' + self.path)