has_perms not working for Group based permissions

Hi I am new to django and not sure if posting it here in correct.

I am using bultin has_perms ot perm to check for the permissions.

I have made a set of groups with different permissions (like to chck default ones add, change , update , view) and have assigned the users to those group

When I check the permission using following it always redirects user to 403 page :


if not (request.user.has_perm('ClientManagementSystem.view_clientinformation')):
              raise PermissionDenied("You are not authorised")

But when I do something like this, it works:


 if not (request.user.has_group_perm('ClientManagementSystem.view_clientinformation')):
                raise PermissionDenied("You are not authorised")

My has_group_perm function:


def has_group_perm(self, perm):
        if not self.is_active:
            return False
        return perm in self.get_group_permissions()

Am i doing somthing wrong ?

Welcome @samdam82 !

It’s not clear from your description what the situation is with the user(s) you are testing.

What is the data regarding the user, group, group permission, and group membership that you are testing? What results are you getting, and what do you expect to have happen?

Are you using a custom user model, or are you using the standard django.auth.User?

Note 1: The user.has_perm check includes checking for group permissions. That test passes if the user has that permission directly assigned to them or if that permission is assigned to a group to which they are assigned.

Note 2: The standard model auth backend already checks for the user being active. You don’t need to include a test for that.

In other words, you don’t need to create special tests for permissions. You only need to interrogate whether the user has the permission.

Hey Ken Thank you so much for the reply.

So what I want to do is redirect the user to login_url if not logged.
And for if I do not have permission I want to redirect it to Permission Deinied page.

I was using PermissionMixin with login mixin but was being redirected to 403 page instead of Login url (for logged out users), so that is why for now I am using permission check in dispatch manual adding it

I am doing checks for detail, list, create and update views (for list of records of clients registered) and using permission mixin in all of them.

I have created a custom user model

class CustomUser(AbstractUser,PermissionsMixin):

    email = models.EmailField(unique=True,null=False) 
    ............................

Note 1. user.has_perm this is always returns false, even if user has permissions (I am using group based permissions not direct ones)


class Client_List(LoginRequiredMixin,ListView):
      model = ClientInformation
      template_name = 'ClientManagementSystem/dashboard.html'
      context_object_name = 'client_list'


      login_url = '/UserManagementSystem/user-employee-login/'
      # permission_required = 'ClientManagementSystem.view_clientinformation'
      # raise_exception = True

      def dispatch(self, request, *args, **kwargs):
            
            if not request.user.is_authenticated:
                  return redirect('user-employee-login')
            
            # returns false always
            # if not (request.user.has_perm('ClientManagementSystem.add_clientinformation') or request.user.is_superuser):
            #     raise PermissionDenied("You are not authorised")

            # works
            if not (request.user.has_group_perm('ClientManagementSystem.view_clientinformation') or request.user.is_superuser):
                raise PermissionDenied("You are not authorised")
         
            
            return super().dispatch(request, *args, **kwargs)

That’s because of this:

The purpose of this setting is to cause the view to throw an exception for a non-logged-in user. See Using the Django authentication system | Django documentation | Django

Thank you , tried it before but was getting redirected to login page even for logged in user , so added it in every view with custom function to make sure user get redirected accordingly view, but now I checked it is working all along :sweat_smile:

Now about group_permission and Direct permission, I am not able to figure it out why has_perm not working even if my user has the permssions ( same permission name works in group permission check but not with has_perm or has_perms or permission_required = ‘ClientManagementSystem.view_clientinformation’ of permissionmixin)

For clarity, you’re saying that with:

A user can access this view if a group they belong to is granted this permission, but they cannot if the permission is granted directly to the user. Is that what you’re saying here?

Side note: You show that you have -

The AbstractUser already includes PermissionsMixin in its class definition, you should not be repeating it here. (I don’t think this would have anything to do with this issue, but it should be cleaned up regardless.)

If I use has_perm or Permission Mixin , the user gets redirected to 403 always even when user have the permissions (through groups).

so

class Client_List(LoginRequiredMixin,ListView):
      permission_required = 'ClientManagementSystem.view_clientinformation'

or

if not (request.user.has_perm('ClientManagementSystem.add_clientinformation') or request.user.is_superuser):
            raise PermissionDenied("You are not authorised")

always redirects

but when I use this

 if not (request.user.has_group_perm('ClientManagementSystem.view_clientinformation')):
                raise PermissionDenied("You are not authorised")

it works without any issues

Have you fixed your CustomUser model and retried this?

Yes only super user or permission group check function works (tried with permission required mixin in my view and , also with my has_perm check

Please post the current version of the view you’re trying to run, your current version of your CustomUser model, and some visible evidence that the instance of the user you are trying to use has the desired permission assigned to it.

Also, let’s stay focused on one specific issue - the 403s that you are getting when you’re using the PermissionsRequiredMixin. Talking about these other issues is only confusing the conversation.

Sure my current view (I also have other but they are similar) :-

class Client_List(LoginRequiredMixin,PermissionRequiredMixin,ListView):
      model = ClientInformation
      template_name = 'ClientManagementSystem/dashboard.html'
      context_object_name = 'client_list'


      login_url = '/UserManagementSystem/user-employee-login/'
      permission_required = 'ClientManagementSystem.view_clientinformation'
      # raise_exception = True

      def dispatch(self, request, *args, **kwargs):
           
            print(request.user.get_all_permissions())
            print(request.user.has_perm("ClientManagementSystem.view_clientinformation"))
            return super().dispatch(request, *args, **kwargs)


Custom User: -

class CustomUser(AbstractUser): 

    email = models.EmailField(unique=True,null=False) 
    phone_number = models.CharField(max_length=20, blank=True, null=True)
    employee_id = models.CharField(max_length=20, blank=True, null=True)
    client_id = models.CharField(max_length=20, blank=True, null=True) 
    role = models.ForeignKey('Role', on_delete=models.SET_NULL, null=True, blank=True)
    module_access = models.ManyToManyField('Module', blank=True)
    is_employee = models.BooleanField(default=False)
    is_verified = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']
    objects = CustomUserManager()
    username_validator = AbstractUser.username_validator
    USER_TYPE_CHOICES = [
        ('EMPLOYEE', 'Employee'),
        ('CLIENT', 'Client'),
        ('ADMIN', 'Admin'),
    ]
    user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES)

    def has_group_perm(self, perm):
        return perm in self.get_group_permissions()

    def __str__(self):
        return f"{self.username} ({self.role.name if self.role else 'No Role'})"

Output for : " print(user.get_all_permissions()) " :

{
    'UserManagementSystem.add_customuser',
    'ClientManagementSystem.view_health_record',
    'UserManagementSystem.view_clientprofile',
    'ClientManagementSystem.view_clientinformation',
    'ClientManagementSystem.change_dept_code',
    'ClientManagementSystem.add_consultation_diagnosis',
    ......................................... and many more


}

Ouput for print(request.user.has_perm(“ClientManagementSystem.view_clientinformation”)) :

False

Can’t add images

Looking at other possible issues - what version of Django and Python are you using? What third-party packages do you have installed? (What versions are they?)

Python version : Python 3.12.3

Packages installed:

Package                   Version
------------------------- --------------
absl-py                   2.1.0
annotated-types           0.7.0
anyio                     4.6.2.post1
argon2-cffi               23.1.0
argon2-cffi-bindings      21.2.0
arrow                     1.3.0
asgiref                   3.8.1
asttokens                 3.0.0
astunparse                1.6.3
async-lru                 2.0.4
attrs                     24.2.0
autobahn                  24.4.2
Automat                   25.4.16
babel                     2.16.0
beautifulsoup4            4.12.3
binaryornot               0.4.4
bleach                    6.2.0
blis                      1.0.1
catalogue                 2.0.10
certifi                   2024.8.30
cffi                      1.17.1
channels                  4.2.2
channels_redis            4.2.1
chardet                   5.2.0
charset-normalizer        3.4.0
click                     8.1.7
cloudpathlib              0.20.0
comm                      0.2.2
confection                0.1.5
constantly                23.10.4
contourpy                 1.3.1
cookiecutter              2.6.0
crispy-bootstrap5         2024.10
cryptography              44.0.2
cycler                    0.12.1
cymem                     2.0.10
daphne                    4.1.2
debugpy                   1.8.9
decorator                 5.1.1
defusedxml                0.7.1
Django                    5.1.3
django-appconf            1.1.0
django-browser-reload     1.18.0
django-compressor         4.5.1
django-crispy-forms       2.3
django-tailwind           4.0.1
django-widget-tweaks      1.5.0
en_core_web_sm            3.8.0
executing                 2.1.0
fastjsonschema            2.21.0
flatbuffers               24.3.25
fonttools                 4.55.0
fqdn                      1.5.1
gast                      0.6.0
google-pasta              0.2.0
grpcio                    1.68.0
h11                       0.14.0
h5py                      3.12.1
httpcore                  1.0.7
httpx                     0.28.0
hyperlink                 21.0.0
idna                      3.10
incremental               24.7.2
ipykernel                 6.29.5
ipython                   8.30.0
ipywidgets                8.1.5
isoduration               20.11.0
jedi                      0.19.2
Jinja2                    3.1.4
joblib                    1.4.2
json5                     0.10.0
jsonpointer               3.0.0
jsonschema                4.23.0
jsonschema-specifications 2024.10.1
jupyter                   1.1.1
jupyter_client            8.6.3
jupyter-console           6.6.3
jupyter_core              5.7.2
jupyter-events            0.10.0
jupyter-lsp               2.2.5
jupyter_server            2.14.2
jupyter_server_terminals  0.5.3
jupyterlab                4.2.6
jupyterlab_pygments       0.3.0
jupyterlab_server         2.27.3
jupyterlab_widgets        3.0.13
keras                     3.7.0
kiwisolver                1.4.7
langcodes                 3.5.0
language_data             1.3.0
libclang                  18.1.1
marisa-trie               1.2.1
Markdown                  3.7
markdown-it-py            3.0.0
MarkupSafe                3.0.2
matplotlib                3.9.3
matplotlib-inline         0.1.7
mdurl                     0.1.2
mistune                   3.0.2
ml-dtypes                 0.4.1
msgpack                   1.1.0
murmurhash                1.0.11
mysqlclient               2.2.6
namex                     0.0.8
nbclient                  0.10.1
nbconvert                 7.16.4
nbformat                  5.10.4
nest-asyncio              1.6.0
notebook                  7.2.2
notebook_shim             0.2.4
numpy                     2.0.2
nvidia-cublas-cu12        12.5.3.2
nvidia-cuda-cupti-cu12    12.5.82
nvidia-cuda-nvcc-cu12     12.5.82
nvidia-cuda-nvrtc-cu12    12.5.82
nvidia-cuda-runtime-cu12  12.5.82
nvidia-cudnn-cu12         9.3.0.75
nvidia-cufft-cu12         11.2.3.61
nvidia-curand-cu12        10.3.6.82
nvidia-cusolver-cu12      11.6.3.83
nvidia-cusparse-cu12      12.5.1.3
nvidia-nccl-cu12          2.21.5
nvidia-nvjitlink-cu12     12.5.82
opencv-python             4.10.0.84
opt_einsum                3.4.0
optree                    0.13.1
overrides                 7.7.0
packaging                 24.2
pandas                    2.2.3
pandocfilters             1.5.1
parso                     0.8.4
pexpect                   4.9.0
pillow                    11.0.0
pip                       25.1.1
platformdirs              4.3.6
preshed                   3.0.9
prometheus_client         0.21.0
prompt_toolkit            3.0.48
protobuf                  5.29.0
psutil                    6.1.0
ptyprocess                0.7.0
pure_eval                 0.2.3
pyasn1                    0.6.1
pyasn1_modules            0.4.2
pycparser                 2.22
pydantic                  2.10.2
pydantic_core             2.27.1
Pygments                  2.18.0
pyOpenSSL                 25.0.0
pyparsing                 3.2.0
python-dateutil           2.9.0.post0
python-json-logger        2.0.7
python-slugify            8.0.4
pytz                      2024.2
PyYAML                    6.0.2
pyzmq                     26.2.0
rcssmin                   1.1.2
redis                     5.2.1
referencing               0.35.1
requests                  2.32.3
rfc3339-validator         0.1.4
rfc3986-validator         0.1.1
rich                      13.9.4
rjsmin                    1.2.2
rpds-py                   0.21.0
scikit-learn              1.5.2
scipy                     1.14.1
Send2Trash                1.8.3
service-identity          24.2.0
setuptools                75.6.0
shellingham               1.5.4
six                       1.16.0
smart-open                7.0.5
sniffio                   1.3.1
soupsieve                 2.6
spacy                     3.8.2
spacy-legacy              3.0.12
spacy-loggers             1.0.5
sqlparse                  0.5.2
srsly                     2.4.8
stack-data                0.6.3
tensorboard               2.18.0
tensorboard-data-server   0.7.2
tensorflow                2.18.0
termcolor                 2.5.0
terminado                 0.18.1
text-unidecode            1.3
thinc                     8.3.2
threadpoolctl             3.5.0
tinycss2                  1.4.0
tornado                   6.4.2
tqdm                      4.67.1
traitlets                 5.14.3
Twisted                   24.11.0
txaio                     23.1.1
typer                     0.14.0
types-python-dateutil     2.9.0.20241003
typing_extensions         4.12.2
tzdata                    2024.2
uri-template              1.3.0
urllib3                   2.2.3
uuid7                     0.1.0
wasabi                    1.1.3
wcwidth                   0.2.13
weasel                    0.4.1
webcolors                 24.11.1
webencodings              0.5.1
websocket-client          1.8.0
Werkzeug                  3.1.3
wheel                     0.45.1
widgetsnbextension        4.0.13
wrapt                     1.17.0
zope.interface            7.2

By looking at the code , is there anything wrong ? :face_with_monocle:

I don’t see anything wrong with the code posted so far - that’s why I’m looking at the data, and other possible configuration issues.

From your settings file, please post your INSTALLED_APPS, MIDDLEWARE, and AUTHENTICATION_BACKENDS settings.

MIDDLEWARE :

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    "django_browser_reload.middleware.BrowserReloadMiddleware",
]

INSTALELD_APPS:


INSTALLED_APPS = [
    'daphne',
     'channels',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]


EXTERNAL_APPS = [
    'crispy_forms',
    'compressor',
    'django_browser_reload',
    'crispy_bootstrap5',
    'comm_missl.apps.CommMisslConfig',
    'ClientManagementSystem.apps.ClientmanagementsystemConfig',
    'ImagingRecordKeepingSystem.apps.ImagingrecordkeepingsystemConfig',
    'LaboratoryManagementSystem.apps.LaboratorymanagementsystemConfig',
    'PharmacyManagementSystem.apps.PharmacymanagementsystemConfig',
    'InventoryManagementSystem.apps.InventorymanagementsystemConfig',
    'HumanResourceManagementSystem.apps.HumanresourcemanagementsystemConfig',
    'AppointmentSchedulingSystem.apps.AppointmentschedulingsystemConfig',
    'TelemedicineSystem.apps.TelemedicinesystemConfig',
    'BillingManagementSystem.apps.BillingmanagementsystemConfig',
    'UserManagementSystem.apps.UsermanagementsystemConfig',


    
]

AUTHENTICATION_BACKENDS:

# But it is commented , but It is used bydefault Right?
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

I’m still not seeing anything wrong, so I need to ask for more information.

What is commented? What you’re showing here for that setting isn’t commented.

(But yes, if AUTHENTICATION_BACKENDS isn’t supplied, what you’re showing is the default in global_settings.py.)

I’m assuming you have something in your project that combines INSTALLED_APPS with EXTERNAL_APPS?

Also, please post your AUTH_USER_MODEL setting.

Also, I see where you’re using a custom model manager for your CustomUser, please post it here.

Customer User model manager:

class CustomUserManager(BaseUserManager):
    def create_user(self, email, username, password=None, **extra_fields):
        if not email:
            raise ValueError("Users must have an email address")
        if not username:
            raise ValueError("Users must have a username")
        email = self.normalize_email(email)
        user = self.model(email=email, username=username, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, username, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError("Superuser must have is_staff=True.")
        if extra_fields.get('is_superuser') is not True:
            raise ValueError("Superuser must have is_superuser=True.")

        return self.create_user(email, username, password, **extra_fields)

Sorry, I mean the I added it and tried, but same thing happened to So Commented it out (because I do not have any custom auth backends so no need to add it)

Auth User model :

AUTH_USER_MODEL = 'UserManagementSystem.CustomUser'

My settings .py:

"""
Django settings for ArogyaX project.

Generated by 'django-admin startproject' using Django 5.0.4.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '-------------------------------------'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = [
    '*'
]

SITE_DOMAIN = 'http://127.0.0.1:8000'

# DEBUG = True
# ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'daphne',
     'channels',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]


EXTERNAL_APPS = [
    'crispy_forms',
    'compressor',
    'django_browser_reload',
    'comm_missl.apps.CommMisslConfig',
    'ClientManagementSystem.apps.ClientmanagementsystemConfig',
    'ImagingRecordKeepingSystem.apps.ImagingrecordkeepingsystemConfig',
    'LaboratoryManagementSystem.apps.LaboratorymanagementsystemConfig',
    'PharmacyManagementSystem.apps.PharmacymanagementsystemConfig',
    'InventoryManagementSystem.apps.InventorymanagementsystemConfig',
    'HumanResourceManagementSystem.apps.HumanresourcemanagementsystemConfig',
    'AppointmentSchedulingSystem.apps.AppointmentschedulingsystemConfig',
    'TelemedicineSystem.apps.TelemedicinesystemConfig',
    'BillingManagementSystem.apps.BillingmanagementsystemConfig',
    'UserManagementSystem.apps.UsermanagementsystemConfig',


    
]


NPM_BIN_PATH = '/home/samdam82/.nvm/versions/node/v22.15.0/bin/npm'


INSTALLED_APPS += EXTERNAL_APPS



INTERNAL_IPS = [
    '127.0.0.1',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    "django_browser_reload.middleware.BrowserReloadMiddleware",
]



TEMPLATES_DIR  = os.path.join(BASE_DIR,'templates')

ROOT_URLCONF = 'ArogyaX.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR/ "templates"],
        # 'DIRS': [TEMPLATES_DIR], # for template dir on root
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'ArogyaX.context_processors.navbar_items', 
                'ArogyaX.context_processors.client_navbar_items', 
            ],
        },
    },
]

# WSGI_APPLICATION = 'ArogyaX.wsgi.application'
ASGI_APPLICATION = 'ArogyaX.asgi.application'



DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'arogyax',
        'USER':'root',
        'PASSWORD':'----------',
        'PORT':3306,
        'HOST': '127.0.0.1',
    }
}



# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
]

# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

# USE_TZ = False
USE_TZ = True




DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'


DATE_INPUT_FORMATS = ['%d/%m/%Y']

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

CRISPY_TEMPLATE_PACK = "bootstrap5"


# we can also add multiple path like [STATIC_DIR,"Some_path1",Some_path2]
STATIC_URL = '/static/' # what will be url path for static files to show on template side.
# STATIC_URL Needed for both root static folder (using static folder or root directory) and app static folder (static folder inside app)
STATICFILES_DIRS = [BASE_DIR / 'theme' / 'static']


AUTH_USER_MODEL = 'UserManagementSystem.CustomUser'



MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')




CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

COMPRESS_ROOT = BASE_DIR / 'static'

COMPRESS_ENABLED = True

# STATICFILES_FINDERS = ('compressor.finders.CompressorFinder',)

STATICFILES_FINDERS = (
    "django.contrib.staticfiles.finders.FileSystemFinder", # for django builtin static files like for admin etc
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
    "compressor.finders.CompressorFinder",
)



EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = '00000000000000'
EMAIL_PORT = 465  # Integer, not string
EMAIL_USE_SSL = True  # Use SSL, not TLS
EMAIL_USE_TLS = False
EMAIL_HOST_USER = '----------'
EMAIL_HOST_PASSWORD = '----------'


PASSWORD_RESET_TIMEOUT = 3600 * 24 # for one day



From the information you’ve provided, I am currently unable to recreate the symptoms being described here.

I think the first thing I’d do would be to make sure this user is logged out, and delete all the session data from the database, then try again.

After that, I think the next thing I’d do would be to examine the actual User, Permission, and ContentType data in the database. I’d check all the fields of all the appropriate rows to make sure the data is internally consistent.

I’d probably also write some queries in the ORM to try and verify whether the functions are returning the expected results.

The other thing I’d think about doing would be to create a completely new project, using this existing database and none of the additional apps. I’d then repeat the permission API tests.