зеркало из https://github.com/mozilla/treeherder.git
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:
Родитель
38a2e3a1b5
Коммит
8b669ee9cd
|
@ -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):
|
||||
"""
|
||||
|
|
Загрузка…
Ссылка в новой задаче