Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip #287 #292

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changes
=======

v1.3.0 (future)

- Add signals

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really great addition and amazing documentation!

- Refactoring some classes to facilitate inheritance / customisation
- Documentation for signals and inheritance / customisation

v1.2.2 (2021-05-27)
-------------------

Expand Down
83 changes: 54 additions & 29 deletions djangosaml2/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from django.core.exceptions import (ImproperlyConfigured,
MultipleObjectsReturned)

from .signals import authenticate, pre_user_save, post_user_save


logger = logging.getLogger('djangosaml2')

Expand Down Expand Up @@ -123,6 +125,13 @@ def authenticate(self, request, session_info=None, attribute_mapping=None, creat

if not self.is_authorized(attributes, attribute_mapping, idp_entityid, assertion_info):
logger.error('Request not authorized')
authenticate.send(sender=self,
request=request,
is_authorized=False,
can_authenticate=None,
user=None,
user_created=None,
attributes=attributes)
return None

user_lookup_key, user_lookup_value = self._extract_user_identifier_params(
Expand All @@ -141,7 +150,15 @@ def authenticate(self, request, session_info=None, attribute_mapping=None, creat
user = self._update_user(
user, attributes, attribute_mapping, force_save=created)

if self.user_can_authenticate(user):
can_authenticate = self.user_can_authenticate(user)
authenticate.send(sender=self,
request=request,
is_authorized=True,
can_authenticate=can_authenticate,
user=user,
user_created=created,
attributes=attributes)
if can_authenticate:
return user

def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_save: bool = False):
Expand All @@ -156,46 +173,54 @@ def _update_user(self, user, attributes: dict, attribute_mapping: dict, force_sa
if not attribute_mapping:
# Always save a brand new user instance
if user.pk is None:
pre_user_save.send(sender=self, user=user, attributes=attributes)
user = self.save_user(user)
post_user_save.send(sender=self, user=user, attributes=attributes)
return user

# Lookup key
user_lookup_key = self._user_lookup_attribute
has_updated_fields = self.lookup_and_set_attributes(user, attributes, attribute_mapping)

if has_updated_fields or force_save:
pre_user_save.send(sender=self, user=user, attributes=attributes)
user = self.save_user(user)
post_user_save.send(sender=self, user=user, attributes=attributes)

return user

# ################################################
# Methods to override by end-users in subclasses #
# ################################################

def lookup_and_set_attributes(self, user, attributes: dict, attribute_mapping: dict) -> bool:
has_updated_fields = False
for saml_attr, django_attrs in attribute_mapping.items():
attr_value_list = attributes.get(saml_attr)
if not attr_value_list:
logger.debug(
f'Could not find value for "{saml_attr}", not updating fields "{django_attrs}"')
continue

for attr in django_attrs:
if attr == user_lookup_key:
# Don't update user_lookup_key (e.g. username) (issue #245)
# It was just used to find/create this user and might have
# been changed by `clean_user_main_attribute`
continue
elif hasattr(user, attr):
user_attr = getattr(user, attr)
if callable(user_attr):
modified = user_attr(attr_value_list)
else:
modified = set_attribute(
user, attr, attr_value_list[0])

has_updated_fields = has_updated_fields or modified
else:
logger.debug(
f'Could not find attribute "{attr}" on user "{user}"')

if has_updated_fields or force_save:
user = self.save_user(user)

return user

# ############################################
# Hooks to override by end-users in subclasses
# ############################################
has_updated_fields = self.lookup_and_set_attribute(
user, attr, attr_value_list
) or has_updated_fields
return has_updated_fields

def lookup_and_set_attribute(self, user, attr, attr_value_list) -> bool:
if attr == self._user_lookup_attribute:
# Don't update user_lookup_key (e.g. username) (issue #245)
# It was just used to find/create this user and might have
# been changed by `clean_user_main_attribute`
return False
elif hasattr(user, attr):
user_attr = getattr(user, attr)
if callable(user_attr):
return user_attr(attr_value_list)
else:
return set_attribute(user, attr, attr_value_list[0])
else:
logger.debug(f'Could not find attribute "{attr}" on user "{user}"')
return False

def clean_attributes(self, attributes: dict, idp_entityid: str, **kwargs) -> dict:
""" Hook to clean or filter attributes from the SAML response. No-op by default. """
Expand Down
9 changes: 5 additions & 4 deletions djangosaml2/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

import django.dispatch

pre_user_save = django.dispatch.Signal(
providing_args=['attributes', 'user_modified'])
post_authenticated = django.dispatch.Signal(
providing_args=['session_info', 'request'])
pre_user_save = django.dispatch.Signal(providing_args=['user', 'attributes'])
post_user_save = django.dispatch.Signal(providing_args=['user', 'attributes'])
authenticate = django.dispatch.Signal(providing_args=[
'request', 'is_authorized', 'can_authenticate', 'user', 'user_created', 'attributes'
])
Loading