diff --git a/codespeed/__init__.py b/codespeed/__init__.py index e69de29b..8c1ca00e 100644 --- a/codespeed/__init__.py +++ b/codespeed/__init__.py @@ -0,0 +1 @@ +default_app_config = 'codespeed.apps.CodespeedConfig' diff --git a/codespeed/apps.py b/codespeed/apps.py new file mode 100644 index 00000000..354c4064 --- /dev/null +++ b/codespeed/apps.py @@ -0,0 +1,18 @@ +from django.apps import AppConfig +from django.conf import settings + + +class CodespeedConfig(AppConfig): + name = 'codespeed' + + def ready(self): + import warnings + if settings.ALLOW_ANONYMOUS_POST: + warnings.warn("Results can be posted by unregistered users") + warnings.warn( + "In the future anonymous posting will be disabled by default", + category=FutureWarning) + elif not settings.REQUIRE_SECURE_AUTH: + warnings.warn( + "REQUIRE_SECURE_AUTH is not True. This server may prompt for" + " user credentials to be submitted in plaintext") diff --git a/codespeed/auth.py b/codespeed/auth.py new file mode 100644 index 00000000..85764dcb --- /dev/null +++ b/codespeed/auth.py @@ -0,0 +1,65 @@ +import logging +from functools import wraps +from django.contrib.auth import authenticate, login +from django.http import HttpResponse, HttpResponseForbidden +from django.conf import settings +from base64 import b64decode + +__ALL__ = ['basic_auth_required'] +logger = logging.getLogger(__name__) + + +def basic_auth_required(realm='default'): + def _helper(func): + @wraps(func) + def _decorator(request, *args, **kwargs): + allowed = False + logger.info('request is secure? {}'.format(request.is_secure())) + if settings.ALLOW_ANONYMOUS_POST: + logger.debug('allowing anonymous post') + allowed = True + elif hasattr(request, 'user') and request.user.is_authenticated(): + allowed = True + elif 'HTTP_AUTHORIZATION' in request.META: + logger.debug('checking for http authorization header') + if settings.REQUIRE_SECURE_AUTH and not request.is_secure(): + return insecure_connection_response() + http_auth = request.META['HTTP_AUTHORIZATION'] + authmeth, auth = http_auth.split(' ', 1) + if authmeth.lower() == 'basic': + username, password = decode_basic_auth(auth) + user = authenticate(username=username, password=password) + if user is not None and user.is_active: + logger.info( + 'Authentication succeeded for {}'.format(username)) + login(request, user) + allowed = True + else: + logger.info( + 'Failed auth for {}'.format(username)) + return HttpResponseForbidden() + if allowed: + return func(request, *args, **kwargs) + + if settings.REQUIRE_SECURE_AUTH and not request.is_secure(): + logger.debug('not requesting auth over an insecure channel') + return insecure_connection_response() + else: + res = HttpResponse() + res.status_code = 401 + res.reason_phrase = 'Unauthorized' + res['WWW-Authenticate'] = 'Basic realm="{}"'.format(realm) + return res + return _decorator + + return _helper + + +def insecure_connection_response(): + return HttpResponseForbidden('Secure connection required') + + +def decode_basic_auth(auth): + authb = b64decode(auth.strip()) + auth = authb.decode() + return auth.split(':', 1) diff --git a/codespeed/settings.py b/codespeed/settings.py index 9c298a09..912ea21e 100644 --- a/codespeed/settings.py +++ b/codespeed/settings.py @@ -68,3 +68,7 @@ # ('myexe', 'L'),] USE_MEDIAN_BANDS = True # True to enable median bands on Timeline view + + +ALLOW_ANONYMOUS_POST = True # Whether anonymous users can post results +REQUIRE_SECURE_AUTH = True # Whether auth needs to be over a secure channel diff --git a/codespeed/tests/test_views.py b/codespeed/tests/test_views.py index 33db6590..28588294 100644 --- a/codespeed/tests/test_views.py +++ b/codespeed/tests/test_views.py @@ -3,13 +3,14 @@ import copy import json -from django.test import TestCase +from django.test import TestCase, override_settings from django.core.urlresolvers import reverse from codespeed.models import (Project, Benchmark, Revision, Branch, Executable, Environment, Result, Report) +@override_settings(ALLOW_ANONYMOUS_POST=True) class TestAddResult(TestCase): def setUp(self): @@ -162,6 +163,7 @@ def test_add_result_with_no_project(self): response.content.decode(), "Result data saved successfully") +@override_settings(ALLOW_ANONYMOUS_POST=True) class TestAddJSONResults(TestCase): def setUp(self): @@ -361,6 +363,7 @@ def test_gettimelinedata(self): [u'2011/04/13 17:04:22 ', 2000.0, 1.11111, u'2', u'', u'default']) +@override_settings(ALLOW_ANONYMOUS_POST=True) class TestReports(TestCase): def setUp(self): diff --git a/codespeed/views.py b/codespeed/views.py index e7a3552e..370a3819 100644 --- a/codespeed/views.py +++ b/codespeed/views.py @@ -14,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt from django.template import RequestContext from django.conf import settings +from .auth import basic_auth_required from .models import (Environment, Report, Project, Revision, Result, Executable, Benchmark, Branch) @@ -697,6 +698,7 @@ def displaylogs(request): @csrf_exempt @require_POST +@basic_auth_required('results') def add_result(request): response, error = save_result(request.POST) if error: @@ -710,6 +712,7 @@ def add_result(request): @csrf_exempt @require_POST +@basic_auth_required('results') def add_json_results(request): if not request.POST.get('json'): return HttpResponseBadRequest("No key 'json' in POST payload")