From 5844747e3243004ea81301be057314920dfaf8df Mon Sep 17 00:00:00 2001
From: Matthias Mair <code@mjmair.com>
Date: Thu, 4 Jul 2024 23:24:55 +0200
Subject: [PATCH] Fix SSO theme selection (#7556)

* Adjust caching key to be numeric for user

* Add strong usermodel to colortheme

* switch to using user model everywhere for colortheme

* add some types

* use pk instead of id

* fix migration clash

* fix request
---
 .../templatetags/inventree_extras.py          | 11 ++++--
 src/backend/InvenTree/InvenTree/views.py      |  4 +-
 .../migrations/0028_colortheme_user_obj.py    | 39 +++++++++++++++++++
 src/backend/InvenTree/common/models.py        |  1 +
 .../InvenTree/settings/user_display.html      |  2 +-
 .../InvenTree/templates/account/base.html     |  2 +-
 src/backend/InvenTree/templates/base.html     |  2 +-
 src/backend/InvenTree/users/models.py         | 10 ++---
 8 files changed, 57 insertions(+), 14 deletions(-)
 create mode 100644 src/backend/InvenTree/common/migrations/0028_colortheme_user_obj.py

diff --git a/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py b/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py
index 00641abc19..1ac050f7e6 100644
--- a/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py
+++ b/src/backend/InvenTree/InvenTree/templatetags/inventree_extras.py
@@ -438,9 +438,9 @@ def progress_bar(val, max_val, *args, **kwargs):
 
 
 @register.simple_tag()
-def get_color_theme_css(username):
+def get_color_theme_css(user):
     """Return the custom theme .css file for the selected user."""
-    user_theme_name = get_user_color_theme(username)
+    user_theme_name = get_user_color_theme(user)
     # Build path to CSS sheet
     inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css')
 
@@ -451,12 +451,15 @@ def get_color_theme_css(username):
 
 
 @register.simple_tag()
-def get_user_color_theme(username):
+def get_user_color_theme(user):
     """Get current user color theme."""
     from common.models import ColorTheme
 
+    if not user.is_authenticated:
+        return 'default'
+
     try:
-        user_theme = ColorTheme.objects.filter(user=username).get()
+        user_theme = ColorTheme.objects.filter(user_obj=user).get()
         user_theme_name = user_theme.name
         if not user_theme_name or not ColorTheme.is_valid_choice(user_theme):
             user_theme_name = 'default'
diff --git a/src/backend/InvenTree/InvenTree/views.py b/src/backend/InvenTree/InvenTree/views.py
index 176e704b19..4f2293b894 100644
--- a/src/backend/InvenTree/InvenTree/views.py
+++ b/src/backend/InvenTree/InvenTree/views.py
@@ -614,7 +614,7 @@ class AppearanceSelectView(RedirectView):
         """Get current user color theme."""
         try:
             user_theme = common_models.ColorTheme.objects.filter(
-                user=self.request.user
+                user_obj=self.request.user
             ).get()
         except common_models.ColorTheme.DoesNotExist:
             user_theme = None
@@ -631,7 +631,7 @@ class AppearanceSelectView(RedirectView):
         # Create theme entry if user did not select one yet
         if not user_theme:
             user_theme = common_models.ColorTheme()
-            user_theme.user = request.user
+            user_theme.user_obj = request.user
 
         if theme:
             try:
diff --git a/src/backend/InvenTree/common/migrations/0028_colortheme_user_obj.py b/src/backend/InvenTree/common/migrations/0028_colortheme_user_obj.py
new file mode 100644
index 0000000000..5e2ea45c01
--- /dev/null
+++ b/src/backend/InvenTree/common/migrations/0028_colortheme_user_obj.py
@@ -0,0 +1,39 @@
+# Generated by Django 4.2.12 on 2024-07-04 10:23
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+def migrate_userthemes(apps, schema_editor):
+    """Mgrate text-based user references to ForeignKey references."""
+    ColorTheme = apps.get_model("common", "ColorTheme")
+    User = apps.get_model(settings.AUTH_USER_MODEL)
+
+    for theme in ColorTheme.objects.all():
+        try:
+            theme.user_obj = User.objects.get(username=theme.user)
+            theme.save()
+        except User.DoesNotExist:
+            pass
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("common", "0027_alter_customunit_symbol"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="colortheme",
+            name="user_obj",
+            field=models.ForeignKey(
+                blank=True,
+                null=True,
+                on_delete=django.db.models.deletion.CASCADE,
+                to=settings.AUTH_USER_MODEL,
+            ),
+        ),
+        migrations.RunPython(migrate_userthemes, migrations.RunPython.noop),
+    ]
diff --git a/src/backend/InvenTree/common/models.py b/src/backend/InvenTree/common/models.py
index 0309caf442..ceb5e2dcb9 100644
--- a/src/backend/InvenTree/common/models.py
+++ b/src/backend/InvenTree/common/models.py
@@ -2583,6 +2583,7 @@ class ColorTheme(models.Model):
     name = models.CharField(max_length=20, default='', blank=True)
 
     user = models.CharField(max_length=150, unique=True)
+    user_obj = models.ForeignKey(User, on_delete=models.CASCADE, blank=True, null=True)
 
     @classmethod
     def get_color_themes_choices(cls):
diff --git a/src/backend/InvenTree/templates/InvenTree/settings/user_display.html b/src/backend/InvenTree/templates/InvenTree/settings/user_display.html
index 7ae7ae19e3..ab2144939d 100644
--- a/src/backend/InvenTree/templates/InvenTree/settings/user_display.html
+++ b/src/backend/InvenTree/templates/InvenTree/settings/user_display.html
@@ -41,7 +41,7 @@
             <div class='form-group input-group mb-3'>
                 <select id='theme' name='theme' class='select form-control'>
                     {% get_available_themes as themes %}
-                    {% get_user_color_theme request.user.username as user_theme %}
+                    {% get_user_color_theme request.user as user_theme %}
                     {% for theme in themes %}
                     <option value='{{ theme.key }}'{% if theme.key == user_theme %} selected{% endif %}>{{ theme.name }}</option>
                     {% endfor %}
diff --git a/src/backend/InvenTree/templates/account/base.html b/src/backend/InvenTree/templates/account/base.html
index 26b0a39639..cec3145135 100644
--- a/src/backend/InvenTree/templates/account/base.html
+++ b/src/backend/InvenTree/templates/account/base.html
@@ -38,7 +38,7 @@
 <link rel="stylesheet" href="{% static 'select2/css/select2-bootstrap-5-theme.css' %}">
 <link rel="stylesheet" href="{% static 'css/inventree.css' %}">
 
-<link rel="stylesheet" href="{% get_color_theme_css user.get_username %}">
+<link rel="stylesheet" href="{% get_color_theme_css request.user %}">
 
 <title>
     {% inventree_title %} | {% block head_title %}{% endblock head_title %}
diff --git a/src/backend/InvenTree/templates/base.html b/src/backend/InvenTree/templates/base.html
index f8c675db92..5cb34fca5f 100644
--- a/src/backend/InvenTree/templates/base.html
+++ b/src/backend/InvenTree/templates/base.html
@@ -55,7 +55,7 @@
 
 <link rel="stylesheet" href="{% static 'css/inventree.css' %}">
 
-<link rel="stylesheet" href="{% get_color_theme_css user.get_username %}">
+<link rel="stylesheet" href="{% get_color_theme_css request.user %}">
 
 <style>
     {% block css %}
diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py
index 48bb915b6a..4486cd63eb 100644
--- a/src/backend/InvenTree/users/models.py
+++ b/src/backend/InvenTree/users/models.py
@@ -405,7 +405,7 @@ class RuleSet(models.Model):
     )
 
     @classmethod
-    def check_table_permission(cls, user, table, permission):
+    def check_table_permission(cls, user: User, table, permission):
         """Check if the provided user has the specified permission against the table."""
         # Superuser knows no bounds
         if user.is_superuser:
@@ -664,7 +664,7 @@ def update_group_roles(group, debug=False):
                         )
 
 
-def clear_user_role_cache(user):
+def clear_user_role_cache(user: User):
     """Remove user role permission information from the cache.
 
     - This function is called whenever the user / group is updated
@@ -674,11 +674,11 @@ def clear_user_role_cache(user):
     """
     for role in RuleSet.get_ruleset_models().keys():
         for perm in ['add', 'change', 'view', 'delete']:
-            key = f'role_{user}_{role}_{perm}'
+            key = f'role_{user.pk}_{role}_{perm}'
             cache.delete(key)
 
 
-def check_user_role(user, role, permission):
+def check_user_role(user: User, role, permission):
     """Check if a user has a particular role:permission combination.
 
     If the user is a superuser, this will return True
@@ -687,7 +687,7 @@ def check_user_role(user, role, permission):
         return True
 
     # First, check the cache
-    key = f'role_{user}_{role}_{permission}'
+    key = f'role_{user.pk}_{role}_{permission}'
 
     try:
         result = cache.get(key)