@@ -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
5540def 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
7257class 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 (
0 commit comments