Skip to content

Commit 8082bd8

Browse files
committed
Merge branch 'knaperek-master' into user-fetching-customization
2 parents 4763645 + 521089e commit 8082bd8

File tree

8 files changed

+133
-20
lines changed

8 files changed

+133
-20
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Thanks to plumdog
1919

2020
Thanks to plumdog
2121

22+
UNRELEASED
23+
----------
24+
- Allowed creating Users with multiple required fields.
25+
2226
0.17.1 (2018-07-16)
2327
----------
2428
- A 403 (permission denied) is now raised if a SAMLResponse is replayed, instead of 500.

djangosaml2/backends.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,8 @@ def get_or_create_user(self, user_lookup_key, user_lookup_value, create_unknown_
138138
except UserModel.DoesNotExist:
139139
# Create new one if desired by settings
140140
if create_unknown_user:
141-
try:
142-
user, created = UserModel.objects.get_or_create(**user_query_args, defaults={user_lookup_key: user_lookup_value})
143-
except Exception as e:
144-
logger.error('Could not create new user: %s', e)
145-
141+
user = UserModel(**user_query_args)
142+
created = True
146143
if created:
147144
logger.debug('New user created: %s', user)
148145
else:

djangosaml2/tests/__init__.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import datetime
1919
import re
2020
import sys
21-
from unittest import skip
21+
from unittest import mock, skip
2222

2323
from django.conf import settings
2424
from django.contrib.auth import SESSION_KEY, get_user_model
@@ -27,15 +27,16 @@
2727
from django.template import Context, Template
2828
from django.test import TestCase
2929
from django.test.client import RequestFactory
30+
from saml2.config import SPConfig
31+
from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
32+
3033
from djangosaml2 import views
3134
from djangosaml2.cache import OutstandingQueriesCache
3235
from djangosaml2.conf import get_config
3336
from djangosaml2.signals import post_authenticated
3437
from djangosaml2.tests import conf
3538
from djangosaml2.tests.auth_response import auth_response
3639
from djangosaml2.views import finish_logout
37-
from saml2.config import SPConfig
38-
from saml2.s_utils import decode_base64_and_inflate, deflate_and_base64_encode
3940

4041
try:
4142
from django.urls import reverse
@@ -519,6 +520,43 @@ def test_idplist_templatetag(self):
519520

520521
self.assertEqual(rendered, expected)
521522

523+
def test_sigalg_not_passed_when_not_signing_request(self):
524+
# monkey patch SAML configuration
525+
settings.SAML_CONFIG = conf.create_conf(
526+
sp_host='sp.example.com',
527+
idp_hosts=['idp.example.com'],
528+
metadata_file='remote_metadata_one_idp.xml',
529+
)
530+
531+
with mock.patch(
532+
'djangosaml2.views.Saml2Client.prepare_for_authenticate',
533+
return_value=('session_id', {'url': 'fake'}),
534+
535+
) as prepare_for_auth_mock:
536+
self.client.get(reverse('saml2_login'))
537+
prepare_for_auth_mock.assert_called_once()
538+
_args, kwargs = prepare_for_auth_mock.call_args
539+
self.assertNotIn('sigalg', kwargs)
540+
541+
def test_sigalg_passed_when_signing_request(self):
542+
# monkey patch SAML configuration
543+
settings.SAML_CONFIG = conf.create_conf(
544+
sp_host='sp.example.com',
545+
idp_hosts=['idp.example.com'],
546+
metadata_file='remote_metadata_one_idp.xml',
547+
)
548+
549+
settings.SAML_CONFIG['service']['sp']['authn_requests_signed'] = True
550+
with mock.patch(
551+
'djangosaml2.views.Saml2Client.prepare_for_authenticate',
552+
return_value=('session_id', {'url': 'fake'}),
553+
554+
) as prepare_for_auth_mock:
555+
self.client.get(reverse('saml2_login'))
556+
prepare_for_auth_mock.assert_called_once()
557+
_args, kwargs = prepare_for_auth_mock.call_args
558+
self.assertIn('sigalg', kwargs)
559+
522560

523561
def test_config_loader(request):
524562
config = SPConfig()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
X500ATTR_OID = 'urn:oid:2.5.4.'
2+
PKCS_9 = 'urn:oid:1.2.840.113549.1.9.1.'
3+
UCL_DIR_PILOT = 'urn:oid:0.9.2342.19200300.100.1.'
4+
5+
MAP = {
6+
'identifier': 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
7+
'fro': {
8+
X500ATTR_OID+'3': 'first_name', # cn
9+
X500ATTR_OID+'4': 'last_name', # sn
10+
PKCS_9+'1': 'email',
11+
UCL_DIR_PILOT+'1': 'uid',
12+
},
13+
'to': {
14+
'first_name': X500ATTR_OID+'3',
15+
'last_name': X500ATTR_OID+'4',
16+
'email' : PKCS_9+'1',
17+
'uid': UCL_DIR_PILOT+'1',
18+
}
19+
}

djangosaml2/views.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,20 @@
2929
from django.utils.http import is_safe_url
3030
from django.views.decorators.csrf import csrf_exempt
3131
from django.views.decorators.http import require_POST
32-
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
32+
from saml2 import BINDING_HTTP_REDIRECT, BINDING_HTTP_POST
33+
from saml2.client_base import LogoutError
34+
from saml2.metadata import entity_descriptor
3335
from saml2.ident import code, decode
3436
from saml2.metadata import entity_descriptor
3537
from saml2.response import (SignatureError, StatusAuthnFailed, StatusError,
3638
StatusNoAuthnContext, StatusRequestDenied,
3739
UnsolicitedResponse)
3840
from saml2.s_utils import UnsupportedBinding
41+
from saml2.response import (
42+
StatusError, StatusAuthnFailed, SignatureError, StatusRequestDenied,
43+
UnsolicitedResponse, StatusNoAuthnContext,
44+
)
45+
from saml2.mdstore import SourceNotFound
3946
from saml2.sigver import MissingKey
4047
from saml2.validate import ResponseLifetimeExceed, ToEarly
4148
from saml2.xmldsig import ( # support for SHA1 is required by spec
@@ -122,7 +129,15 @@ def login(request,
122129
})
123130

124131
selected_idp = request.GET.get('idp', None)
125-
conf = get_config(config_loader_path, request)
132+
try:
133+
conf = get_config(config_loader_path, request)
134+
except SourceNotFound as excp:
135+
msg = ('Error, IdP EntityID was not found '
136+
'in metadata: {}')
137+
logger.exception(msg.format(excp))
138+
return HttpResponse(msg.format(('Please contact '
139+
'technical support.')),
140+
status=500)
126141

127142
kwargs = {}
128143
# pysaml needs a string otherwise: "cannot serialize True (type bool)"
@@ -176,17 +191,18 @@ def login(request,
176191
logger.debug('Redirecting user to the IdP via %s binding.', binding)
177192
if binding == BINDING_HTTP_REDIRECT:
178193
try:
179-
# do not sign the xml itself, instead use the sigalg to
180-
# generate the signature as a URL param
181-
sig_alg_option_map = {'sha1': SIG_RSA_SHA1,
182-
'sha256': SIG_RSA_SHA256}
183-
sig_alg_option = getattr(conf, '_sp_authn_requests_signed_alg', 'sha1')
184-
sigalg = sig_alg_option_map[sig_alg_option] if sign_requests else None
185194
nsprefix = get_namespace_prefixes()
195+
if sign_requests:
196+
# do not sign the xml itself, instead use the sigalg to
197+
# generate the signature as a URL param
198+
sig_alg_option_map = {'sha1': SIG_RSA_SHA1,
199+
'sha256': SIG_RSA_SHA256}
200+
sig_alg_option = getattr(conf, '_sp_authn_requests_signed_alg', 'sha1')
201+
kwargs["sigalg"] = sig_alg_option_map[sig_alg_option]
186202
session_id, result = client.prepare_for_authenticate(
187203
entityid=selected_idp, relay_state=came_from,
188-
binding=binding, sign=False, sigalg=sigalg,
189-
nsprefix=nsprefix, **kwargs)
204+
binding=binding, sign=False, nsprefix=nsprefix,
205+
**kwargs)
190206
except TypeError as e:
191207
logger.error('Unable to know which IdP to use')
192208
return HttpResponse(str(e))
@@ -376,7 +392,13 @@ def logout(request, config_loader_path=None):
376392
'The session does not contain the subject id for user %s',
377393
request.user)
378394

379-
result = client.global_logout(subject_id)
395+
try:
396+
result = client.global_logout(subject_id)
397+
except LogoutError as exp:
398+
logger.exception('Error Handled - SLO not supported by IDP: {}'.format(exp))
399+
auth.logout(request)
400+
state.sync()
401+
return HttpResponseRedirect('/')
380402

381403
state.sync()
382404

setup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import codecs
1717
import os
18-
import sys
1918
from setuptools import setup, find_packages
2019

2120

@@ -63,4 +62,8 @@ def read(*rnames):
6362
'Django>=2.2',
6463
'pysaml2>=4.6.0',
6564
],
65+
tests_require=[
66+
# Provides assert_called_once.
67+
'mock;python_version < "3.6"',
68+
]
6669
)

tests/testprofiles/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ class StandaloneUserModel(models.Model):
3030
USERNAME_FIELD.
3131
"""
3232
username = models.CharField(max_length=30, unique=True)
33+
34+
35+
class RequiredFieldUser(models.Model):
36+
email = models.EmailField(unique=True)
37+
email_verified = models.BooleanField()
38+
39+
USERNAME_FIELD = 'email'
40+
41+
def set_unusable_password(self):
42+
pass

tests/testprofiles/tests.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,26 @@ def test_invalid_model_attribute_log(self):
190190
logs.output,
191191
)
192192

193+
@override_settings(AUTH_USER_MODEL='testprofiles.RequiredFieldUser')
194+
def test_create_user_with_required_fields(self):
195+
backend = Saml2Backend()
196+
attribute_mapping = {
197+
'mail': ['email'],
198+
'mail_verified': ['email_verified']
199+
}
200+
attributes = {
201+
'mail': ['[email protected]'],
202+
'mail_verified': [True],
203+
}
204+
# User creation does not fail if several fields are required.
205+
user = backend._get_or_create_saml2_user(
206+
207+
attributes,
208+
attribute_mapping,
209+
)
210+
self.assertEquals(user.email, '[email protected]')
211+
self.assertIs(user.email_verified, True)
212+
193213
def test_django_user_main_attribute(self):
194214
old_username_field = User.USERNAME_FIELD
195215
User.USERNAME_FIELD = 'slug'

0 commit comments

Comments
 (0)