0003-provisionning-add-synchronous-provisionning-to-DRF-a.patch
hobo/provisionning/utils.py | ||
---|---|---|
16 | 16 | |
17 | 17 |
import hashlib |
18 | 18 |
import logging |
19 |
from urllib.parse import quote, urljoin |
|
19 | 20 | |
21 |
import requests |
|
22 |
from django.conf import settings |
|
20 | 23 |
from django.contrib.auth import get_user_model |
21 | 24 |
from django.contrib.auth.models import Group |
22 | 25 |
from django.db import IntegrityError |
... | ... | |
24 | 27 |
from django.db.transaction import atomic |
25 | 28 |
from mellon.models import Issuer |
26 | 29 | |
30 |
from hobo import signature |
|
27 | 31 |
from hobo.agent.common.models import Role |
28 | 32 |
from hobo.multitenant.utils import provision_user_groups |
29 | 33 | |
... | ... | |
52 | 56 |
return s |
53 | 57 | |
54 | 58 | |
59 |
def get_idp_service(): |
|
60 |
idp_services = list((settings.KNOWN_SERVICES or {}).get('authentic', {}).values()) |
|
61 |
return (idp_services or [None])[0] |
|
62 | ||
63 | ||
55 | 64 |
def get_issuer(entity_id): |
56 | 65 |
issuer, created = Issuer.objects.get_or_create(entity_id=entity_id) |
57 | 66 |
return issuer |
... | ... | |
259 | 268 |
except TryAgain: |
260 | 269 |
continue |
261 | 270 |
break |
271 | ||
272 | ||
273 |
def get_user_from_name_id(name_id, entity_id=None, raise_on_missing=False): |
|
274 |
User = get_user_model() |
|
275 |
try: |
|
276 |
user = User.objects.get( |
|
277 |
saml_identifiers__name_id=name_id, saml_identifiers__issuer__entity_id=entity_id |
|
278 |
) |
|
279 |
except User.DoesNotExist: |
|
280 |
try: |
|
281 |
user = User.objects.get(Q(username=name_id[:30]) | Q(username=name_id[:150])) |
|
282 |
except User.DoesNotExist: |
|
283 |
user = None |
|
284 |
if not user: |
|
285 |
if raise_on_missing: |
|
286 |
raise User.DoesNotExist |
|
287 |
return user |
|
288 | ||
289 | ||
290 |
def get_or_create_user_from_name_id(name_id, raise_on_missing=False): |
|
291 |
User = get_user_model() |
|
292 | ||
293 |
user = get_user_from_name_id(name_id=name_id) |
|
294 |
if user: |
|
295 |
return user |
|
296 | ||
297 |
idp_service = get_idp_service() |
|
298 |
if not idp_service: |
|
299 |
if raise_on_missing: |
|
300 |
raise User.DoesNotExist |
|
301 |
return None |
|
302 | ||
303 |
entity_id = idp_service['saml-idp-metadata-url'] |
|
304 |
issuer = get_issuer(entity_id) |
|
305 |
users_api_url = urljoin(idp_service['url'], 'api/users/%s/' % quote(name_id, safe='')) |
|
306 |
try: |
|
307 |
response = requests.get(signature.sign_url(users_api_url, idp_service['secret']), timeout=5) |
|
308 |
response.raise_for_status() |
|
309 |
except requests.RequestException: |
|
310 |
if raise_on_missing: |
|
311 |
raise User.DoesNotExist('Failed to reach IdP') |
|
312 |
return None |
|
313 |
return provision_user(issuer.entity_id, o=response.json()) |
hobo/rest_authentication.py | ||
---|---|---|
8 | 8 |
from rest_framework import authentication, exceptions, status |
9 | 9 | |
10 | 10 |
from hobo import signature |
11 |
from hobo.provisionning.utils import get_or_create_user_from_name_id |
|
11 | 12 | |
12 | 13 |
try: |
13 | 14 |
from mellon.models import UserSAMLIdentifier |
... | ... | |
82 | 83 | |
83 | 84 |
elif UserSAMLIdentifier: |
84 | 85 |
try: |
85 |
return UserSAMLIdentifier.objects.get(name_id=name_id).user
|
|
86 |
except UserSAMLIdentifier.DoesNotExist:
|
|
86 |
return get_or_create_user_from_name_id(name_id, raise_on_missing=True)
|
|
87 |
except User.DoesNotExist: |
|
87 | 88 |
raise PublikAuthenticationFailed('user-not-found') |
88 | 89 |
else: |
89 | 90 |
raise PublikAuthenticationFailed('no-usable-model') |
tests_multitenant/conftest.py | ||
---|---|---|
1 | 1 |
import pytest |
2 |
from tenant_schemas.utils import tenant_context |
|
2 | 3 | |
3 | 4 | |
4 | 5 |
@pytest.fixture |
... | ... | |
68 | 69 |
'service-id': 'authentic', |
69 | 70 |
'base_url': 'http://other.example.net', |
70 | 71 |
'legacy_urls': [{'base_url': 'http://olda2.example.net'}], |
72 |
'saml-idp-metadata-url': 'https://other.example.net/idp/saml2/metadata', |
|
71 | 73 |
}, |
72 | 74 |
{ |
73 | 75 |
'slug': 'another', |
... | ... | |
82 | 84 |
'service-id': 'combo', |
83 | 85 |
'template_name': '...portal-user...', |
84 | 86 |
'base_url': 'http://portal-user.example.net', |
87 |
'secret_key': 'abcdefg', |
|
85 | 88 |
}, |
86 | 89 |
], |
87 | 90 |
}, |
... | ... | |
116 | 119 | |
117 | 120 |
@pytest.fixture |
118 | 121 |
def tenant(make_tenant): |
119 |
return make_tenant('tenant.example.net') |
|
122 |
tenant = make_tenant('tenant.example.net') |
|
123 | ||
124 |
with tenant_context(tenant): |
|
125 |
yield tenant |
|
120 | 126 | |
121 | 127 | |
122 | 128 |
@pytest.fixture |
tests_multitenant/test_provisionning.py | ||
---|---|---|
1 |
from hobo.provisionning.utils import NotificationProcessing |
|
1 |
import json |
|
2 | ||
3 |
import pytest |
|
4 |
import requests |
|
5 |
from httmock import HTTMock, urlmatch |
|
6 |
from mellon.models import Issuer |
|
7 |
from rest_framework import permissions |
|
8 |
from rest_framework.response import Response |
|
9 |
from rest_framework.views import APIView |
|
10 | ||
11 |
from hobo import rest_authentication, signature |
|
12 |
from hobo.provisionning.utils import ( |
|
13 |
NotificationProcessing, |
|
14 |
get_or_create_user_from_name_id, |
|
15 |
get_user_from_name_id, |
|
16 |
) |
|
2 | 17 | |
3 | 18 | |
4 | 19 |
def test_truncate_role_name(): |
... | ... | |
20 | 35 |
assert len(truncated) == max_length |
21 | 36 |
assert truncated not in seen |
22 | 37 |
seen.add(truncated) |
38 | ||
39 | ||
40 |
@urlmatch() |
|
41 |
def request_exception(url, request): |
|
42 |
raise requests.ConnectionError |
|
43 | ||
44 | ||
45 |
NAME_ID = '1234' * 8 |
|
46 | ||
47 | ||
48 |
@urlmatch(path='/api/users/') |
|
49 |
def user_payload(url, request): |
|
50 |
return { |
|
51 |
'status_code': 200, |
|
52 |
'content': json.dumps( |
|
53 |
{ |
|
54 |
'uuid': NAME_ID, |
|
55 |
'first_name': 'John', |
|
56 |
'last_name': 'Doe', |
|
57 |
'email': 'john.doe@example.net', |
|
58 |
'is_superuser': False, |
|
59 |
'is_active': True, |
|
60 |
} |
|
61 |
), |
|
62 |
} |
|
63 | ||
64 | ||
65 |
def test_get_or_create_user_from_name_id(tenant, django_user_model): |
|
66 |
Issuer.objects.create(entity_id='https://idp.examle.net/idp/saml2/metadata') |
|
67 | ||
68 |
assert get_user_from_name_id(NAME_ID) is None |
|
69 |
with pytest.raises(django_user_model.DoesNotExist): |
|
70 |
get_user_from_name_id(NAME_ID, raise_on_missing=True) |
|
71 | ||
72 |
with HTTMock(request_exception): |
|
73 |
assert get_or_create_user_from_name_id(NAME_ID) is None |
|
74 |
with pytest.raises(django_user_model.DoesNotExist): |
|
75 |
get_or_create_user_from_name_id(NAME_ID, raise_on_missing=True) |
|
76 | ||
77 |
with HTTMock(user_payload): |
|
78 |
user = get_or_create_user_from_name_id(NAME_ID) |
|
79 |
assert user is not None |
|
80 |
assert user.first_name == 'John' |
|
81 |
assert user.username == NAME_ID |
|
82 |
assert user.saml_identifiers.get().name_id == NAME_ID |
|
83 |
assert django_user_model.objects.count() == 1 |
|
84 | ||
85 | ||
86 |
class DummyAPIView(APIView): |
|
87 |
authentication_classes = (rest_authentication.PublikAuthentication,) |
|
88 |
permission_classes = (permissions.IsAuthenticated,) |
|
89 | ||
90 |
def get(self, request, format=None): |
|
91 |
return Response({'err': 0}) |
|
92 | ||
93 | ||
94 |
def test_rest_authentication_provisionning_ok(tenant, settings, rf, django_user_model): |
|
95 |
key = settings.KNOWN_SERVICES['combo']['another2']['secret'] |
|
96 |
request = rf.get(signature.sign_url('/api/misc/?param1=2&NameID=%s&orig=portal-user.example.net', key)) |
|
97 | ||
98 |
view = DummyAPIView.as_view() |
|
99 | ||
100 |
with HTTMock(user_payload): |
|
101 |
response = view(request) |
|
102 |
assert response.status_code == 200 |
|
103 |
assert response.data == {'err': 0} |
|
104 |
user = django_user_model.objects.get() |
|
105 |
assert user.first_name == 'John' |
|
106 |
assert user.username == NAME_ID |
|
107 |
assert user.saml_identifiers.get().name_id == NAME_ID |
|
108 | ||
109 | ||
110 |
def test_rest_authentication_provisionning_nok(tenant, settings, rf): |
|
111 |
key = settings.KNOWN_SERVICES['combo']['another2']['secret'] |
|
112 |
request = rf.get(signature.sign_url('/api/misc/?param1=2&NameID=%s&orig=portal-user.example.net', key)) |
|
113 | ||
114 |
view = DummyAPIView.as_view() |
|
115 | ||
116 |
response = view(request) |
|
117 |
assert response.status_code == 401 |
|
118 |
assert response.data == {'err': 1, 'err_desc': 'user-not-found'} |
tests_multitenant/test_uwsgidecorators.py | ||
---|---|---|
44 | 44 | |
45 | 45 | |
46 | 46 |
def test_mocked_uwsgi_tenant(uwsgi, tenant): |
47 |
from tenant_schemas.utils import tenant_context |
|
48 | ||
49 | 47 |
@hobo.multitenant.uwsgidecorators.spool |
50 | 48 |
def function(a, b): |
51 | 49 |
pass |
52 | 50 | |
53 |
with tenant_context(tenant): |
|
54 |
function.spool(1, 2) |
|
51 |
function.spool(1, 2) |
|
55 | 52 | |
56 | 53 |
assert set(uwsgi.spool.call_args[1].keys()) == {'body', 'tenant', 'name'} |
57 | 54 |
assert pickle.loads(uwsgi.spool.call_args[1]['body']) == {'args': (1, 2), 'kwargs': {}} |
58 |
- |