Projet

Général

Profil

0003-drop-and-rename-issuer-field-56819.patch

Benjamin Dauvergne, 14 septembre 2021 22:59

Télécharger (13,3 ko)

Voir les différences:

Subject: [PATCH 3/3] drop and rename issuer field (#56819)

 mellon/adapters.py                           | 18 +++++-----
 mellon/migrations/0005_drop_rename_issuer.py | 30 ++++++++++++++++
 mellon/models.py                             |  5 +--
 mellon/models_utils.py                       | 30 ++++++++++++++++
 mellon/utils.py                              |  2 +-
 mellon/views.py                              | 19 +++++-----
 tests/test_default_adapter.py                |  4 ++-
 tests/test_utils.py                          | 37 ++++++++++++++++++++
 8 files changed, 121 insertions(+), 24 deletions(-)
 create mode 100644 mellon/migrations/0005_drop_rename_issuer.py
 create mode 100644 mellon/models_utils.py
mellon/adapters.py
36 36
from django.utils.six.moves.urllib.parse import urlparse
37 37
from django.utils.translation import ugettext as _
38 38

  
39
from . import app_settings, models, utils
39
from . import app_settings, models, models_utils, utils
40 40

  
41 41
User = auth.get_user_model()
42 42

  
......
325 325
                return None
326 326
        else:
327 327
            name_id = saml_attributes['name_id_content']
328
        issuer = saml_attributes['issuer']
328
        entity_id = saml_attributes['issuer']
329 329
        try:
330 330
            saml_identifier = models.UserSAMLIdentifier.objects.select_related('user').get(
331
                name_id=name_id, issuer=issuer
331
                name_id=name_id, issuer=models_utils.get_issuer(entity_id)
332 332
            )
333 333
            user = saml_identifier.user
334 334
            user.saml_identifier = saml_identifier
335
            logger.info('mellon: looked up user %s with name_id %s from issuer %s', user, name_id, issuer)
335
            logger.info('mellon: looked up user %s with name_id %s from issuer %s', user, name_id, entity_id)
336 336
            return user
337 337
        except models.UserSAMLIdentifier.DoesNotExist:
338 338
            pass
......
347 347
            created = True
348 348
            user = self.create_user(User)
349 349

  
350
        nameid_user = self._link_user(idp, saml_attributes, issuer, name_id, user)
350
        nameid_user = self._link_user(idp, saml_attributes, entity_id, name_id, user)
351 351
        if user != nameid_user:
352 352
            logger.info(
353
                'mellon: looked up user %s with name_id %s from issuer %s', nameid_user, name_id, issuer
353
                'mellon: looked up user %s with name_id %s from issuer %s', nameid_user, name_id, entity_id
354 354
            )
355 355
            if created:
356 356
                user.delete()
......
363 363
                user.delete()
364 364
                return None
365 365
            logger.info(
366
                'mellon: created new user %s with name_id %s from issuer %s', nameid_user, name_id, issuer
366
                'mellon: created new user %s with name_id %s from issuer %s', nameid_user, name_id, entity_id
367 367
            )
368 368
        return nameid_user
369 369

  
......
455 455
            )
456 456
        return None
457 457

  
458
    def _link_user(self, idp, saml_attributes, issuer, name_id, user):
458
    def _link_user(self, idp, saml_attributes, entity_id, name_id, user):
459 459
        saml_id, created = models.UserSAMLIdentifier.objects.get_or_create(
460
            name_id=name_id, issuer=issuer, defaults={'user': user}
460
            name_id=name_id, issuer=models_utils.get_issuer(entity_id), defaults={'user': user}
461 461
        )
462 462
        if created:
463 463
            user.saml_identifier = saml_id
mellon/migrations/0005_drop_rename_issuer.py
1
# Generated by Django 2.2.19 on 2021-09-14 19:31
2

  
3
from django.db import migrations
4

  
5

  
6
class Migration(migrations.Migration):
7

  
8
    dependencies = [
9
        ('mellon', '0004_migrate_issuer'),
10
    ]
11

  
12
    operations = [
13
        migrations.AlterUniqueTogether(
14
            name='usersamlidentifier',
15
            unique_together={('issuer_fk', 'name_id')},
16
        ),
17
        migrations.RemoveField(
18
            model_name='usersamlidentifier',
19
            name='issuer',
20
        ),
21
        migrations.RenameField(
22
            model_name='usersamlidentifier',
23
            old_name='issuer_fk',
24
            new_name='issuer',
25
        ),
26
        migrations.AlterUniqueTogether(
27
            name='usersamlidentifier',
28
            unique_together={('issuer', 'name_id')},
29
        ),
30
    ]
mellon/models.py
28 28
        related_name='saml_identifiers',
29 29
        on_delete=models.CASCADE,
30 30
    )
31
    issuer = models.TextField(verbose_name=_('Issuer'), null=True)
32 31
    name_id = models.TextField(verbose_name=_('SAML identifier'))
33 32
    created = models.DateTimeField(verbose_name=_('created'), auto_now_add=True)
34
    issuer_fk = models.ForeignKey(
35
        'mellon.Issuer', verbose_name=_('Issuer'), null=True, on_delete=models.CASCADE
36
    )
33
    issuer = models.ForeignKey('mellon.Issuer', verbose_name=_('Issuer'), null=True, on_delete=models.CASCADE)
37 34

  
38 35
    class Meta:
39 36
        verbose_name = _('user SAML identifier')
mellon/models_utils.py
1
# django-mellon - SAML2 authentication for Django
2
# Copyright (C) 2014-2019 Entr'ouvert
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU Affero General Public License as
5
# published by the Free Software Foundation, either version 3 of the
6
# License, or (at your option) any later version.
7

  
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU Affero General Public License for more details.
12

  
13
# You should have received a copy of the GNU Affero General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

  
16
from . import models, utils
17

  
18

  
19
def get_issuer(entity_id):
20
    idp = utils.get_idp(entity_id)
21
    slug = idp.get('SLUG')
22
    if slug:
23
        issuer = models.Issuer.objects.filter(slug=slug).first()
24
        # migrate issuer entity_id based on the slug
25
        if issuer and issuer.entity_id != entity_id:
26
            issuer.entity_id = entity_id
27
            issuer.save()
28
    if not slug or not issuer:
29
        issuer, created = models.Issuer.objects.update_or_create(entity_id=entity_id, defaults={'slug': slug})
30
    return issuer
mellon/utils.py
212 212
    name_qualifier = lasso_name_id.nameQualifier and force_text(lasso_name_id.nameQualifier)
213 213
    sp_name_qualifier = lasso_name_id.spNameQualifier and force_text(lasso_name_id.spNameQualifier)
214 214
    for index in indexes:
215
        issuer = index.saml_identifier.issuer
215
        issuer = index.saml_identifier.issuer.entity_id
216 216
        session_infos.append(
217 217
            {
218 218
                'entity_id': issuer,
mellon/views.py
32 32
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect
33 33
from django.shortcuts import render, resolve_url
34 34
from django.urls import reverse
35
from django.utils import six
36 35
from django.utils.encoding import force_str, force_text
37 36
from django.utils.http import urlencode
38 37
from django.utils.translation import ugettext as _
......
41 40
from django.views.generic.base import RedirectView
42 41
from requests.exceptions import RequestException
43 42

  
44
from . import app_settings, models, utils
43
from . import app_settings, models, models_utils, utils
45 44

  
46 45
RETRY_LOGIN_COOKIE = 'MELLON_RETRY_LOGIN'
47 46

  
......
244 243
                    content = self.get_attribute_value(at, attribute_value)
245 244
                    if content is not None:
246 245
                        values.append(content)
247
        attributes['issuer'] = login.remoteProviderId
246
        entity_id = attributes['issuer'] = login.remoteProviderId
248 247
        in_response_to = login.response.inResponseTo
249 248
        if in_response_to:
250 249
            attributes['nonce'] = request.session.get('mellon-nonce-%s' % in_response_to)
......
280 279
        return response
281 280

  
282 281
    def authenticate(self, request, login, attributes):
283
        user = auth.authenticate(request=request, saml_attributes=attributes)
282
        user = auth.authenticate(
283
            request=request, issuer=models_utils.get_issuer(attributes['issuer']), saml_attributes=attributes
284
        )
284 285
        next_url = self.get_next_url(default=resolve_url(settings.LOGIN_REDIRECT_URL))
285 286
        if user is not None:
286 287
            if user.is_active:
......
598 599
    def post(self, request, *args, **kwargs):
599 600
        return self.idp_logout(request, force_str(request.body), 'soap')
600 601

  
601
    def logout(self, request, issuer, saml_user, session_indexes, indexes, mode):
602
    def logout(self, request, saml_user, session_indexes, indexes, mode):
602 603
        session_keys = set(indexes.values_list('session_key', flat=True))
603 604
        indexes.delete()
604 605

  
......
647 648
        except lasso.Error as e:
648 649
            return HttpResponseBadRequest('error processing logout request: %r' % e)
649 650

  
650
        issuer = force_text(logout.remoteProviderId)
651
        entity_id = force_text(logout.remoteProviderId)
651 652
        session_indexes = {force_text(sessionIndex) for sessionIndex in logout.request.sessionIndexes}
652 653

  
653 654
        saml_identifier = (
654 655
            models.UserSAMLIdentifier.objects.filter(
655
                name_id=force_text(logout.nameIdentifier.content), issuer=issuer
656
                name_id=force_text(logout.nameIdentifier.content),
657
                issuer=models_utils.get_issuer(entity_id),
656 658
            )
657
            .select_related('user')
659
            .select_related('user', 'issuer')
658 660
            .first()
659 661
        )
660 662

  
......
680 682
                    self.log.info('full logout requested, no sessionIndexes')
681 683
                self.logout(
682 684
                    request,
683
                    issuer=issuer,
684 685
                    saml_user=name_id_user,
685 686
                    session_indexes=session_indexes,
686 687
                    indexes=indexes,
tests/test_default_adapter.py
92 92
    assert User.objects.count() == 0
93 93

  
94 94

  
95
def test_lookup_user_transaction(transactional_db, concurrency, idp, saml_attributes):
95
def test_lookup_user_transaction(transactional_db, concurrency, idp, saml_attributes, settings):
96 96
    adapter = DefaultAdapter()
97 97
    p = ThreadPool(concurrency)
98 98

  
99
    settings.MELLON_IDENTITY_PROVIDERS = [idp]
100

  
99 101
    if connection.vendor == 'postgresql':
100 102
        with connection.cursor() as c:
101 103
            c.execute('SHOW max_connections')
tests/test_utils.py
20 20
import lasso
21 21
from xml_utils import assert_xml_constraints
22 22

  
23
from mellon.models import Issuer
24
from mellon.models_utils import get_issuer
23 25
from mellon.utils import create_metadata, flatten_datetime, iso8601_to_datetime
24 26
from mellon.views import check_next_url
25 27

  
......
199 201
    assert not check_next_url(rf.get('/'), 'https://example.invalid/')
200 202
    # default hostname is testserver
201 203
    assert check_next_url(rf.get('/'), 'http://testserver/ok/')
204

  
205

  
206
def test_get_issuer_entity_id_migration(db, settings, metadata):
207
    entity_id1 = 'http://idp5/metadata'
208
    entity_id2 = 'http://idp6/metadata'
209
    settings.MELLON_IDENTITY_PROVIDERS = [
210
        {
211
            'METADATA': metadata,
212
        },
213
    ]
214
    issuer1 = get_issuer(entity_id1)
215
    assert issuer1.entity_id == entity_id1
216
    assert issuer1.slug is None
217

  
218
    settings.MELLON_IDENTITY_PROVIDERS = [
219
        {
220
            'METADATA': metadata,
221
            'SLUG': 'idp',
222
        },
223
    ]
224
    issuer2 = get_issuer(entity_id1)
225
    assert issuer2.id == issuer1.id
226
    assert issuer2.entity_id == entity_id1
227
    assert issuer2.slug == 'idp'
228

  
229
    settings.MELLON_IDENTITY_PROVIDERS = [
230
        {
231
            'METADATA': metadata.replace(entity_id1, entity_id2),
232
            'SLUG': 'idp',
233
        },
234
    ]
235
    issuer3 = get_issuer(entity_id2)
236
    assert issuer3.id == issuer1.id
237
    assert issuer3.entity_id == entity_id2
238
    assert issuer3.slug == 'idp'
202
-