Projet

Général

Profil

0003-provisionning-add-synchronous-provisionning-to-DRF-a.patch

Benjamin Dauvergne, 30 juin 2022 23:24

Télécharger (9,89 ko)

Voir les différences:

Subject: [PATCH 3/3] provisionning: add synchronous provisionning to DRF
 authentication (#59135)

 hobo/provisionning/utils.py               | 52 ++++++++++++
 hobo/rest_authentication.py               |  5 +-
 tests_multitenant/conftest.py             |  8 +-
 tests_multitenant/test_provisionning.py   | 98 ++++++++++++++++++++++-
 tests_multitenant/test_uwsgidecorators.py |  5 +-
 5 files changed, 160 insertions(+), 8 deletions(-)
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
-