Skip to content

Commit 7955cd2

Browse files
authored
Merge pull request tobami#214 from mwatts15/34-add-security-measures-to-posting
Added http basic authentication to results resources
2 parents ad9ede1 + d0483f4 commit 7955cd2

6 files changed

Lines changed: 95 additions & 1 deletion

File tree

codespeed/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
default_app_config = 'codespeed.apps.CodespeedConfig'

codespeed/apps.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from django.apps import AppConfig
2+
from django.conf import settings
3+
4+
5+
class CodespeedConfig(AppConfig):
6+
name = 'codespeed'
7+
8+
def ready(self):
9+
import warnings
10+
if settings.ALLOW_ANONYMOUS_POST:
11+
warnings.warn("Results can be posted by unregistered users")
12+
warnings.warn(
13+
"In the future anonymous posting will be disabled by default",
14+
category=FutureWarning)
15+
elif not settings.REQUIRE_SECURE_AUTH:
16+
warnings.warn(
17+
"REQUIRE_SECURE_AUTH is not True. This server may prompt for"
18+
" user credentials to be submitted in plaintext")

codespeed/auth.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
from functools import wraps
3+
from django.contrib.auth import authenticate, login
4+
from django.http import HttpResponse, HttpResponseForbidden
5+
from django.conf import settings
6+
from base64 import b64decode
7+
8+
__ALL__ = ['basic_auth_required']
9+
logger = logging.getLogger(__name__)
10+
11+
12+
def basic_auth_required(realm='default'):
13+
def _helper(func):
14+
@wraps(func)
15+
def _decorator(request, *args, **kwargs):
16+
allowed = False
17+
logger.info('request is secure? {}'.format(request.is_secure()))
18+
if settings.ALLOW_ANONYMOUS_POST:
19+
logger.debug('allowing anonymous post')
20+
allowed = True
21+
elif hasattr(request, 'user') and request.user.is_authenticated():
22+
allowed = True
23+
elif 'HTTP_AUTHORIZATION' in request.META:
24+
logger.debug('checking for http authorization header')
25+
if settings.REQUIRE_SECURE_AUTH and not request.is_secure():
26+
return insecure_connection_response()
27+
http_auth = request.META['HTTP_AUTHORIZATION']
28+
authmeth, auth = http_auth.split(' ', 1)
29+
if authmeth.lower() == 'basic':
30+
username, password = decode_basic_auth(auth)
31+
user = authenticate(username=username, password=password)
32+
if user is not None and user.is_active:
33+
logger.info(
34+
'Authentication succeeded for {}'.format(username))
35+
login(request, user)
36+
allowed = True
37+
else:
38+
logger.info(
39+
'Failed auth for {}'.format(username))
40+
return HttpResponseForbidden()
41+
if allowed:
42+
return func(request, *args, **kwargs)
43+
44+
if settings.REQUIRE_SECURE_AUTH and not request.is_secure():
45+
logger.debug('not requesting auth over an insecure channel')
46+
return insecure_connection_response()
47+
else:
48+
res = HttpResponse()
49+
res.status_code = 401
50+
res.reason_phrase = 'Unauthorized'
51+
res['WWW-Authenticate'] = 'Basic realm="{}"'.format(realm)
52+
return res
53+
return _decorator
54+
55+
return _helper
56+
57+
58+
def insecure_connection_response():
59+
return HttpResponseForbidden('Secure connection required')
60+
61+
62+
def decode_basic_auth(auth):
63+
authb = b64decode(auth.strip())
64+
auth = authb.decode()
65+
return auth.split(':', 1)

codespeed/settings.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,7 @@
6868
# ('myexe', 'L'),]
6969

7070
USE_MEDIAN_BANDS = True # True to enable median bands on Timeline view
71+
72+
73+
ALLOW_ANONYMOUS_POST = True # Whether anonymous users can post results
74+
REQUIRE_SECURE_AUTH = True # Whether auth needs to be over a secure channel

codespeed/tests/test_views.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import copy
44
import json
55

6-
from django.test import TestCase
6+
from django.test import TestCase, override_settings
77
from django.core.urlresolvers import reverse
88

99
from codespeed.models import (Project, Benchmark, Revision, Branch, Executable,
1010
Environment, Result, Report)
1111

1212

13+
@override_settings(ALLOW_ANONYMOUS_POST=True)
1314
class TestAddResult(TestCase):
1415

1516
def setUp(self):
@@ -162,6 +163,7 @@ def test_add_result_with_no_project(self):
162163
response.content.decode(), "Result data saved successfully")
163164

164165

166+
@override_settings(ALLOW_ANONYMOUS_POST=True)
165167
class TestAddJSONResults(TestCase):
166168

167169
def setUp(self):
@@ -361,6 +363,7 @@ def test_gettimelinedata(self):
361363
[u'2011/04/13 17:04:22 ', 2000.0, 1.11111, u'2', u'', u'default'])
362364

363365

366+
@override_settings(ALLOW_ANONYMOUS_POST=True)
364367
class TestReports(TestCase):
365368

366369
def setUp(self):

codespeed/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from django.views.decorators.csrf import csrf_exempt
1515
from django.template import RequestContext
1616
from django.conf import settings
17+
from .auth import basic_auth_required
1718

1819
from .models import (Environment, Report, Project, Revision, Result,
1920
Executable, Benchmark, Branch)
@@ -697,6 +698,7 @@ def displaylogs(request):
697698

698699
@csrf_exempt
699700
@require_POST
701+
@basic_auth_required('results')
700702
def add_result(request):
701703
response, error = save_result(request.POST)
702704
if error:
@@ -710,6 +712,7 @@ def add_result(request):
710712

711713
@csrf_exempt
712714
@require_POST
715+
@basic_auth_required('results')
713716
def add_json_results(request):
714717
if not request.POST.get('json'):
715718
return HttpResponseBadRequest("No key 'json' in POST payload")

0 commit comments

Comments
 (0)