From b967bef0245912739b0102d554c0506bb893be33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Miranda?= Date: Fri, 6 Nov 2020 01:24:39 -0300 Subject: [PATCH] Added configuration and signal to give more flexibility when creating the user. --- src/django_keycloak/app_settings.py | 8 +++ src/django_keycloak/services/oidc_profile.py | 62 ++++++++++++++++---- src/django_keycloak/signals.py | 5 ++ 3 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 src/django_keycloak/signals.py diff --git a/src/django_keycloak/app_settings.py b/src/django_keycloak/app_settings.py index 10562e6..8ae66b3 100644 --- a/src/django_keycloak/app_settings.py +++ b/src/django_keycloak/app_settings.py @@ -10,3 +10,11 @@ # Profile KEYCLOAK_REMOTE_USER_MODEL = 'django_keycloak.remote_user.KeycloakRemoteUser' KEYCLOAK_PERMISSIONS_METHOD = 'role' # 'role' of 'resource' + + +KEYCLOAK_ALWAYS_UPDATE_USER = False +KEYCLOAK_USERNAME_ATTR = 'sub' +KEYCLOAK_USER_ATTR_MAP = { + 'first_name': 'given_name', + 'last_name': 'family_name', +} diff --git a/src/django_keycloak/services/oidc_profile.py b/src/django_keycloak/services/oidc_profile.py index 2313231..b385b84 100644 --- a/src/django_keycloak/services/oidc_profile.py +++ b/src/django_keycloak/services/oidc_profile.py @@ -16,6 +16,7 @@ import django_keycloak.services.realm +from django_keycloak.signals import keycloak_populate_user logger = logging.getLogger(__name__) @@ -75,9 +76,40 @@ def get_or_create_from_id_token(client, id_token): client=client, id_token_object=id_token_object) -def update_or_create_user_and_oidc_profile(client, id_token_object): +def get_or_build_user(id_token_object): + UserModel = get_user_model() + + built = False + lookup = {UserModel.USERNAME_FIELD: id_token_object[settings.KEYCLOAK_USERNAME_ATTR]} + + try: + user = UserModel.objects.get(**lookup) + except UserModel.DoesNotExist: + user = UserModel(**lookup) + built = True + + return user, built + + +def _populate_user_from_attributes(user, id_token_object): + email_field_name = get_user_model().get_email_field_name() + + if email_field_name not in settings.KEYCLOAK_USER_ATTR_MAP: + settings.KEYCLOAK_USER_ATTR_MAP.update(**{email_field_name: 'email'}) + + for field, attr in settings.KEYCLOAK_USER_ATTR_MAP.items(): + try: + value = id_token_object[attr] + except (TypeError, LookupError): + logger.warning('Does not exists value for attribute {}'.format(attr)) + else: + setattr(user, field, value) + + +def update_or_create_user_and_oidc_profile(client, id_token_object, force_populate=False): """ + :param force_populate: boolean :param client: :param id_token_object: :return: @@ -100,16 +132,24 @@ def update_or_create_user_and_oidc_profile(client, id_token_object): return oidc_profile with transaction.atomic(): - UserModel = get_user_model() - email_field_name = UserModel.get_email_field_name() - user, _ = UserModel.objects.update_or_create( - username=id_token_object['sub'], - defaults={ - email_field_name: id_token_object.get('email', ''), - 'first_name': id_token_object.get('given_name', ''), - 'last_name': id_token_object.get('family_name', '') - } - ) + save_user = False + user, built = get_or_build_user(id_token_object) + + should_populate = force_populate or settings.KEYCLOAK_ALWAYS_UPDATE_USER or built + + if should_populate: + logger.info('Populating Django user {}'.format(user)) + _populate_user_from_attributes(user, id_token_object) + save_user = True + + # Give the client a chance to finish populating the user just before saving. + keycloak_populate_user.send( + type('KeycloakSignal', (object, ), {}), + user=user + ) + + if save_user or built: + user.save() oidc_profile, _ = OpenIdConnectProfileModel.objects.update_or_create( sub=id_token_object['sub'], diff --git a/src/django_keycloak/signals.py b/src/django_keycloak/signals.py new file mode 100644 index 0000000..bc9c50a --- /dev/null +++ b/src/django_keycloak/signals.py @@ -0,0 +1,5 @@ +from django.dispatch import Signal + +# Allows clients to perform custom user population. +# Passed arguments: user +keycloak_populate_user = Signal()