11# Copyright (C) 2020 Glodo UK <https://www.glodo.uk/>
2- # Copyright (C) 2010-2016, 2022 XCG Consulting <https://xcg-consulting.fr />
2+ # Copyright (C) 2010-2016, 2022, 2025-2026 XCG SAS <https://orbeet.io />
33# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
44
55import base64
@@ -91,6 +91,7 @@ class AuthSamlProvider(models.Model):
9191 "auth.saml.attribute.mapping" ,
9292 "provider_id" ,
9393 string = "Attribute Mapping" ,
94+ copy = True ,
9495 )
9596 active = fields .Boolean (default = True )
9697 sequence = fields .Integer (index = True )
@@ -146,6 +147,27 @@ class AuthSamlProvider(models.Model):
146147 default = True ,
147148 help = "Whether metadata should be signed or not" ,
148149 )
150+ # User creation fields
151+ create_user = fields .Boolean (
152+ default = False ,
153+ help = "Create user if not found. The login and name will defaults to the SAML "
154+ "user matching attribute. Use the mapping attributes to change the value "
155+ "used. If a deactivated user has a matching saml uid, activate it rather than"
156+ "create a new one." ,
157+ )
158+ create_user_template_id = fields .Many2one (
159+ comodel_name = "res.users" ,
160+ # Template users should be disabled so allow them too
161+ domain = "[('active', 'in', (True, False))]" ,
162+ help = "When creating user, this user is used as a template" ,
163+ )
164+ create_user_reactivate = fields .Boolean (
165+ "Reactivate when Creating Users" ,
166+ default = False ,
167+ help = "If a deactivated user has a matching SAML uid when trying to create the "
168+ "user, and this is checked, then the user is reactivated. Otherwise,"
169+ "access is denied." ,
170+ )
149171
150172 @api .model
151173 def _sig_alg_selection (self ):
@@ -275,9 +297,7 @@ def _get_auth_request(self, extra_state=None, url_root=None):
275297 }
276298 state .update (extra_state )
277299
278- sig_alg = ds .SIG_RSA_SHA1
279- if self .sig_alg :
280- sig_alg = getattr (ds , self .sig_alg )
300+ sig_alg = getattr (ds , self .sig_alg )
281301
282302 saml_client = self ._get_client_for_provider (url_root )
283303 reqid , info = saml_client .prepare_for_authenticate (
@@ -291,6 +311,7 @@ def _get_auth_request(self, extra_state=None, url_root=None):
291311 for key , value in info ["headers" ]:
292312 if key == "Location" :
293313 redirect_url = value
314+ break
294315
295316 self ._store_outstanding_request (reqid )
296317
@@ -319,30 +340,19 @@ def _validate_auth_response(self, token: str, base_url: str = None):
319340 )
320341 else :
321342 raise
322- matching_value = None
323-
324- if self .matching_attribute == "subject.nameId" :
325- matching_value = response .name_id .text
326- else :
327- attrs = response .get_identity ()
328-
329- for k , v in attrs .items ():
330- if k == self .matching_attribute :
331- matching_value = v
332- break
333-
334- if not matching_value :
335- raise Exception (
336- self .env ._ (
337- "Matching attribute %(matching_attribute)s"
338- " not found in user attrs: %(attrs)s" ,
339- matching_attribute = self .matching_attribute ,
340- attrs = attrs ,
341- )
343+ try :
344+ matching_value = self ._get_attribute_value (
345+ response , self .matching_attribute
346+ )
347+ except KeyError :
348+ raise KeyError (
349+ self .env ._ (
350+ "Matching attribute %(matching_attribute)s not found "
351+ "in user attrs: %(attrs)s" ,
352+ matching_attribute = self .matching_attribute ,
353+ attrs = response .get_identity (),
342354 )
343-
344- if matching_value and isinstance (matching_value , list ):
345- matching_value = next (iter (matching_value ), None )
355+ ) from None
346356
347357 if isinstance (matching_value , str ) and self .matching_attribute_to_lower :
348358 matching_value = matching_value .lower ()
@@ -385,25 +395,39 @@ def _metadata_string(self, valid=None, base_url: str = None):
385395 sign = self .sign_metadata ,
386396 )
387397
398+ @staticmethod
399+ def _get_attribute_value (response , attribute_name : str ):
400+ """
401+
402+ :raise: KeyError if attribute is not in the response
403+ :param response:
404+ :param attribute_name:
405+ :return: value of the attribute. if the value is an empty list, return None
406+ otherwise return the first element of the list
407+ """
408+ if attribute_name == "subject.nameId" :
409+ return response .name_id .text
410+ attrs = response .get_identity ()
411+ attribute_value = attrs [attribute_name ]
412+ if isinstance (attribute_value , list ):
413+ attribute_value = next (iter (attribute_value ), None )
414+ return attribute_value
415+
388416 def _hook_validate_auth_response (self , response , matching_value ):
389417 self .ensure_one ()
390418 vals = {}
391- attrs = response .get_identity ()
392419
393420 for attribute in self .attribute_mapping_ids :
394- if attribute .attribute_name not in attrs :
395- _logger .debug (
421+ try :
422+ vals [attribute .field_name ] = self ._get_attribute_value (
423+ response , attribute .attribute_name
424+ )
425+ except KeyError :
426+ _logger .warning (
396427 "SAML attribute '%s' not found in response %s" ,
397428 attribute .attribute_name ,
398- attrs ,
429+ response . get_identity () ,
399430 )
400- continue
401-
402- attribute_value = attrs [attribute .attribute_name ]
403- if isinstance (attribute_value , list ):
404- attribute_value = attribute_value [0 ]
405-
406- vals [attribute .field_name ] = attribute_value
407431
408432 return {"mapped_attrs" : vals }
409433
@@ -450,3 +474,24 @@ def action_refresh_metadata_from_url(self):
450474 updated = True
451475
452476 return updated
477+
478+ def _user_copy_defaults (self , validation ):
479+ """
480+ Returns defaults when copying the template user.
481+
482+ Can be overridden with extra information.
483+ :param validation: validation result
484+ :return: a dictionary for copying template user, empty to avoid copying
485+ """
486+ self .ensure_one ()
487+ if not self .create_user :
488+ return {}
489+ saml_uid = validation ["user_id" ]
490+ return {
491+ "name" : saml_uid ,
492+ "login" : saml_uid ,
493+ "active" : True ,
494+ # if signature is not provided by mapped_attrs, it will be computed
495+ # due to call to compute method in calling method.
496+ "signature" : None ,
497+ } | validation .get ("mapped_attrs" , {})
0 commit comments