From 5c95a01996f6b4df8c767395565a5d8999c0107d Mon Sep 17 00:00:00 2001 From: Art Lukyanchyk <artiom.lukyanchyk@hs-hannover.de> Date: Mon, 18 Mar 2024 17:13:52 +0100 Subject: [PATCH] Use a generic unique ID attribute instead of UUID --- ssoauth/app_settings/defaults.py | 16 +++++----- ssoauth/auth_utils.py | 32 +++++++++----------- ssoauth/migrations/0003_replace_unique_id.py | 23 ++++++++++++++ ssoauth/models.py | 5 +-- ssoauth/views.py | 6 ++-- 5 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 ssoauth/migrations/0003_replace_unique_id.py diff --git a/ssoauth/app_settings/defaults.py b/ssoauth/app_settings/defaults.py index 1325de5..6b1586e 100644 --- a/ssoauth/app_settings/defaults.py +++ b/ssoauth/app_settings/defaults.py @@ -58,6 +58,14 @@ PREDEFINED_GROUPS = { CLEANUP_DEACTIVATE_AFTER = timedelta(days=7) # people are getting suspicious because of the old users that still seem active according to django CLEANUP_DELETE_USER_AFTER = timedelta(days=180) +# the block below defines from which SAML2 attributes this SP receives user data +ATTRIBUTE_USERNAME = "urn:oid:2.5.4.3" # cn (alternatively for example uid "urn:oid:0.9.2342.19200300.100.1.1") +ATTRIBUTE_EMAIL = "urn:oid:0.9.2342.19200300.100.1.3" # "mail" +ATTRIBUTE_FORENAME = "urn:oid:2.5.4.42" # "givenName" +ATTRIBUTE_SURNAME = "urn:oid:2.5.4.4" # "sn" +ATTRIBUTE_UNIQUE_ID = "urn:oid:1.3.6.1.4.1.5923.1.1.1.6" # "eduPersonPrincipalName" (expected to be permanent) +ATTRIBUTE_GROUPS = "ssoGroup" # HsH custom attribute (alternatively something like "urn:oid:1.3.6.1.4.1.5923.1.5.1.1" which is "isMemberOf") + """ Settings you might want to change on development (don't change them for production): @@ -79,14 +87,6 @@ PROJECT_NAME = os.environ.get('DJANGO_SETTINGS_MODULE').split('.')[0] PRETEND_AUTH_BACKEND = django_settings.AUTHENTICATION_BACKENDS[0] # pretend to be this backend; django does not expect that it is possible to log in without an authentication backend -# the block below defines from which SAML2 attributes this SP receives user data -ATTRIBUTE_USERNAME = "urn:oid:0.9.2342.19200300.100.1.1" # "uid" -ATTRIBUTE_EMAIL = "urn:oid:0.9.2342.19200300.100.1.3" # "mail" -ATTRIBUTE_FORENAME = "urn:oid:2.5.4.42" # "givenName" -ATTRIBUTE_SURNAME = "urn:oid:2.5.4.4" # "sn" -ATTRIBUTE_ACCOUNT_UUID = "UUID" # custom stuff, this one has no OID -ATTRIBUTE_GROUPS = "urn:oid:1.3.6.1.4.1.5923.1.5.1.1" # "isMemberOf" - GROUP_RESOLVER = None # deprecated """ diff --git a/ssoauth/auth_utils.py b/ssoauth/auth_utils.py index f8323bc..a83c3ed 100644 --- a/ssoauth/auth_utils.py +++ b/ssoauth/auth_utils.py @@ -1,4 +1,4 @@ -from uuid import UUID, uuid4 +import secrets from django.db import transaction from django.contrib.auth import get_user_model from django.contrib.auth.models import Group @@ -16,19 +16,17 @@ def _validate_username(username): raise ValueError("Username must be lowere case") -def get_user(uuid=None, username=None): +def get_user(unique_id=None, username=None): """ This helper just gets a user instance. """ - assert bool(uuid) ^ bool(username), "Need uuid OR username" - if uuid: - assert isinstance(uuid, UUID), "Not an UUID instance" - return models.UserMapping.objects.get(uuid=uuid).user + assert bool(unique_id) ^ bool(username), "Need unique_id OR username" + if unique_id: + return models.UserMapping.objects.get(unique_id=unique_id).user elif username: - assert isinstance(username, str), "Seriously?" return get_user_model().objects.get(username__iexact=username.lower()) @transaction.atomic() -def get_or_create_user(uuid, username): +def get_or_create_user(unique_id, username): """ Returns a user. Should be able to process a bunch of weird cases like changed username or duplicate usernames. @@ -40,14 +38,14 @@ def get_or_create_user(uuid, username): other_user = get_user(username=username) except get_user_model().DoesNotExist: return - new_username = "{0}_OLD_{1}".format(other_user.username, uuid4()) + new_username = "{0}_OLD_{1}".format(other_user.username, secrets.token_hex()) logger.warning("Found another user with username {old_username}. Renaming {old_username} to {new_username}".format(old_username=other_user.username, new_username=new_username)) other_user.username = new_username other_user.save() - def get_existing_user(uuid, username): + def get_existing_user(unique_id, username): try: - user = get_user(uuid=uuid) + user = get_user(unique_id=unique_id) if user.username != username: free_up_username(username) logger.warning("Username has changed. Renaming locally from {0} to {1}.".format(user.username, username)) @@ -57,23 +55,21 @@ def get_or_create_user(uuid, username): except models.UserMapping.DoesNotExist: return None - def create_user(uuid, username): + def create_user(unique_id, username): _validate_username(username) free_up_username(username) user = get_user_model().objects.create(username=username, is_staff=False) user.set_unusable_password() user.save() - models.UserMapping.objects.create(user=user, uuid=uuid) - logger.info("Created user: {username} {uuid}".format(**locals())) + models.UserMapping.objects.create(user=user, unique_id=unique_id) + logger.info("Created user: {username} {unique_id}".format(**locals())) return user # prepare - if isinstance(uuid, str): - uuid = UUID(uuid) - assert isinstance(uuid, UUID) and isinstance(username, str) + assert isinstance(unique_id, str) and isinstance(username, str) username = username.lower() # get or create - user = get_existing_user(uuid, username) or create_user(uuid, username) + user = get_existing_user(unique_id, username) or create_user(unique_id, username) # just in case, ensure the user object complies with the security rules cleanup_direct_permissions(user) return user diff --git a/ssoauth/migrations/0003_replace_unique_id.py b/ssoauth/migrations/0003_replace_unique_id.py new file mode 100644 index 0000000..874e33a --- /dev/null +++ b/ssoauth/migrations/0003_replace_unique_id.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2024-03-18 16:11 + +from django.db import migrations, models +import secrets + + +class Migration(migrations.Migration): + + dependencies = [ + ('ssoauth', '0002_auto_20190306_1823'), + ] + + operations = [ + migrations.RemoveField( + model_name='usermapping', + name='uuid', + ), + migrations.AddField( + model_name='usermapping', + name='unique_id', + field=models.TextField(default=secrets.token_hex), + ), + ] diff --git a/ssoauth/models.py b/ssoauth/models.py index 5bc2b06..753a659 100644 --- a/ssoauth/models.py +++ b/ssoauth/models.py @@ -1,13 +1,14 @@ +import secrets from django.db import models from django import conf class UserMapping(models.Model): user = models.OneToOneField(conf.settings.AUTH_USER_MODEL, primary_key=True, on_delete=models.CASCADE, related_name="sso_mapping") - uuid = models.UUIDField(null=False) + unique_id = models.TextField(null=False, default=secrets.token_hex) anonymized = models.BooleanField(null=False, default=False) imported_on = models.DateField(null=False, auto_now_add=True) def __str__(self): - return "{user} <-> {uuid}".format(user=self.user, uuid=self.uuid) + return "{user} <-> {unique_id}".format(user=self.user, unique_id=self.unique_id) diff --git a/ssoauth/views.py b/ssoauth/views.py index 62babb9..a80172a 100644 --- a/ssoauth/views.py +++ b/ssoauth/views.py @@ -1,3 +1,4 @@ +import secrets from django.views.generic import View, FormView, TemplateView, RedirectView from django import http from django import urls @@ -192,7 +193,7 @@ class ACSAuthNView(SAMLMixin, View): logger.debug("Synchronizing user using SAML2 data: {}".format(auth.get_attributes())) # get the user user = auth_utils.get_or_create_user( - uuid=get_attr(app_settings.ATTRIBUTE_ACCOUNT_UUID), + unique_id=get_attr(app_settings.ATTRIBUTE_UNIQUE_ID), username=get_attr(app_settings.ATTRIBUTE_USERNAME), ) # update user data @@ -338,8 +339,7 @@ class DevView(FormView): try: user = auth_utils.get_user(username=log_in_as_username) except exceptions.ObjectDoesNotExist: - import uuid - user = auth_utils.get_or_create_user(username=log_in_as_username, uuid=uuid.uuid4()) + user = auth_utils.get_or_create_user(username=log_in_as_username, unique_id=secrets.token_hex()) user.backend = app_settings.PRETEND_AUTH_BACKEND self.request.user = user contrib_auth.login(request=self.request, user=user) -- GitLab