0002-rest_authentication-add-api-client-authentication-67.patch
debian/debian_config_common.py | ||
---|---|---|
397 | 397 |
if 'rest_framework' in INSTALLED_APPS: |
398 | 398 |
if 'REST_FRAMEWORK' not in globals(): |
399 | 399 |
REST_FRAMEWORK = {} |
400 |
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = ('hobo.rest_authentication.PublikAuthentication',) |
|
400 |
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = ( |
|
401 |
'hobo.rest_authentication.PublikAuthentication', |
|
402 |
'hobo.rest_authentication.APIClientAuthentication', |
|
403 |
) |
|
401 | 404 |
REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',) |
402 | 405 |
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] = ('rest_framework.renderers.JSONRenderer',) |
403 | 406 |
hobo/rest_authentication.py | ||
---|---|---|
1 | 1 |
import logging |
2 | 2 | |
3 |
import requests |
|
3 | 4 |
from django.conf import settings |
4 | 5 |
from django.contrib.auth import get_user_model |
5 | 6 |
from django.contrib.auth.models import AnonymousUser |
... | ... | |
8 | 9 |
from rest_framework import authentication, exceptions, status |
9 | 10 | |
10 | 11 |
from hobo import signature |
12 |
from hobo.requests_wrapper import Requests |
|
11 | 13 | |
12 | 14 |
try: |
13 | 15 |
from mellon.models import UserSAMLIdentifier |
... | ... | |
51 | 53 |
return 'Publik Service Admin' |
52 | 54 | |
53 | 55 | |
56 |
class APIClientUser: |
|
57 | ||
58 |
is_active = True |
|
59 |
is_anonymous = False |
|
60 |
is_authenticated = True |
|
61 |
is_superuser = False |
|
62 |
roles = [] |
|
63 | ||
64 |
def __init__(self, is_active, is_anonymous, is_authenticated, is_superuser, roles): |
|
65 |
self.is_active = is_active |
|
66 |
self.is_anonymous = is_anonymous |
|
67 |
self.is_authenticated = is_authenticated |
|
68 |
self.is_superuser = is_superuser |
|
69 |
self.roles = roles |
|
70 | ||
71 |
@classmethod |
|
72 |
def from_dict(cls, data): |
|
73 |
return cls( |
|
74 |
is_active=data['is_active'], |
|
75 |
is_anonymous=data['is_anonymous'], |
|
76 |
is_authenticated=data['is_authenticated'], |
|
77 |
is_superuser=data['is_superuser'], |
|
78 |
roles=data['roles'], |
|
79 |
) |
|
80 | ||
81 | ||
54 | 82 |
class PublikAuthenticationFailed(exceptions.APIException): |
55 | 83 |
status_code = status.HTTP_401_UNAUTHORIZED |
56 | 84 |
default_code = 'invalid-signature' |
... | ... | |
125 | 153 |
user = self.resolve_user(request) |
126 | 154 |
self.logger.info('user authenticated with signature %s', user) |
127 | 155 |
return (user, None) |
156 | ||
157 | ||
158 |
class APIClientAuthenticationUnavailable(exceptions.APIException): |
|
159 |
status_code = status.HTTP_503_SERVICE_UNAVAILABLE |
|
160 |
default_code = 'IDP temporarily unavailable, try again later.' |
|
161 | ||
162 |
def __init__(self): |
|
163 |
self.detail = {'err': 1, 'err_desc': self.default_code} |
|
164 | ||
165 | ||
166 |
class APIClientAuthentication(authentication.BasicAuthentication): |
|
167 |
def authenticate_credentials(self, identifier, password, request=None): |
|
168 |
idp_services = list(getattr(settings, 'KNOWN_SERVICES', {}).get('authentic', {}).values()) |
|
169 |
if not idp_services: |
|
170 |
return None |
|
171 |
authentic = idp_services[0] |
|
172 |
url = authentic['url'] + 'api/check-api-client/' |
|
173 | ||
174 |
try: |
|
175 |
response = Requests().post(url, json={'identifier': identifier, 'password': password}) |
|
176 |
except requests.Timeout: |
|
177 |
raise APIClientAuthenticationUnavailable() |
|
178 |
except requests.RequestException as err: |
|
179 |
raise APIClientAuthenticationUnavailable() |
|
180 | ||
181 |
try: |
|
182 |
response.raise_for_status() |
|
183 |
except requests.exceptions.RequestException: |
|
184 |
return None |
|
185 | ||
186 |
result = response.json() |
|
187 |
if 'err' not in result or 'data' not in result or result['err'] == 1: |
|
188 |
return None |
|
189 |
try: |
|
190 |
api_client = APIClientUser.from_dict(result['data']) |
|
191 |
except Exception: |
|
192 |
return None |
|
193 |
return api_client, None |
hobo/test_urls.py | ||
---|---|---|
3 | 3 |
from django.conf.urls import url |
4 | 4 |
from django.core.exceptions import PermissionDenied |
5 | 5 |
from django.http import HttpResponse |
6 |
from rest_framework import permissions |
|
7 |
from rest_framework.response import Response |
|
8 |
from rest_framework.views import APIView |
|
6 | 9 | |
7 | 10 | |
8 | 11 |
def helloworld(request): |
... | ... | |
16 | 19 |
return HttpResponse('Hello world %s' % request.META['REMOTE_ADDR']) |
17 | 20 | |
18 | 21 | |
22 |
class AuthenticatedTestView(APIView): |
|
23 | ||
24 |
permission_classes = [permissions.IsAuthenticated] |
|
25 | ||
26 |
def get(self, request): |
|
27 |
return Response({'some': 'data'}) |
|
28 | ||
29 | ||
19 | 30 |
urlpatterns = [ |
20 | 31 |
url(r'^$', helloworld), |
32 |
url(r'^authenticated-testview/', AuthenticatedTestView.as_view()), |
|
21 | 33 |
] |
tests/settings.py | ||
---|---|---|
26 | 26 |
} |
27 | 27 | |
28 | 28 |
TEMPLATES[0]['OPTIONS'].setdefault('builtins', []).append('hobo.templatetags.hobo') |
29 | ||
30 |
REST_FRAMEWORK = { |
|
31 |
'DEFAULT_AUTHENTICATION_CLASSES': ( |
|
32 |
'hobo.rest_authentication.PublikAuthentication', |
|
33 |
'hobo.rest_authentication.APIClientAuthentication', |
|
34 |
), |
|
35 |
'DEFAULT_RENDERER_CLASSES': ('rest_framework.renderers.JSONRenderer',), |
|
36 |
} |
tests/test_rest_authentication.py | ||
---|---|---|
1 |
import base64 |
|
2 | ||
3 |
import pytest |
|
4 |
import requests |
|
5 |
import responses |
|
6 |
from django.test import RequestFactory |
|
7 | ||
8 | ||
9 |
@pytest.fixture |
|
10 |
def settings_with_idp(settings): |
|
11 |
settings.ROOT_URLCONF = 'hobo.test_urls' |
|
12 |
settings.KNOWN_SERVICES = { |
|
13 |
'authentic': { |
|
14 |
'idp': { |
|
15 |
'title': 'Foobar', |
|
16 |
'url': 'https://idp.example.invalid/', |
|
17 |
'orig': 'example.org', |
|
18 |
'secret': 'xxx', |
|
19 |
} |
|
20 |
} |
|
21 |
} |
|
22 |
return settings |
|
23 | ||
24 | ||
25 |
@pytest.fixture |
|
26 |
def app_with_auth(app): |
|
27 |
app.authorization = ('Basic', ('foo', 'bar')) |
|
28 |
return app |
|
29 | ||
30 | ||
31 |
def test_no_known_services(app_with_auth, db, settings): |
|
32 |
settings.ROOT_URLCONF = 'hobo.test_urls' |
|
33 |
resp = app_with_auth.get('/authenticated-testview/', status=403) |
|
34 | ||
35 | ||
36 |
def test_no_idp_in_known_services(app_with_auth, db, settings): |
|
37 |
settings.ROOT_URLCONF = 'hobo.test_urls' |
|
38 |
settings.KNOWN_SERVICES = {} |
|
39 |
resp = app_with_auth.get('/authenticated-testview/', status=403) |
|
40 | ||
41 | ||
42 |
def test_idp_connection_error(app_with_auth, db, settings_with_idp): |
|
43 |
with responses.RequestsMock() as rsps: |
|
44 |
rsps.post('https://idp.example.invalid/api/check-api-client/', status=403) |
|
45 |
resp = app_with_auth.get('/authenticated-testview/', status=403) |
|
46 | ||
47 | ||
48 |
def test_idp_timeout(app_with_auth, db, settings_with_idp): |
|
49 |
with responses.RequestsMock() as rsps: |
|
50 |
rsps.post('https://idp.example.invalid/api/check-api-client/', body=requests.Timeout('...')) |
|
51 |
resp = app_with_auth.get('/authenticated-testview/', status=503) |
|
52 |
assert resp.json == {'err': 1, 'err_desc': 'IDP temporarily unavailable, try again later.'} |
|
53 | ||
54 | ||
55 |
def test_idp_connection_error(app_with_auth, db, settings_with_idp): |
|
56 |
with responses.RequestsMock() as rsps: |
|
57 |
rsps.post('https://idp.example.invalid/api/check-api-client/', body=requests.RequestException('...')) |
|
58 |
resp = app_with_auth.get('/authenticated-testview/', status=503) |
|
59 |
assert resp.json == {'err': 1, 'err_desc': 'IDP temporarily unavailable, try again later.'} |
|
60 | ||
61 | ||
62 |
def test_idp_no_err_key(app_with_auth, db, settings_with_idp): |
|
63 |
with responses.RequestsMock() as rsps: |
|
64 |
rsps.post( |
|
65 |
'https://idp.example.invalid/api/check-api-client/', |
|
66 |
json={'foo': 'bar'}, |
|
67 |
status=200, |
|
68 |
) |
|
69 |
resp = app_with_auth.get('/authenticated-testview/', status=403) |
|
70 | ||
71 | ||
72 |
def test_idp_app_error(app_with_auth, db, settings_with_idp): |
|
73 |
with responses.RequestsMock() as rsps: |
|
74 |
rsps.post( |
|
75 |
'https://idp.example.invalid/api/check-api-client/', |
|
76 |
json={'err': 1}, |
|
77 |
status=200, |
|
78 |
) |
|
79 |
resp = app_with_auth.get('/authenticated-testview/', status=403) |
|
80 | ||
81 | ||
82 |
def test_idp_wrong_serialization(app_with_auth, db, settings_with_idp): |
|
83 |
with responses.RequestsMock() as rsps: |
|
84 |
rsps.post( |
|
85 |
'https://idp.example.invalid/api/check-api-client/', |
|
86 |
json={'err': 0, 'data': {'foo': 'bar'}}, |
|
87 |
status=200, |
|
88 |
) |
|
89 |
resp = app_with_auth.get('/authenticated-testview/', status=403) |
|
90 | ||
91 | ||
92 |
def test_no_credentials(app, db, settings_with_idp): |
|
93 |
# test that the '/authenticated-testview/' really requires authentication, |
|
94 |
# otherwise all the others tests are meaningless. |
|
95 |
resp = app.get('/authenticated-testview/', status=403) |
|
96 | ||
97 | ||
98 |
def test_access_granted(app_with_auth, db, settings_with_idp): |
|
99 |
with responses.RequestsMock() as rsps: |
|
100 |
rsps.post( |
|
101 |
'https://idp.example.invalid/api/check-api-client/', |
|
102 |
json={ |
|
103 |
'err': 0, |
|
104 |
'data': { |
|
105 |
'is_active': True, |
|
106 |
'is_anonymous': False, |
|
107 |
'is_authenticated': True, |
|
108 |
'is_superuser': False, |
|
109 |
'restrict_to_anonymised_data': False, |
|
110 |
'roles': [], |
|
111 |
}, |
|
112 |
}, |
|
113 |
status=200, |
|
114 |
) |
|
115 |
resp = app_with_auth.get('/authenticated-testview/') |
tox.ini | ||
---|---|---|
57 | 57 |
mock<4 |
58 | 58 |
httmock |
59 | 59 |
requests |
60 |
responses |
|
60 | 61 |
pytest-freezegun |
61 | 62 |
xmlschema<1.1 |
62 | 63 |
enum34<=1.1.6 |
63 |
- |