Repository: goauthentik/authentik
Pull request: core: remove `OldAuthenticatedSession` content type
Fixes #14437

Problem statement:
When adding Can view Authenticated Session as a global permission to a role, authentik returns 405
**Describe the bug**
When adding Can view Authenticated Session as a global permission to a role, authentik returns 405

**To Reproduce**
Steps to reproduce the behavior:

1. Create a role
2. Go to the roles settings
3. Add global permission for Can view Authenticated Session
4. Response returned an error code in GUI, 405 in console

**Expected behavior**
Permission to be successfully added.

**Logs**
Output of docker-compose logs or kubectl logs respectively

```json
{"domain_url": null, "event": "Task started", "level": "info", "logger": "authentik.root.celery", "pid": 141119, "schema_name": "public", "task_id": "fbf069ab-67cf-4ab0-8a4b-e0f4afdda981", "task_name": "event_trigger_handler", "timestamp": "2025-05-09T05:16:41.314635"}
{"domain_url": null, "event": "Task finished", "level": "info", "logger": "authentik.root.celery", "pid": 141119, "schema_name": "public", "state": "SUCCESS", "task_id": "fbf069ab67cf4ab08a4be0f4afdda981", "task_name": "event_trigger_handler", "timestamp": "2025-05-09T05:16:41.360221
```

**Version and Deployment (please complete the following information):**

-   authentik version: 2025.4.0
-   Deployment: docker-compose

Hints from issue discussion:
I can't seem to replicate this on my test deployments. Could you please provide the whole request/response pair so that we can investigate further?
```
Traceback (most recent call last):
  File "/ak-root/.venv/lib/python3.12/site-packages/asgiref/sync.py", line 518, in thread_handler
    raise exc_info[1]
  File "/ak-root/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 253, in _get_response_async
    response = await wrapped_callback(
               ^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/asgiref/sync.py", line 468, in __call__
    ret = await asyncio.shield(exec_coro)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/asgiref/current_thread_executor.py", line 40, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/asgiref/sync.py", line 522, in thread_handler
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/django/views/decorators/csrf.py", line 65, in _view_wrapper
    return view_func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py", line 515, in dispatch
    response = self.handle_exception(exc)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py", line 475, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py", line 486, in raise_uncaught_exception
    raise exc
  File "/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py", line 512, in dispatch
    response = handler(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/rbac/decorators.py", line 39, in wrapper
    return func(self, request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/authentik/rbac/api/rbac_assigned_by_roles.py", line 105, in assign
    assigned_perm = assign_perm(perm, role.group, data.validated_data["model_instance"])
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/guardian/shortcuts.py", line 95, in assign_perm
    perm = Permission.objects.get(content_type__app_label=app_label,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/django/db/models/manager.py", line 87, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/ak-root/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 652, in get
    raise self.model.MultipleObjectsReturned(
django.contrib.auth.models.MultipleObjectsReturned: get() returned more than one Permission -- it returned 2!

Show less
Context
{
    "message": "Traceback (most recent call last):\n  File \"/ak-root/.venv/lib/python3.12/site-packages/asgiref/sync.py\", line 518, in thread_handler\n    raise exc_info[1]\n  File \"/ak-root/.venv/lib/python3.12/site-packages/django/core/handlers/base.py\", line 253, in _get_response_async\n    response = await wrapped_callback(\n               ^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/asgiref/sync.py\", line 468, in __call__\n    ret = await asyncio.shield(exec_coro)\n          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/asgiref/current_thread_executor.py\", line 40, in run\n    result = self.fn(*self.args, **self.kwargs)\n             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/asgiref/sync.py\", line 522, in thread_handler\n    return func(*args, **kwargs)\n           ^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/django/views/decorators/csrf.py\", line 65, in _view_wrapper\n    return view_func(request, *args, **kwargs)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/rest_framework/viewsets.py\", line 125, in view\n    return self.dispatch(request, *args, **kwargs)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py\", line 515, in dispatch\n    response = self.handle_exception(exc)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py\", line 475, in handle_exception\n    self.raise_uncaught_exception(exc)\n  File \"/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py\", line 486, in raise_uncaught_exception\n    raise exc\n  File \"/ak-root/.venv/lib/python3.12/site-packages/rest_framework/views.py\", line 512, in dispatch\n    response = handler(request, *args, **kwargs)\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/authentik/rbac/decorators.py\", line 39, in wrapper\n    return func(self, request, *args, **kwargs)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/authentik/rbac/api/rbac_assigned_by_roles.py\", line 105, in assign\n    assigned_perm = assign_perm(perm, role.group, data.validated_data[\"model_instance\"])\n                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/guardian/shortcuts.py\", line 95, in assign_perm\n    perm = Permission.objects.get(content_type__app_label=app_label,\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/django/db/models/manager.py\", line 87, in manager_method\n    return getattr(self.get_queryset(), name)(*args, **kwargs)\n           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/ak-root/.venv/lib/python3.12/site-packages/django/db/models/query.py\", line 652, in get\n    raise self.model.MultipleObjectsReturned(\ndjango.contrib.auth.models.MultipleObjectsReturned: get() returned more than one Permission -- it returned 2!",
    "http_request": {
        "args": {},
        "path": "/api/v3/rbac/permissions/assigned_by_roles/ea2c7839-f3b8-4403-b0b3-a6630369f9b2/assign/",
        "method": "POST",
        "request_id": "9344264a7eb4453ab6e3a1c821fec252",
        "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
    }
}
User
{
    "pk": 26,
    "email": "somebody@somewhere.com",
    "username": "somebody"
}
```

Commit message: remove stale content types in `repair_permissions`

Co-authored-by: Jens Langhammer <jens@goauthentik.io>

Patch to review:
diff --git a/authentik/core/management/commands/repair_permissions.py b/authentik/core/management/commands/repair_permissions.py
index 3dfa9b8d7d07..ebf2e29874a8 100644
--- a/authentik/core/management/commands/repair_permissions.py
+++ b/authentik/core/management/commands/repair_permissions.py
@@ -2,6 +2,7 @@
 
 from django.apps import apps
 from django.contrib.auth.management import create_permissions
+from django.core.management import call_command
 from django.core.management.base import BaseCommand, no_translations
 from guardian.management import create_anonymous_user
 
@@ -14,6 +15,11 @@ class Command(BaseCommand):
     @no_translations
     def handle(self, *args, **options):
         """Check permissions for all apps"""
+
+        # Remove potential lingering old permissions
+        # See https://code.djangoproject.com/ticket/28417
+        call_command("remove_stale_contenttypes", "--no-input")
+
         for tenant in Tenant.objects.filter(ready=True):
             with tenant:
                 for app in apps.get_app_configs():
diff --git a/authentik/core/migrations/0048_delete_oldauthenticatedsession_content_type.py b/authentik/core/migrations/0048_delete_oldauthenticatedsession_content_type.py
new file mode 100644
index 000000000000..d79f14a74a80
--- /dev/null
+++ b/authentik/core/migrations/0048_delete_oldauthenticatedsession_content_type.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.1.9 on 2025-05-14 11:15
+
+from django.apps.registry import Apps
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+
+
+def remove_old_authenticated_session_content_type(
+    apps: Apps, schema_editor: BaseDatabaseSchemaEditor
+):
+    db_alias = schema_editor.connection.alias
+    ContentType = apps.get_model("contenttypes", "ContentType")
+
+    ContentType.objects.using(db_alias).filter(model="oldauthenticatedsession").delete()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("authentik_core", "0047_delete_oldauthenticatedsession"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            code=remove_old_authenticated_session_content_type,
+        ),
+    ]


Code review comments:
diff_hunk: @@ -14,6 +15,11 @@ class Command(BaseCommand):
     @no_translations
     def handle(self, *args, **options):
         """Check permissions for all apps"""
+
+        # Remove potential lingering old permissions
+        # See https://code.djangoproject.com/ticket/28417
+        call_command("remove_stale_contenttypes", "--no-input")
path: authentik/core/management/commands/repair_permissions.py
text: @user1:
Instead of using `call_command` this should be directly calling the command from `from django.contrib.contenttypes.management.commands import remove_stale_contenttypes`, also needs to be done in the `with tenant:` context manager

@author:
Offline resolution:

- `with tenant`: you're correct
- `call_command`: we should use it instead of writing all the boilerplate