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)