Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Version 8.13 (Unreleased)
-------------------------

- Support for setting a custom security header for javascript fetching.

Version 8.12
------------

Expand Down
6 changes: 5 additions & 1 deletion src/sentry/lang/javascript/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/sentry/templates/sentry/projects/manage.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ <h3>{% trans "Client Security" %}</h3>
{% endwith %}
{{ form.scrape_javascript|as_crispy_field }}
{{ form.token|as_crispy_field }}
{{ form.token_header|as_crispy_field }}
{{ form.blacklisted_ips|as_crispy_field }}
</div>
</div>
Expand Down
37 changes: 35 additions & 2 deletions src/sentry/web/frontend/project_settings.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.'))
Expand Down Expand Up @@ -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}$'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These patterns are effectively arbitrary and likely over conservative, but should be sufficient for anyone using this and prevents someone putting in complete garbage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically you can basic auth this now. Might make sense to mention.

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'
Expand All @@ -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)),
Expand All @@ -235,6 +267,7 @@ def handle(self, request, organization, team, project):
for opt in (
'origins',
'token',
'token_header',
'resolve_age',
'scrub_data',
'scrub_defaults',
Expand Down
3 changes: 2 additions & 1 deletion tests/sentry/web/frontend/test_project_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down