Skip to content

Commit 9d298c5

Browse files
committed
restructure
1 parent b233626 commit 9d298c5

File tree

2 files changed

+97
-92
lines changed

2 files changed

+97
-92
lines changed

djangosaml2/backends.py

Lines changed: 68 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,9 @@ def get_model(model_path: str):
3232
try:
3333
return apps.get_model(model_path)
3434
except LookupError:
35-
raise ImproperlyConfigured("SAML_USER_MODEL refers to model '%s' that has not been installed" % model_path)
35+
raise ImproperlyConfigured(f"SAML_USER_MODEL refers to model '{model_path}' that has not been installed")
3636
except ValueError:
37-
raise ImproperlyConfigured("SAML_USER_MODEL must be of the form 'app_label.model_name'")
38-
39-
40-
def get_saml_user_model():
41-
""" Returns the user model specified in the settings, or the default one from this Django installation """
42-
if hasattr(settings, 'SAML_USER_MODEL'):
43-
return get_model(settings.SAML_USER_MODEL)
44-
return auth.get_user_model()
45-
46-
47-
def get_django_user_lookup_attribute(userModel) -> str:
48-
""" Returns the attribute on which to match the identifier with for the user lookup
49-
"""
50-
if hasattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE'):
51-
return settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
52-
return getattr(userModel, 'USERNAME_FIELD', 'username')
37+
raise ImproperlyConfigured(f"SAML_USER_MODEL is {model_path}, but must be of the form 'app_label.model_name'")
5338

5439

5540
def set_attribute(obj: Any, attr: str, new_value: Any) -> bool:
@@ -70,26 +55,31 @@ def set_attribute(obj: Any, attr: str, new_value: Any) -> bool:
7055

7156

7257
class Saml2Backend(ModelBackend):
73-
def is_authorized(self, attributes, attribute_mapping) -> bool:
74-
""" Hook to allow custom authorization policies based on SAML attributes. """
75-
return True
7658

77-
def clean_attributes(self, attributes: dict) -> dict:
78-
""" Hook to clean attributes from the SAML response. """
79-
return attributes
59+
# ############################################
60+
# Internal logic, not meant to be overwritten
61+
# ############################################
8062

81-
def clean_user_main_attribute(self, main_attribute):
82-
""" Clean the extracted user identifying value. No-op by default. """
83-
return main_attribute
63+
@property
64+
def _user_model(self):
65+
""" Returns the user model specified in the settings, or the default one from this Django installation """
66+
if hasattr(settings, 'SAML_USER_MODEL'):
67+
return get_model(settings.SAML_USER_MODEL)
68+
return auth.get_user_model()
69+
70+
@property
71+
def _user_lookup_attribute(self) -> str:
72+
""" Returns the attribute on which to match the identifier with when performing a user lookup """
73+
if hasattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE'):
74+
return settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
75+
return getattr(self._user_model, 'USERNAME_FIELD', 'username')
8476

8577
def _extract_user_identifier_params(self, session_info, attributes, attribute_mapping) -> Tuple[str, Optional[Any]]:
8678
""" Returns the attribute to perform a user lookup on, and the value to use for it.
8779
The value could be the name_id, or any other saml attribute from the request.
8880
"""
89-
UserModel = get_saml_user_model()
90-
9181
# Lookup key
92-
user_lookup_key = get_django_user_lookup_attribute(UserModel)
82+
user_lookup_key = self._user_lookup_attribute
9383

9484
# Lookup value
9585
if getattr(settings, 'SAML_USE_NAME_ID_AS_USERNAME', False):
@@ -117,37 +107,6 @@ def _get_attribute_value(self, django_field, attributes, attribute_mapping):
117107
'session is expired.')
118108
return saml_attribute
119109

120-
def get_or_create_user(self, user_lookup_key, user_lookup_value, create_unknown_user, **kwargs) -> Tuple[Optional[settings.AUTH_USER_MODEL], bool]:
121-
""" Look up the user to authenticate. If he doesn't exist, this method creates him (if so desired).
122-
The default implementation looks only at the user_identifier. Override this method in order to do more complex behaviour,
123-
e.g. customize this per IdP. The kwargs contain these additional params: session_info, attribute_mapping, attributes, request.
124-
The identity provider id can be found in kwargs['session_info']['issuer]
125-
"""
126-
UserModel = get_saml_user_model()
127-
128-
# Construct query parameters to query the userModel with. An additional lookup modifier could be specified in the settings.
129-
user_query_args = {
130-
user_lookup_key + getattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', ''): user_lookup_value
131-
}
132-
133-
# Lookup existing user
134-
user, created = None, False
135-
try:
136-
user = UserModel.objects.get(**user_query_args)
137-
except MultipleObjectsReturned:
138-
logger.error("Multiple users match, model: %s, lookup: %s", str(UserModel._meta), user_query_args)
139-
except UserModel.DoesNotExist:
140-
# Create new one if desired by settings
141-
if create_unknown_user:
142-
user = UserModel(**user_query_args)
143-
created = True
144-
if created:
145-
logger.debug('New user created: %s', user)
146-
else:
147-
logger.error('The user does not exist, model: %s, lookup: %s', str(UserModel._meta), user_query_args)
148-
149-
return user, created
150-
151110
def authenticate(self, request, session_info=None, attribute_mapping=None, create_unknown_user=True, **kwargs):
152111
if session_info is None or attribute_mapping is None:
153112
logger.info('Session info or attribute mapping are None')
@@ -223,8 +182,56 @@ def _update_user(self, user, attributes, attribute_mapping, force_save=False):
223182

224183
return user
225184

226-
def send_user_update_signal(self, user, attributes, user_modified) -> bool:
185+
# ############################################
186+
# Hooks to override by end-users in subclasses
187+
# ############################################
188+
189+
def clean_attributes(self, attributes: dict) -> dict:
190+
""" Hook to clean or filter attributes from the SAML response. No-op by default. """
191+
return attributes
192+
193+
def is_authorized(self, attributes: dict, attribute_mapping: dict) -> bool:
194+
""" Hook to allow custom authorization policies based on SAML attributes. True by default. """
195+
return True
196+
197+
def clean_user_main_attribute(self, main_attribute: Any) -> Any:
198+
""" Hook to clean the extracted user-identifying value. No-op by default. """
199+
return main_attribute
200+
201+
def get_or_create_user(self, user_lookup_key: str, user_lookup_value: Any, create_unknown_user: bool, **kwargs) -> Tuple[Optional[settings.AUTH_USER_MODEL], bool]:
202+
""" Look up the user to authenticate. If he doesn't exist, this method creates him (if so desired).
203+
The default implementation looks only at the user_identifier. Override this method in order to do more complex behaviour,
204+
e.g. customize this per IdP. The kwargs contain these additional params: session_info, attribute_mapping, attributes, request.
205+
The identity provider id can be found in kwargs['session_info']['issuer]
206+
"""
207+
UserModel = self._user_model
208+
209+
# Construct query parameters to query the userModel with. An additional lookup modifier could be specified in the settings.
210+
user_query_args = {
211+
user_lookup_key + getattr(settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', ''): user_lookup_value
212+
}
213+
214+
# Lookup existing user
215+
user, created = None, False
216+
try:
217+
user = UserModel.objects.get(**user_query_args)
218+
except MultipleObjectsReturned:
219+
logger.error("Multiple users match, model: %s, lookup: %s", UserModel._meta, user_query_args)
220+
except UserModel.DoesNotExist:
221+
# Create new one if desired by settings
222+
if create_unknown_user:
223+
user = UserModel(**user_query_args)
224+
created = True
225+
logger.debug('New user created: %s', user)
226+
else:
227+
logger.error('The user does not exist, model: %s, lookup: %s', UserModel._meta, user_query_args)
228+
229+
return user, created
230+
231+
def send_user_update_signal(self, user: settings.AUTH_USER_MODEL, attributes: dict, user_modified: bool) -> bool:
227232
""" Send out a pre-save signal after the user has been updated with the SAML attributes.
233+
This does not have to be overwritten, but depending on your custom implementation of get_or_create_user,
234+
you might want to not send out this signal. In that case, just override this method to return False.
228235
"""
229236
logger.debug('Sending the pre_save signal')
230237
signal_modified = any(

tests/testprofiles/tests.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
from django.core.exceptions import ImproperlyConfigured
2222
from django.test import TestCase, override_settings
2323

24-
from djangosaml2.backends import (Saml2Backend,
25-
get_django_user_lookup_attribute, get_model,
26-
get_saml_user_model, set_attribute)
24+
from djangosaml2.backends import (Saml2Backend, get_model, set_attribute)
2725

2826
from .models import TestUser
2927

@@ -44,27 +42,9 @@ def test_get_model_nonexisting(self):
4442
def test_get_model_invalid_specifier(self):
4543
nonexisting_model = 'random_package.specifier.testprofiles.NonExisting'
4644

47-
with self.assertRaisesMessage(ImproperlyConfigured, "SAML_USER_MODEL must be of the form 'app_label.model_name'"):
45+
with self.assertRaisesMessage(ImproperlyConfigured, "SAML_USER_MODEL is random_package.specifier.testprofiles.NonExisting, but must be of the form 'app_label.model_name'"):
4846
get_model(nonexisting_model)
4947

50-
def test_get_saml_user_model_specified(self):
51-
with override_settings(AUTH_USER_MODEL='auth.User'):
52-
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
53-
self.assertEqual(get_saml_user_model(), TestUser)
54-
55-
def test_get_saml_user_model_default(self):
56-
with override_settings(AUTH_USER_MODEL='auth.User'):
57-
self.assertEqual(get_saml_user_model(), DjangoUserModel)
58-
59-
def test_get_django_user_lookup_attribute_specified(self):
60-
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
61-
with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE='age'):
62-
self.assertEqual(get_django_user_lookup_attribute(TestUser), 'age')
63-
64-
def test_get_django_user_lookup_attribute_default(self):
65-
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
66-
self.assertEqual(get_django_user_lookup_attribute(TestUser), 'username')
67-
6848
def test_set_attribute(self):
6949
u = TestUser()
7050
self.assertFalse(hasattr(u, 'custom_attribute'))
@@ -94,6 +74,24 @@ def setUp(self):
9474
self.backend = self.backend_cls()
9575
self.user = TestUser.objects.create(username='john')
9676

77+
def test_user_model_specified(self):
78+
with override_settings(AUTH_USER_MODEL='auth.User'):
79+
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
80+
self.assertEqual(self.backend._user_model, TestUser)
81+
82+
def test_user_model_default(self):
83+
with override_settings(AUTH_USER_MODEL='auth.User'):
84+
self.assertEqual(self.backend._user_model, DjangoUserModel)
85+
86+
def test_user_lookup_attribute_specified(self):
87+
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
88+
with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE='age'):
89+
self.assertEqual(self.backend._user_lookup_attribute, 'age')
90+
91+
def test_user_lookup_attribute_default(self):
92+
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
93+
self.assertEqual(self.backend._user_lookup_attribute, 'username')
94+
9795
def test_is_authorized(self):
9896
self.assertTrue(self.backend.is_authorized({}, {}))
9997

@@ -183,7 +181,7 @@ def test_invalid_model_attribute_log(self):
183181
}
184182

185183
with self.assertLogs('djangosaml2', level='DEBUG') as logs:
186-
user, _ = self.backend.get_or_create_user(get_django_user_lookup_attribute(get_saml_user_model()), 'john', True)
184+
user, _ = self.backend.get_or_create_user(self.backend._user_lookup_attribute, 'john', True)
187185
self.backend._update_user(user, attributes, attribute_mapping)
188186

189187
self.assertIn(
@@ -203,7 +201,7 @@ def test_create_user_with_required_fields(self):
203201
}
204202
# User creation does not fail if several fields are required.
205203
user, created = self.backend.get_or_create_user(
206-
get_django_user_lookup_attribute(get_saml_user_model()),
204+
self.backend._user_lookup_attribute,
207205
208206
True
209207
)
@@ -217,27 +215,27 @@ def test_create_user_with_required_fields(self):
217215
def test_django_user_main_attribute(self):
218216
old_username_field = User.USERNAME_FIELD
219217
User.USERNAME_FIELD = 'slug'
220-
self.assertEqual(get_django_user_lookup_attribute(get_saml_user_model()), 'slug')
218+
self.assertEqual(self.backend._user_lookup_attribute, 'slug')
221219
User.USERNAME_FIELD = old_username_field
222220

223221
with override_settings(AUTH_USER_MODEL='auth.User'):
224222
self.assertEqual(
225223
DjangoUserModel.USERNAME_FIELD,
226-
get_django_user_lookup_attribute(get_saml_user_model()))
224+
self.backend._user_lookup_attribute)
227225

228226
with override_settings(
229227
AUTH_USER_MODEL='testprofiles.StandaloneUserModel'):
230228
self.assertEqual(
231-
get_django_user_lookup_attribute(get_saml_user_model()),
229+
self.backend._user_lookup_attribute,
232230
'username')
233231

234232
with override_settings(SAML_DJANGO_USER_MAIN_ATTRIBUTE='foo'):
235-
self.assertEqual(get_django_user_lookup_attribute(get_saml_user_model()), 'foo')
233+
self.assertEqual(self.backend._user_lookup_attribute, 'foo')
236234

237235
def test_get_or_create_user_existing(self):
238236
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
239237
user, created = self.backend.get_or_create_user(
240-
get_django_user_lookup_attribute(get_saml_user_model()),
238+
self.backend._user_lookup_attribute,
241239
'john',
242240
False,
243241
)
@@ -267,7 +265,7 @@ def test_get_or_create_user_no_create(self):
267265
with self.assertLogs('djangosaml2', level='DEBUG') as logs:
268266
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
269267
user, created = self.backend.get_or_create_user(
270-
get_django_user_lookup_attribute(get_saml_user_model()),
268+
self.backend._user_lookup_attribute,
271269
'paul',
272270
False,
273271
)
@@ -283,7 +281,7 @@ def test_get_or_create_user_create(self):
283281
with self.assertLogs('djangosaml2', level='DEBUG') as logs:
284282
with override_settings(SAML_USER_MODEL='testprofiles.TestUser'):
285283
user, created = self.backend.get_or_create_user(
286-
get_django_user_lookup_attribute(get_saml_user_model()),
284+
self.backend._user_lookup_attribute,
287285
'paul',
288286
True,
289287
)

0 commit comments

Comments
 (0)