Conversation
UserViewSet annotates every user with is_superuser using one EXISTS with an OR across two join paths: direct membership of a superuser group, and membership of a descendant group through authentik_core_groupancestry. PostgreSQL can't convert a subquery whose OR mixes two correlated conditions into a hashed subplan, so it runs once per user row, including rows skipped by OFFSET and inside DISTINCT/COUNT wrappers. Its join plan also depends on the ancestry materialized view's statistics: once a first group hierarchy exists and the view's stats are stale, the planner can pick a merge join that scans every group for every user. Use two separate EXISTS subqueries combined with OR instead. Each one is correlated only by user id equality, so PostgreSQL can run it once per query as a hashed subplan. The result is unchanged: a user is a superuser if they are a member of a superuser group or of any of its descendants, the same as User.is_superuser. Add tests for direct, child, grandchild, non-superuser parent and no group membership in the list and detail endpoints, and check that the list doesn't add group queries per user.
Contributor
Author
✅ Deploy Preview for authentik-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #26275 +/- ##
==========================================
- Coverage 92.00% 91.97% -0.03%
==========================================
Files 1175 1175
Lines 76288 76333 +45
Branches 4056 4056
==========================================
+ Hits 70192 70211 +19
- Misses 6054 6080 +26
Partials 42 42
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
rissson
approved these changes
Sep 21, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Details
What does this PR change?
Splits the
_annotated_is_superuserannotation inUserViewSet.get_queryset()from oneEXISTSwith anORacross two join paths into twoEXISTSsubqueries combined withOR: one for direct membership of a superuser group, one for membership of a descendant group.The result is unchanged: a user is a superuser if they're a member of a superuser group or of any of its descendants, the same as
User.is_superuser.Why is this change needed?
PostgreSQL can only run a correlated
EXISTSonce per query as a hashed subplan when the correlation is simple equality. With anORmixing two correlated conditions in one subquery, it runs once per user row instead, including rows skipped byOFFSETand rows inside theDISTINCT/COUNT(*)wrappers some filters add.The plan for that per-row subquery also depends on the statistics of the
authentik_core_groupancestrymaterialized view, which changes too little to be autoanalyzed. After the first group hierarchy is created, stale stats can flip it to a merge join that scans all groups for every user.EXPLAIN ANALYZEof the users list with ~10k users:SubPlan, once per rowhashed SubPlanx2, once per queryIn a real deployment, the mean users list query time dropped by ~78%.
How was this tested?
authentik/core/tests/test_users_api.py:test_list_is_superuser: list and detailis_superuserfor direct, child and grandchild superuser group members, a member of a child of a non-superuser group, and a user without groupstest_list_is_superuser_query_count: more users don't add group queries (fails if the annotation is removed)black,ruffandmypy --strictare cleanUser.is_superuseragree for all usersThe same code is on
version-2026.5andversion-2026.8, so a backport would be welcome.AI usage: I used an AI coding agent to help investigate the query plans and draft the change, tests and description. I reviewed and edited all of it and ran the checks above.
Linked issues
None.
Checklist
make all)make docs)