From b6f83742a75890c859f6088b028fc40c6f0c55df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rn=20K=C3=BCnsem=C3=B6ller?=
 <jkuensem@physik.uni-bielefeld.de>
Date: Wed, 21 Apr 2021 10:10:00 +0200
Subject: [PATCH] TMSS-719: claim project roles from OIDC/Keycloak and adapt
 user model to hold them

---
 SAS/TMSS/backend/src/tmss/CMakeLists.txt      |  1 +
 .../src/tmss/authentication_backends.py       | 41 ++++++++++++
 SAS/TMSS/backend/src/tmss/settings.py         |  6 +-
 SAS/TMSS/backend/src/tmss/tmssapp/admin.py    |  6 +-
 .../tmss/tmssapp/migrations/0001_initial.py   | 67 +++++++++++++++----
 .../src/tmss/tmssapp/models/permissions.py    | 12 ++++
 .../src/tmss/tmssapp/models/scheduling.py     |  2 +-
 SAS/TMSS/backend/src/tmss/tmssapp/populate.py |  8 ++-
 .../tmss/tmssapp/serializers/specification.py |  2 +-
 .../src/tmss/tmssapp/viewsets/permissions.py  | 12 +---
 .../tmss/tmssapp/viewsets/specification.py    |  3 +-
 SAS/TMSS/backend/test/t_permissions.py        |  2 -
 .../test/t_permissions_system_roles.py        |  4 +-
 .../test/t_tmssapp_specification_REST_API.py  |  4 +-
 SAS/TMSS/backend/test/test_utils.py           |  3 +-
 15 files changed, 134 insertions(+), 39 deletions(-)
 create mode 100644 SAS/TMSS/backend/src/tmss/authentication_backends.py

diff --git a/SAS/TMSS/backend/src/tmss/CMakeLists.txt b/SAS/TMSS/backend/src/tmss/CMakeLists.txt
index 3e7754777f2..eff8df62cd8 100644
--- a/SAS/TMSS/backend/src/tmss/CMakeLists.txt
+++ b/SAS/TMSS/backend/src/tmss/CMakeLists.txt
@@ -7,6 +7,7 @@ set(_py_files
     urls.py
     wsgi.py
     exceptions.py
+    authentication_backends.py
     )
 
 python_install(${_py_files}
diff --git a/SAS/TMSS/backend/src/tmss/authentication_backends.py b/SAS/TMSS/backend/src/tmss/authentication_backends.py
new file mode 100644
index 00000000000..465146217c9
--- /dev/null
+++ b/SAS/TMSS/backend/src/tmss/authentication_backends.py
@@ -0,0 +1,41 @@
+from mozilla_django_oidc.auth import OIDCAuthenticationBackend
+import logging
+from lofar.sas.tmss.tmss.tmssapp.models import ProjectRole
+
+logger = logging.getLogger(__name__)
+
+class TMSSOIDCAuthenticationBackend(OIDCAuthenticationBackend):
+    """
+    A custom OIDCAuthenticationBackend, that allows us to perform extra actions when a user gets authenticated,
+    most importantly we can assign the user's system and project roles according to the claims that we get from the
+    identity provider.
+    """
+
+    def _set_user_project_roles_from_claims(self, user, claims):
+        project_roles = []
+        prefix = 'urn:mace:astron.nl:science:group:lofar:project:'
+        for entitlement in claims.get('eduperson_entitlement', []):
+            try:
+                if entitlement.startswith(prefix):
+                    project, role = entitlement.replace(prefix, '').split(':')
+                    role = role.replace('role=', '')
+                    if ProjectRole.objects.filter(value=role).count() > 0:
+                        project_roles.append({'project': project, 'role': role})
+                    else:
+                        logger.error('could not parse entitlement=%s because no project role exists that matches the entitlement role=%s' % (entitlement, role))
+            except Exception as e:
+                logger.error('could not parse entitlement=%s because of exception=%s' % (entitlement, e))
+        user.project_roles = project_roles
+        logger.info("### assigned project_roles=%s to user=%s" % (project_roles, user))
+        user.save()
+
+    def create_user(self, claims):
+        user = super(TMSSOIDCAuthenticationBackend, self).create_user(claims)
+        logger.info('### create user=%s claims=%s' % (user, claims))
+        self._set_user_project_roles_from_claims(user, claims)
+        return user
+
+    def update_user(self, user, claims):
+        logger.info('### update user=%s claims=%s' % (user, claims))
+        self._set_user_project_roles_from_claims(user, claims)
+        return user
\ No newline at end of file
diff --git a/SAS/TMSS/backend/src/tmss/settings.py b/SAS/TMSS/backend/src/tmss/settings.py
index d7791f7e04d..5c1c7a6250b 100644
--- a/SAS/TMSS/backend/src/tmss/settings.py
+++ b/SAS/TMSS/backend/src/tmss/settings.py
@@ -221,6 +221,7 @@ REST_FRAMEWORK = {
 # AUTHENTICATION: simple LDAP, or OpenID, or both
 
 AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)
+AUTH_USER_MODEL = 'tmssapp.TMSSUser'
 
 if "TMSS_LDAPCREDENTIALS" in os.environ.keys():
     # plain LDAP
@@ -265,7 +266,7 @@ if "OIDC_RP_CLIENT_ID" in os.environ.keys():
 
     # Defaults are for using the Astron dev OIDC (Keycloak) deployment for SDC
     # !! Set OIDC_RP_CLIENT_ID and OIDC_RP_CLIENT_SECRET in environment (todo: use dbcredentials to configure as we have for LDAP?)
-    OIDC_RP_SCOPES = "openid email profile"  # todo: groups are not a standard scope, how to handle those?
+    OIDC_RP_SCOPES = "openid email profile eduperson_entitlement"
     OIDC_RP_CLIENT_ID = os.environ.get('OIDC_RP_CLIENT_ID', 'secret')  # Secret, do not put real credentials on Git
     OIDC_RP_CLIENT_SECRET = os.environ.get('OIDC_RP_CLIENT_SECRET', 'secret')  # Secret, do not put real credentials on Git
     OIDC_RP_SIGN_ALGO = os.environ.get('OIDC_RP_SIGN_ALGO', 'RS256')
@@ -276,7 +277,7 @@ if "OIDC_RP_CLIENT_ID" in os.environ.keys():
     OIDC_OP_TOKEN_ENDPOINT = os.environ.get('OIDC_OP_TOKEN_ENDPOINT', "https://sdc-dev.astron.nl/auth/realms/master/protocol/openid-connect/token")
     OIDC_OP_USER_ENDPOINT = os.environ.get('OIDC_OP_USER_ENDPOINT', "https://sdc-dev.astron.nl/auth/realms/master/protocol/openid-connect/userinfo")
 
-    AUTHENTICATION_BACKENDS += ('mozilla_django_oidc.auth.OIDCAuthenticationBackend',)
+    AUTHENTICATION_BACKENDS += ('lofar.sas.tmss.tmss.authentication_backends.TMSSOIDCAuthenticationBackend',)
     MIDDLEWARE.append('mozilla_django_oidc.middleware.SessionRefresh')
 
 if len(AUTHENTICATION_BACKENDS) == 1:
@@ -288,7 +289,6 @@ LOGIN_REDIRECT_URL_FAILURE = "/api/"
 LOGOUT_REDIRECT_URL = "https://sdc-dev.astron.nl/auth/realms/master/account/#/"   # so the user can log out of OpenID provider too
 LOGOUT_REDIRECT_URL_FAILURE = "/api/"
 
-
 # Password validation
 # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
 
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/admin.py b/SAS/TMSS/backend/src/tmss/tmssapp/admin.py
index 8c38f3f3dad..a4e0d5b36c7 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/admin.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/admin.py
@@ -1,3 +1,7 @@
 from django.contrib import admin
 
-# Register your models here.
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin
+from .models import TMSSUser
+
+admin.site.register(TMSSUser, UserAdmin)
\ No newline at end of file
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py
index fc070c79167..827022a8f7e 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/migrations/0001_initial.py
@@ -1,11 +1,14 @@
-# Generated by Django 3.0.9 on 2021-03-29 13:02
+# Generated by Django 3.0.9 on 2021-04-20 17:32
 
 from django.conf import settings
+import django.contrib.auth.models
+import django.contrib.auth.validators
 import django.contrib.postgres.fields
 import django.contrib.postgres.fields.jsonb
 import django.contrib.postgres.indexes
 from django.db import migrations, models
 import django.db.models.deletion
+import django.utils.timezone
 import lofar.sas.tmss.tmss.tmssapp.models.common
 import lofar.sas.tmss.tmss.tmssapp.models.specification
 
@@ -15,10 +18,35 @@ class Migration(migrations.Migration):
     initial = True
 
     dependencies = [
-        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ('auth', '0011_update_proxy_permissions'),
     ]
 
     operations = [
+        migrations.CreateModel(
+            name='TMSSUser',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('password', models.CharField(max_length=128, verbose_name='password')),
+                ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+                ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+                ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
+                ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
+                ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+                ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
+                ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+                ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
+                ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+                ('project_roles', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, help_text='A list of structures that contain a project name and project role')),
+            ],
+            options={
+                'verbose_name': 'user',
+                'verbose_name_plural': 'users',
+                'abstract': False,
+            },
+            managers=[
+                ('objects', django.contrib.auth.models.UserManager()),
+            ],
+        ),
         migrations.CreateModel(
             name='Algorithm',
             fields=[
@@ -687,6 +715,18 @@ class Migration(migrations.Migration):
                 ('unique_identifier', models.BigAutoField(help_text='Unique global identifier.', primary_key=True, serialize=False)),
             ],
         ),
+        migrations.CreateModel(
+            name='StationTimeline',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('station_name', models.CharField(editable=False, help_text='The LOFAR station name.', max_length=16)),
+                ('timestamp', models.DateField(editable=False, help_text='The date (YYYYMMDD).', null=True)),
+                ('sunrise_start', models.DateTimeField(help_text='Start time of the sunrise.', null=True)),
+                ('sunrise_end', models.DateTimeField(help_text='End time of the sunrise.', null=True)),
+                ('sunset_start', models.DateTimeField(help_text='Start time of the sunset.', null=True)),
+                ('sunset_end', models.DateTimeField(help_text='End time of the sunset.', null=True)),
+            ],
+        ),
         migrations.CreateModel(
             name='StationType',
             fields=[
@@ -938,17 +978,6 @@ class Migration(migrations.Migration):
                 ('second', models.ForeignKey(help_text='Second Task Blueprint to connect.', on_delete=django.db.models.deletion.CASCADE, related_name='second_scheduling_relation', to='tmssapp.TaskBlueprint')),
             ],
         ),
-        migrations.CreateModel(
-            name='StationTimeline',
-            fields=[
-                ('station_name', models.CharField(max_length=16, null=False, editable=False, help_text='The LOFAR station name.')),
-                ('timestamp', models.DateField(editable=False, null=True, help_text='The date (YYYYMMDD).')),
-                ('sunrise_start', models.DateTimeField(null=True, help_text='Start time of the sunrise.')),
-                ('sunrise_end', models.DateTimeField(null=True, help_text='End time of the sunrise.')),
-                ('sunset_start', models.DateTimeField(null=True, help_text='Start time of the sunset.')),
-                ('sunset_end', models.DateTimeField(null=True, help_text='End time of the sunset.')),
-            ],
-        ),
         migrations.AddConstraint(
             model_name='taskrelationselectiontemplate',
             constraint=models.UniqueConstraint(fields=('name', 'version'), name='taskrelationselectiontemplate_unique_name_version'),
@@ -1165,7 +1194,7 @@ class Migration(migrations.Migration):
         ),
         migrations.AddConstraint(
             model_name='stationtimeline',
-            constraint=models.UniqueConstraint(fields=('station_name', 'timestamp'),  name='unique_station_time_line'),
+            constraint=models.UniqueConstraint(fields=('station_name', 'timestamp'), name='unique_station_time_line'),
         ),
         migrations.AddConstraint(
             model_name='schedulingunittemplate',
@@ -1479,6 +1508,16 @@ class Migration(migrations.Migration):
             name='station_type',
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tmssapp.StationType'),
         ),
+        migrations.AddField(
+            model_name='tmssuser',
+            name='groups',
+            field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups'),
+        ),
+        migrations.AddField(
+            model_name='tmssuser',
+            name='user_permissions',
+            field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
+        ),
         migrations.AddConstraint(
             model_name='tasktemplate',
             constraint=models.UniqueConstraint(fields=('name', 'version'), name='tasktemplate_unique_name_version'),
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py
index 8cdbf52b01e..a0fecb1c75b 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/permissions.py
@@ -10,6 +10,18 @@ from django.db.models import ManyToManyField
 from enum import Enum
 
 from rest_framework.permissions import DjangoModelPermissions
+from django.contrib.auth.models import AbstractUser
+from django.contrib.postgres.fields import JSONField
+
+class TMSSUser(AbstractUser):
+    """
+    A custom user model that allows for additional information on the user like project roles.
+    """
+    # todo: The project roles field feels very free-form at the moment.
+    #  Maybe this can be modeled better somehow, with references to the ProjectRole table?
+    #  Otherwise, we should probably come up with a schema to makes sure things are consistent.
+    #  Also, I'd suggest to simply map project name to a list of roles instead of the structure that is used here.
+    project_roles = JSONField(null=True, blank=True, help_text='A list of structures that contain a project name and project role')  # e.g. [{'project': 'high', 'role': 'PI'}, {'project': 'high', 'role': 'Friend of Project'}]
 
 
 #
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
index 9535b3c3d97..231c5586d25 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/models/scheduling.py
@@ -11,7 +11,7 @@ from datetime import datetime, timedelta
 from django.db.models import Model, ForeignKey, OneToOneField, CharField, DateTimeField, BooleanField, IntegerField, BigIntegerField, \
     ManyToManyField, CASCADE, SET_NULL, PROTECT, QuerySet, BigAutoField, UniqueConstraint
 from django.contrib.postgres.fields import ArrayField, JSONField
-from django.contrib.auth.models import User
+from .permissions import TMSSUser as User
 from .common import AbstractChoice, BasicCommon, Template, NamedCommon, annotate_validate_add_defaults_to_doc_using_template
 from enum import Enum
 from django.db.models.expressions import RawSQL
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
index 684280c9ad3..999d7111dfb 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/populate.py
@@ -28,7 +28,9 @@ from lofar.sas.tmss.tmss.tmssapp.models.permissions import *
 from lofar.sas.tmss.tmss.tmssapp.conversions import timestamps_and_stations_to_sun_rise_and_set, get_all_stations
 from lofar.common import isTestEnvironment, isDevelopmentEnvironment
 from concurrent.futures import ThreadPoolExecutor
-from django.contrib.auth.models import User, Group, Permission
+from django.contrib.auth.models import Group, Permission
+from django.contrib.auth import get_user_model
+User = get_user_model()
 from django.contrib.contenttypes.models import ContentType
 from django.db.utils import IntegrityError
 
@@ -593,8 +595,8 @@ def assign_system_permissions():
     tmss_admin_group.permissions.add(perm)
 
     # User model permissions
-    ct = ContentType.objects.get(model='user')
-    perm = Permission.objects.get(codename='add_user')
+    ct = ContentType.objects.get(model='tmssuser')
+    perm = Permission.objects.get(codename='add_tmssuser')
     to_observer_group.permissions.add(perm)
     sdco_support_group.permissions.add(perm)
     tmss_maintainer_group.permissions.add(perm)
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py
index fc23e9e9424..0b5255b7a76 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/serializers/specification.py
@@ -7,7 +7,7 @@ from .. import models
 from .scheduling import SubtaskSerializer
 from .common import FloatDurationField, RelationalHyperlinkedModelSerializer, AbstractTemplateSerializer, DynamicRelationalHyperlinkedModelSerializer
 from .widgets import JSONEditorField
-from django.contrib.auth.models import User
+from ..models import TMSSUser as User
 
 # This is required for keeping a user reference as ForeignKey in other models
 # (I think so that the HyperlinkedModelSerializer can generate a URI)
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py
index 34fa46921a6..2fbb882a21d 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/permissions.py
@@ -23,25 +23,16 @@ import urllib.parse
 
 def get_project_roles_for_user(user):
 
-        # todo: this set of project/role pairs needs to be provided by the OIDC federation and will probably enter TMSS
-        #  as a property on request.user. Create this for the requesting user in the following format:
-        # project_roles = ({'project': 'high', 'role': 'PI'},  # demo data
-        #                        {'project': 'low', 'role': 'Friend of Project'},  # demo data
-        #                        {'project': 'test_user_is_pi', 'role': 'PI'},  # for unittests
-        #                        {'project': 'test_user_is_contact', 'role': 'Contact Author'})  # for unittests
-        project_roles = ()
-
         # todo: stupid hack to make test pass, because we so far have failed mocking this function out successfully.
         #  Should not hit production!
         try:
             if user == models.User.objects.get(username='paulus'):
                 return ({'project': 'test_user_is_shared_support', 'role': 'shared_support_user'},
                         {'project': 'test_user_is_contact', 'role': 'contact_author'})
-                        #{'project': 'high', 'role': 'shared_support_user'})
         except:
             pass
 
-        return project_roles
+        return tuple(user.project_roles)
 
 
 def get_project_roles_with_permission(permission_name, method='GET'):
@@ -55,6 +46,7 @@ def get_project_roles_with_permission(permission_name, method='GET'):
         logger.error("This action was configured to enforce project permissions, but no project permission with name '%s' has been defined." % permission_name)
         return []
 
+
 class IsProjectMember(drf_permissions.DjangoObjectPermissions):
     """
     Object-level permission to only allow users of the related project to access it.
diff --git a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py
index 49ddf7a0971..a71c0236ec6 100644
--- a/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py
+++ b/SAS/TMSS/backend/src/tmss/tmssapp/viewsets/specification.py
@@ -5,7 +5,8 @@ This file contains the viewsets (based on the elsewhere defined data models and
 from django.shortcuts import get_object_or_404, get_list_or_404, render
 
 from django.http import JsonResponse
-from django.contrib.auth.models import User
+from django.contrib.auth import get_user_model
+User = get_user_model()
 from django_filters import rest_framework as filters
 import django_property_filter as property_filters
 from rest_framework.viewsets import ReadOnlyModelViewSet
diff --git a/SAS/TMSS/backend/test/t_permissions.py b/SAS/TMSS/backend/test/t_permissions.py
index e79c126f907..5b7bfa469b6 100755
--- a/SAS/TMSS/backend/test/t_permissions.py
+++ b/SAS/TMSS/backend/test/t_permissions.py
@@ -51,8 +51,6 @@ from lofar.sas.tmss.test.tmss_test_data_rest import TMSSRESTTestDataCreator
 
 from django.test import TestCase
 
-from django.contrib.auth.models import User, Group, Permission
-
 class ProjectPermissionTestCase(TestCase):
     # This tests that the project permissions are enforced in light of the project roles that are externally provided
     # for the user through the user admin. This test does not rely on the project permissions as defined in the system,
diff --git a/SAS/TMSS/backend/test/t_permissions_system_roles.py b/SAS/TMSS/backend/test/t_permissions_system_roles.py
index 5d05682bec0..33599b96af1 100755
--- a/SAS/TMSS/backend/test/t_permissions_system_roles.py
+++ b/SAS/TMSS/backend/test/t_permissions_system_roles.py
@@ -66,7 +66,9 @@ test_data_creator = TMSSRESTTestDataCreator(BASE_URL, AUTH)
 
 from lofar.sas.tmss.tmss.tmssapp.viewsets.permissions import TMSSPermissions
 from lofar.sas.tmss.tmss.tmssapp.viewsets.scheduling import SubtaskViewSet
-from django.contrib.auth.models import User, Group
+from django.contrib.auth.models import Group
+from django.contrib.auth import get_user_model
+User = get_user_model()
 
 
 class SystemPermissionTestCase(unittest.TestCase):
diff --git a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py
index f0c8c331dc9..f93eb08abbe 100755
--- a/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py
+++ b/SAS/TMSS/backend/test/t_tmssapp_specification_REST_API.py
@@ -43,7 +43,9 @@ from lofar.sas.tmss.test.tmss_test_environment_unittest_setup import *
 from lofar.sas.tmss.test.tmss_test_data_django_models import *
 from lofar.sas.tmss.tmss.tmssapp import models
 from lofar.sas.tmss.test.test_utils import assertUrlList
-from django.contrib.auth.models import User, Group, Permission
+from django.contrib.auth.models import Group, Permission
+from django.contrib.auth import get_user_model
+User = get_user_model()
 
 # import and setup test data creator
 from lofar.sas.tmss.test.tmss_test_data_rest import TMSSRESTTestDataCreator
diff --git a/SAS/TMSS/backend/test/test_utils.py b/SAS/TMSS/backend/test/test_utils.py
index 88c46e4780d..f05cde5b4a6 100644
--- a/SAS/TMSS/backend/test/test_utils.py
+++ b/SAS/TMSS/backend/test/test_utils.py
@@ -373,7 +373,8 @@ class TMSSTestEnvironment:
         # now that the ldap and django server are running, and the django set has been done,
         # we can announce our test user as superuser, so the test user can do anythin via the API.
         # (there are also other tests, using other (on the fly created) users with restricted permissions, which is fine but not part of this generic setup.
-        from django.contrib.auth.models import User
+        from django.contrib.auth import get_user_model
+        User = get_user_model()
         user, _ = User.objects.get_or_create(username=self.ldap_server.dbcreds.user)
         user.is_superuser = True
         user.save()
-- 
GitLab