backends.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. # Copyright (C) 2010-2012 Yaco Sistemas (http://www.yaco.es)
  2. # Copyright (C) 2009 Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from django.conf import settings
  17. from django.contrib import auth
  18. from django.contrib.auth.backends import ModelBackend
  19. from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, \
  20. ImproperlyConfigured
  21. from djangosaml2.signals import pre_user_save
  22. from . import settings as saml_settings
  23. try:
  24. from django.contrib.auth.models import SiteProfileNotAvailable
  25. except ImportError:
  26. class SiteProfileNotAvailable(Exception):
  27. pass
  28. logger = logging.getLogger('djangosaml2')
  29. def get_model(model_path):
  30. try:
  31. from django.apps import apps
  32. return apps.get_model(model_path)
  33. except ImportError:
  34. # Django < 1.7 (cannot use the new app loader)
  35. from django.db.models import get_model as django_get_model
  36. try:
  37. app_label, model_name = model_path.split('.')
  38. except ValueError:
  39. raise ImproperlyConfigured("SAML_USER_MODEL must be of the form "
  40. "'app_label.model_name'")
  41. user_model = django_get_model(app_label, model_name)
  42. if user_model is None:
  43. raise ImproperlyConfigured("SAML_USER_MODEL refers to model '%s' "
  44. "that has not been installed" % model_path)
  45. return user_model
  46. def get_saml_user_model():
  47. try:
  48. # djangosaml2 custom user model
  49. return get_model(settings.SAML_USER_MODEL)
  50. except AttributeError:
  51. try:
  52. # Django 1.5 Custom user model
  53. return auth.get_user_model()
  54. except AttributeError:
  55. return auth.models.User
  56. class Saml2Backend(ModelBackend):
  57. def authenticate(self, request, session_info=None, attribute_mapping=None,
  58. create_unknown_user=True, **kwargs):
  59. if session_info is None or attribute_mapping is None:
  60. logger.error('Session info or attribute mapping are None')
  61. return None
  62. if not 'ava' in session_info:
  63. logger.error('"ava" key not found in session_info')
  64. return None
  65. attributes = session_info['ava']
  66. if not attributes:
  67. logger.error('The attributes dictionary is empty')
  68. use_name_id_as_username = getattr(
  69. settings, 'SAML_USE_NAME_ID_AS_USERNAME', False)
  70. django_user_main_attribute = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
  71. django_user_main_attribute_lookup = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP
  72. logger.debug('attributes: %s', attributes)
  73. saml_user = None
  74. if use_name_id_as_username:
  75. if 'name_id' in session_info:
  76. logger.debug('name_id: %s', session_info['name_id'])
  77. saml_user = session_info['name_id'].text
  78. else:
  79. logger.error('The nameid is not available. Cannot find user without a nameid.')
  80. else:
  81. saml_user = self.get_attribute_value(django_user_main_attribute, attributes, attribute_mapping)
  82. if saml_user is None:
  83. logger.error('Could not find saml_user value')
  84. return None
  85. if not self.is_authorized(attributes, attribute_mapping):
  86. return None
  87. main_attribute = self.clean_user_main_attribute(saml_user)
  88. # Note that this could be accomplished in one try-except clause, but
  89. # instead we use get_or_create when creating unknown users since it has
  90. # built-in safeguards for multiple threads.
  91. return self.get_saml2_user(
  92. create_unknown_user, main_attribute, attributes, attribute_mapping)
  93. def get_attribute_value(self, django_field, attributes, attribute_mapping):
  94. saml_user = None
  95. logger.debug('attribute_mapping: %s', attribute_mapping)
  96. for saml_attr, django_fields in attribute_mapping.items():
  97. if django_field in django_fields and saml_attr in attributes:
  98. saml_user = attributes[saml_attr][0]
  99. return saml_user
  100. def is_authorized(self, attributes, attribute_mapping):
  101. """Hook to allow custom authorization policies based on
  102. SAML attributes.
  103. """
  104. return True
  105. def clean_user_main_attribute(self, main_attribute):
  106. """Performs any cleaning on the user main attribute (which
  107. usually is "username") prior to using it to get or
  108. create the user object. Returns the cleaned attribute.
  109. By default, returns the attribute unchanged.
  110. """
  111. return main_attribute
  112. def get_user_query_args(self, main_attribute):
  113. django_user_main_attribute = getattr(
  114. settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE', 'username')
  115. django_user_main_attribute_lookup = getattr(
  116. settings, 'SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP', '')
  117. return {
  118. django_user_main_attribute + django_user_main_attribute_lookup: main_attribute
  119. }
  120. def get_saml2_user(self, create, main_attribute, attributes, attribute_mapping):
  121. if create:
  122. return self._get_or_create_saml2_user(main_attribute, attributes, attribute_mapping)
  123. return self._get_saml2_user(main_attribute, attributes, attribute_mapping)
  124. def _get_or_create_saml2_user(self, main_attribute, attributes, attribute_mapping):
  125. logger.debug('Check if the user "%s" exists or create otherwise',
  126. main_attribute)
  127. django_user_main_attribute = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
  128. django_user_main_attribute_lookup = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE_LOOKUP
  129. user_query_args = self.get_user_query_args(main_attribute)
  130. user_create_defaults = {django_user_main_attribute: main_attribute}
  131. User = get_saml_user_model()
  132. try:
  133. user, created = User.objects.get_or_create(
  134. defaults=user_create_defaults, **user_query_args)
  135. except MultipleObjectsReturned:
  136. logger.error("There are more than one user with %s = %s",
  137. django_user_main_attribute, main_attribute)
  138. return None
  139. if created:
  140. logger.debug('New user created')
  141. user = self.configure_user(user, attributes, attribute_mapping)
  142. else:
  143. logger.debug('User updated')
  144. user = self.update_user(user, attributes, attribute_mapping)
  145. return user
  146. def _get_saml2_user(self, main_attribute, attributes, attribute_mapping):
  147. User = get_saml_user_model()
  148. django_user_main_attribute = saml_settings.SAML_DJANGO_USER_MAIN_ATTRIBUTE
  149. user_query_args = self.get_user_query_args(main_attribute)
  150. logger.debug('Retrieving existing user "%s"', main_attribute)
  151. try:
  152. user = User.objects.get(**user_query_args)
  153. user = self.update_user(user, attributes, attribute_mapping)
  154. except User.DoesNotExist:
  155. logger.error('The user "%s" does not exist', main_attribute)
  156. return None
  157. except MultipleObjectsReturned:
  158. logger.error("There are more than one user with %s = %s",
  159. django_user_main_attribute, main_attribute)
  160. return None
  161. return user
  162. def configure_user(self, user, attributes, attribute_mapping):
  163. """Configures a user after creation and returns the updated user.
  164. By default, returns the user with his attributes updated.
  165. """
  166. user.set_unusable_password()
  167. return self.update_user(user, attributes, attribute_mapping,
  168. force_save=True)
  169. def update_user(self, user, attributes, attribute_mapping,
  170. force_save=False):
  171. """Update a user with a set of attributes and returns the updated user.
  172. By default it uses a mapping defined in the settings constant
  173. SAML_ATTRIBUTE_MAPPING. For each attribute, if the user object has
  174. that field defined it will be set, otherwise it will try to set
  175. it in the profile object.
  176. """
  177. if not attribute_mapping:
  178. return user
  179. try:
  180. profile = user.get_profile()
  181. except ObjectDoesNotExist:
  182. profile = None
  183. except SiteProfileNotAvailable:
  184. profile = None
  185. # Django 1.5 custom model assumed
  186. except AttributeError:
  187. profile = user
  188. user_modified = False
  189. profile_modified = False
  190. for saml_attr, django_attrs in attribute_mapping.items():
  191. attr_value_list = attributes.get(saml_attr)
  192. if not attr_value_list:
  193. continue
  194. for attr in django_attrs:
  195. if hasattr(user, attr):
  196. user_attr = getattr(user, attr)
  197. if callable(user_attr):
  198. modified = user_attr(attr_value_list)
  199. else:
  200. modified = self._set_attribute(user, attr, attr_value_list[0])
  201. user_modified = user_modified or modified
  202. elif profile is not None and hasattr(profile, attr):
  203. modified = self._set_attribute(profile, attr, attr_value_list[0])
  204. profile_modified = profile_modified or modified
  205. logger.debug('Sending the pre_save signal')
  206. signal_modified = any(
  207. [response for receiver, response
  208. in pre_user_save.send_robust(sender=user.__class__,
  209. instance=user,
  210. attributes=attributes,
  211. user_modified=user_modified)]
  212. )
  213. if user_modified or signal_modified or force_save:
  214. user.save()
  215. if (profile is not None
  216. and (profile_modified or signal_modified or force_save)):
  217. profile.save()
  218. return user
  219. def _set_attribute(self, obj, attr, value):
  220. """Set an attribute of an object to a specific value.
  221. Return True if the attribute was changed and False otherwise.
  222. """
  223. field = obj._meta.get_field(attr)
  224. if field.max_length is not None and len(value) > field.max_length:
  225. cleaned_value = value[:field.max_length]
  226. logger.warn('The attribute "%s" was trimmed from "%s" to "%s"',
  227. attr, value, cleaned_value)
  228. else:
  229. cleaned_value = value
  230. old_value = getattr(obj, attr)
  231. if cleaned_value != old_value:
  232. setattr(obj, attr, cleaned_value)
  233. return True
  234. return False