From 166387b6908f2e107d09c5649a204d0eba875456 Mon Sep 17 00:00:00 2001
From: Lennard Strohmeyer <lennard.strohmeyer@digitallearning.gmbh>
Date: Tue, 17 Dec 2024 10:51:34 +0100
Subject: [PATCH] #87: added mutex to hopefully prevent rare race condition
 when creating a new user from a shibboleth account

---
 src/backend/.env.dist   |  5 ++++-
 src/backend/.env.test   |  5 ++++-
 src/backend/settings.py |  9 +++++++++
 src/consents/views.py   | 38 ++++++++++++++++++++++++++++++--------
 src/xapi/views.py       |  2 +-
 5 files changed, 48 insertions(+), 11 deletions(-)

diff --git a/src/backend/.env.dist b/src/backend/.env.dist
index cf392f8..add1bca 100644
--- a/src/backend/.env.dist
+++ b/src/backend/.env.dist
@@ -18,4 +18,7 @@ IDP_ENABLED=
 SP_HOST=
 
 ANONYMIZATION_HASH_PREFIX=anon
-ANONYMIZATION_DEFAULT_MINIMUM_COUNT=10
\ No newline at end of file
+ANONYMIZATION_DEFAULT_MINIMUM_COUNT=10
+
+CACHE_BACKEND='redis'
+CACHE_URI='redis://127.0.0.1:6379'
\ No newline at end of file
diff --git a/src/backend/.env.test b/src/backend/.env.test
index 74be673..803a7a2 100644
--- a/src/backend/.env.test
+++ b/src/backend/.env.test
@@ -17,4 +17,7 @@ IDP_SERVER=https://aai-test-v3.ruhr-uni-bochum.de
 SP_HOST=
 
 ANONYMIZATION_HASH_PREFIX=anon
-ANONYMIZATION_DEFAULT_MINIMUM_COUNT=10
\ No newline at end of file
+ANONYMIZATION_DEFAULT_MINIMUM_COUNT=10
+
+CACHE_BACKEND='redis'
+CACHE_URI='redis://127.0.0.1:6379'
\ No newline at end of file
diff --git a/src/backend/settings.py b/src/backend/settings.py
index 258d1fc..e3bd093 100644
--- a/src/backend/settings.py
+++ b/src/backend/settings.py
@@ -127,6 +127,15 @@ else:
         }
     }
 
+# Cache(s)
+# https://docs.djangoproject.com/en/4.1/topics/cache/
+CACHES = {
+    'default': {
+        'BACKEND': 'django.core.cache.backends.redis.RedisCache' if env("CACHE_BACKEND", default="file") == "redis"
+                    else 'django.core.cache.backends.filebased.FileBasedCache',
+        'LOCATION': '/tmp/django_cache' if env("CACHE_BACKEND", default="file") == 'file' else env("CACHE_URI", default='redis://127.0.0.1:6379') ,
+    }
+}
 
 # Password validation
 # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
diff --git a/src/consents/views.py b/src/consents/views.py
index 22ebda2..680047a 100644
--- a/src/consents/views.py
+++ b/src/consents/views.py
@@ -3,7 +3,9 @@ import string
 import random
 import os
 import secrets
+import time
 
+from django.core.cache import cache
 from django.conf import settings
 from django.core.exceptions import ObjectDoesNotExist
 from django.http.response import JsonResponse
@@ -391,18 +393,38 @@ class CreateUserConsentViaConnectServiceView(APIView):
         shib_id = shib_connector_resolver_to_pairwaise_id(email=email, provider=provider)
 
         user = CustomUser.objects.filter(shibboleth_connector_identifier=shib_id).first()
-        
+
+        message = "user exists"
         if not user:
-           user = CustomUser.objects.create(
-                shibboleth_connector_identifier=shib_id, # this is the pairwaise id 
-                email=''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + "@manual-created.polaris",
-                first_name=''.join(random.choices(string.ascii_uppercase + string.digits, k=8)),
-                last_name=''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
-           )
+            lock_key = f"user_creation_lock_{shib_id}"  # "mutex" to prevent race conditions using a cache entry
+            if cache.add(lock_key, "locked", 2):  # 2 is a timeout value, after wich the cache entry is deleted
+                try:
+                    user = CustomUser.objects.create(
+                        shibboleth_connector_identifier=shib_id, # this is the pairwise id
+                        email=''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) + "@manual-created.polaris",
+                        first_name=''.join(random.choices(string.ascii_uppercase + string.digits, k=8)),
+                        last_name=''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
+                    )
+                except Exception as e:
+                    return JsonResponse({"message": "user could not be created"}, safe=False,
+                                        status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+                finally:
+                    message = "user has been created"
+                    cache.delete(lock_key)
+            else:
+                for _ in range(4):  # Wait up to 2x lock_timeout
+                    time.sleep(0.1)
+                    if not cache.get(lock_key):
+                        break
+                # Fetch the user created by another process
+                user = CustomUser.objects.filter(shibboleth_connector_identifier=shib_id).first()
+                if not user:
+                    return JsonResponse({"message": "user could not be created"}, safe=False,
+                                        status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
         return JsonResponse(
                     {
-                        "message": "user is created",
+                        "message": message,
                     },
                     safe=False,
                     status=status.HTTP_200_OK,
diff --git a/src/xapi/views.py b/src/xapi/views.py
index ed18d1d..00262b4 100644
--- a/src/xapi/views.py
+++ b/src/xapi/views.py
@@ -85,7 +85,7 @@ def shib_connector_resolver_to_pairwaise_id(email, provider):
          additionalData = None 
          shib_id = client.service.GetOrGenerateIdAndConnect(app_secret, user_id, lrs_type, linkType, processId, additionalData)
          if len(shib_id) != 1:
-            print("Multiple pairwaise ids found, only use the first one!")
+            print("Multiple pairwise ids found, only use the first one!")
          shib_id = shib_id[0]
          if settings.DEBUG:
              print("Result shib_id: {0}".format(shib_id))
-- 
GitLab