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 APIClientAuthentication(authentication.BasicAuthentication): |
|
159 |
def authenticate_credentials(self, identifier, password, request=None): |
|
160 |
idp_services = list(getattr(settings, 'KNOWN_SERVICES', {}).get('authentic', {}).values()) |
|
161 |
if not idp_services: |
|
162 |
return None |
|
163 |
authentic = idp_services[0] |
|
164 |
url = authentic['url'] + 'api/check-api-client/' |
|
165 |
response = Requests().post(url, json={'identifier': identifier, 'password': password}) |
|
166 |
try: |
|
167 |
response.raise_for_status() |
|
168 |
except requests.exceptions.RequestException: |
|
169 |
return None |
|
170 | ||
171 |
result = response.json() |
|
172 |
if 'err' not in result or 'data' not in result or result['err'] == 1: |
|
173 |
return None |
|
174 |
try: |
|
175 |
api_client = APIClientUser.from_dict(result['data']) |
|
176 |
except Exception: |
|
177 |
return None |
|
178 |
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 responses |
|
4 |
from django.test import RequestFactory |
|
5 |
from rest_framework.views import APIView |
|
6 | ||
7 | ||
8 |
class TestView(APIView): |
|
9 |
def get(self, request): |
|
10 |
return Response({'some': 'data'}) |
|
11 | ||
12 | ||
13 |
@responses.activate |
|
14 |
def test_apiclient_authentication(app, db, settings): |
|
15 |
settings.ROOT_URLCONF = 'hobo.test_urls' |
|
16 |
app.authorization = ('Basic', ('foo', 'bar')) |
|
17 |
# No KNOWN_SERVICES : 403 |
|
18 |
resp = app.get('/authenticated-testview/', status=403) |
|
19 |
# No authentic in KNOWN_SERVICES : 403 |
|
20 |
settings.KNOWN_SERVICES = {} |
|
21 |
resp = app.get('/authenticated-testview/', status=403) |
|
22 | ||
23 |
settings.KNOWN_SERVICES = { |
|
24 |
'authentic': { |
|
25 |
'idp': { |
|
26 |
'title': 'Foobar', |
|
27 |
'url': 'https://idp.example.invalid/', |
|
28 |
'orig': 'example.org', |
|
29 |
'secret': 'xxx', |
|
30 |
} |
|
31 |
} |
|
32 |
} |
|
33 |
# check-api-client returns an error : 403 |
|
34 |
responses.add( |
|
35 |
responses.POST, |
|
36 |
'https://idp.example.invalid/api/check-api-client/', |
|
37 |
status=403, |
|
38 |
) |
|
39 |
resp = app.get('/authenticated-testview/', status=403) |
|
40 | ||
41 |
# no 'err' key in check-api-client response : 403 |
|
42 |
responses.add( |
|
43 |
responses.POST, |
|
44 |
'https://idp.example.invalid/api/check-api-client/', |
|
45 |
json={'foo': 'bar'}, |
|
46 |
status=200, |
|
47 |
) |
|
48 |
resp = app.get('/authenticated-testview/', status=403) |
|
49 | ||
50 |
# authentication refused by a2 : 403 |
|
51 |
responses.add( |
|
52 |
responses.POST, |
|
53 |
'https://idp.example.invalid/api/check-api-client/', |
|
54 |
json={'err': 1}, |
|
55 |
status=200, |
|
56 |
) |
|
57 |
resp = app.get('/authenticated-testview/', status=403) |
|
58 | ||
59 |
# error when deserializing the api client : 403 |
|
60 |
responses.add( |
|
61 |
responses.POST, |
|
62 |
'https://idp.example.invalid/api/check-api-client/', |
|
63 |
json={'err': 0, 'data': {'foo': 'bar'}}, |
|
64 |
status=200, |
|
65 |
) |
|
66 |
resp = app.get('/authenticated-testview/', status=403) |
|
67 | ||
68 |
# accces Granted |
|
69 |
responses.add( |
|
70 |
responses.POST, |
|
71 |
'https://idp.example.invalid/api/check-api-client/', |
|
72 |
json={ |
|
73 |
'err': 0, |
|
74 |
'data': { |
|
75 |
'is_active': True, |
|
76 |
'is_anonymous': False, |
|
77 |
'is_authenticated': True, |
|
78 |
'is_superuser': False, |
|
79 |
'restrict_to_anonymised_data': False, |
|
80 |
'roles': [], |
|
81 |
}, |
|
82 |
}, |
|
83 |
status=200, |
|
84 |
) |
|
85 |
resp = app.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 |
- |