0003-drop-and-rename-issuer-field-56819.patch
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 |
- |