Bug 1160111 - Add hawk authentication scheme

The hawk credentials lookup function is the glue between hawk and
the `application` django app. I wrote tests to verify its logic,
everything else is mostly configuration code.
This commit is contained in:
Mauro Doglio 2015-09-11 16:04:23 +01:00
Родитель 38a2e3a1b5
Коммит 8b669ee9cd
7 изменённых файлов: 148 добавлений и 13 удалений

Просмотреть файл

@ -1,19 +1,24 @@
import oauth2 as oauth
from django.contrib.auth.models import User
from django.core.urlresolvers import resolve, reverse
from mohawk import Sender
import pytest
from rest_framework.decorators import APIView
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from treeherder.credentials.models import Credentials
from treeherder.etl.oauth_utils import OAuthCredentials
from treeherder.webapp.api.auth import TwoLeggedOauthAuthentication
from treeherder.webapp.api import permissions
class AuthenticatedView(APIView):
authentication_classes = [TwoLeggedOauthAuthentication]
permission_classes = (permissions.HasHawkOrLegacyOauthPermissions,)
def get(self, request, *args, **kwargs):
return Response({'authenticated': hasattr(request, 'legacy_oauth_authenticated')})
return Response({'authenticated': True})
def post(self, request, *args, **kwargs):
return Response({'authenticated': True})
factory = APIRequestFactory()
@ -89,3 +94,95 @@ def test_two_legged_oauth_project_via_user(monkeypatch, jm, set_oauth_credential
response = view(request)
assert response.data == {'authenticated': True}
@pytest.fixture
def api_user(request):
user = User.objects.create_user('MyUser')
def fin():
user.delete()
request.addfinalizer(fin)
return user
@pytest.fixture
def client_credentials(request, api_user):
client_credentials = Credentials.objects.create(
client_id='test-credentials', owner=api_user)
def fin():
client_credentials.delete()
request.addfinalizer(fin)
return client_credentials
def _get_hawk_response(client_id, secret, method='GET',
content='', content_type='application/json'):
auth = {
'id': client_id,
'key': secret,
'algorithm': 'sha256'
}
url = 'http://testserver/'
sender = Sender(auth, url, method,
content=content,
content_type='application/json')
do_request = getattr(factory, method.lower())
request = do_request(url,
data=content,
content_type='application/json',
# factory.get doesn't set the CONTENT_TYPE header
# I'm setting it manually here for simplicity
CONTENT_TYPE='application/json',
HTTP_AUTHORIZATION=sender.request_header)
view = AuthenticatedView.as_view()
return view(request)
def test_get_hawk_authorized(client_credentials):
client_credentials.authorized = True
client_credentials.save()
response = _get_hawk_response(client_credentials.client_id,
str(client_credentials.secret))
assert response.data == {'authenticated': True}
def test_get_hawk_unauthorized(client_credentials):
response = _get_hawk_response(client_credentials.client_id,
str(client_credentials.secret))
assert response.data == {'detail': ('No authentication credentials '
'found with id %s') % client_credentials.client_id}
def test_post_hawk_authorized(client_credentials):
client_credentials.authorized = True
client_credentials.save()
response = _get_hawk_response(client_credentials.client_id,
str(client_credentials.secret), method='POST',
content="{'this': 'that'}")
assert response.data == {'authenticated': True}
def test_post_hawk_unauthorized(client_credentials):
response = _get_hawk_response(client_credentials.client_id,
str(client_credentials.secret), method='POST',
content="{'this': 'that'}")
assert response.data == {'detail': ('No authentication credentials '
'found with id %s') % client_credentials.client_id}
def test_no_auth():
url = u'http://testserver/'
request = factory.get(url)
view = AuthenticatedView.as_view()
response = view(request)
assert response.data == {'detail': 'Authentication credentials were not provided.'}

Просмотреть файл

@ -72,6 +72,7 @@ MIDDLEWARE_CLASSES = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'corsheaders.middleware.CorsMiddleware',
'hawkrest.middleware.HawkResponseMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@ -107,6 +108,7 @@ INSTALLED_APPS = [
'rest_framework',
'rest_framework_extensions',
'rest_framework_swagger',
'hawkrest',
'corsheaders',
'django_browserid',
# treeherder apps
@ -272,6 +274,7 @@ REST_FRAMEWORK = {
'ALLOWED_VERSIONS': ('1.0',),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'hawkrest.HawkAuthentication',
'treeherder.webapp.api.auth.TwoLeggedOauthAuthentication',
)
}
@ -451,3 +454,5 @@ SWAGGER_SETTINGS = {"enabled_methods": ['get', ]}
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 15
}
HAWK_CREDENTIALS_LOOKUP = 'treeherder.webapp.api.auth.hawk_lookup'

Просмотреть файл

@ -9,7 +9,7 @@ from treeherder.webapp.api.utils import UrlQueryFilter
class ArtifactViewSet(viewsets.ViewSet):
permission_classes = (permissions.HasLegacyOauthPermissionsOrReadOnly,)
permission_classes = (permissions.HasHawkOrLegacyOauthPermissionsOrReadOnly,)
"""
This viewset is responsible for the artifact endpoint.

Просмотреть файл

@ -4,6 +4,7 @@ from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from rest_framework.renderers import JSONRenderer
from treeherder.credentials.models import Credentials
from treeherder.etl.oauth_utils import OAuthCredentials
@ -80,3 +81,17 @@ class TwoLeggedOauthAuthentication(BaseAuthentication):
)
request.legacy_oauth_authenticated = True
return (DummyUser(), None)
def hawk_lookup(id):
try:
credentials = Credentials.objects.get(client_id=id, authorized=True)
except Credentials.DoesNotExist:
raise exceptions.AuthenticationFailed(
'No authentication credentials found with id %s' % id)
return {
'id': id,
'key': str(credentials.secret),
'algorithm': 'sha256'
}

Просмотреть файл

@ -19,7 +19,7 @@ class JobsViewSet(viewsets.ViewSet):
"""
throttle_scope = 'jobs'
permission_classes = (permissions.HasLegacyOauthPermissionsOrReadOnly,)
permission_classes = (permissions.HasHawkOrLegacyOauthPermissionsOrReadOnly,)
@with_jobs
def retrieve(self, request, project, jm, pk=None):

Просмотреть файл

@ -1,3 +1,4 @@
from mohawk import Receiver
from rest_framework import permissions
@ -37,10 +38,28 @@ class HasLegacyOauthPermissions(permissions.BasePermission):
return hasattr(request, 'legacy_oauth_authenticated')
class HasLegacyOauthPermissionsOrReadOnly(permissions.BasePermission):
class HasHawkPermissions(permissions.BasePermission):
def has_permission(self, request, view):
hawk_header = 'hawk.receiver'
if hawk_header in request.META and isinstance(request.META[hawk_header], Receiver):
return True
return False
class HasHawkOrLegacyOauthPermissions(permissions.BasePermission):
def has_permission(self, request, view):
return (HasHawkPermissions().has_permission(request, view) or
HasLegacyOauthPermissions().has_permission(request, view))
class HasHawkOrLegacyOauthPermissionsOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return hasattr(request, 'legacy_oauth_authenticated')
return HasHawkOrLegacyOauthPermissions().has_permission(request, view)

Просмотреть файл

@ -6,8 +6,7 @@ from rest_framework.response import Response
from rest_framework.reverse import reverse
from treeherder.model.derived import DatasetNotFoundError
from treeherder.webapp.api.permissions import (HasLegacyOauthPermissionsOrReadOnly,
IsStaffOrReadOnly)
from treeherder.webapp.api import permissions
from treeherder.webapp.api.utils import UrlQueryFilter, to_timestamp, with_jobs
@ -19,7 +18,7 @@ class ResultSetViewSet(viewsets.ViewSet):
``result sets`` are synonymous with ``pushes`` in the ui
"""
throttle_scope = 'resultset'
permission_classes = (HasLegacyOauthPermissionsOrReadOnly,)
permission_classes = (permissions.HasHawkOrLegacyOauthPermissionsOrReadOnly,)
@with_jobs
def list(self, request, project, jm):
@ -132,7 +131,7 @@ class ResultSetViewSet(viewsets.ViewSet):
except Exception as ex:
return Response("Exception: {0}".format(ex), 404)
@detail_route(methods=['post'], permission_classes=[IsStaffOrReadOnly])
@detail_route(methods=['post'], permission_classes=[permissions.IsStaffOrReadOnly])
@with_jobs
def trigger_missing_jobs(self, request, project, jm, pk=None):
"""
@ -148,7 +147,7 @@ class ResultSetViewSet(viewsets.ViewSet):
except Exception as ex:
return Response("Exception: {0}".format(ex), 404)
@detail_route(methods=['post'], permission_classes=[IsStaffOrReadOnly])
@detail_route(methods=['post'], permission_classes=[permissions.IsStaffOrReadOnly])
@with_jobs
def trigger_all_talos_jobs(self, request, project, jm, pk=None):
"""