Skip to content

core: split users list superuser annotation in API into two EXISTS subqueries - #26275

Open
ryanpesek wants to merge 1 commit into
goauthentik:mainfrom
ryanpesek:core/api/split-superuser-annotation
Open

ryanpesek wants to merge 1 commit into
goauthentik:mainfrom
ryanpesek:core/api/split-superuser-annotation

Conversation

@ryanpesek

Copy link
Copy Markdown
Contributor

Details

What does this PR change?

Splits the _annotated_is_superuser annotation in UserViewSet.get_queryset() from one EXISTS with an OR across two join paths into two EXISTS subqueries combined with OR: 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 EXISTS once per query as a hashed subplan when the correlation is simple equality. With an OR mixing two correlated conditions in one subquery, it runs once per user row instead, including rows skipped by OFFSET and rows inside the DISTINCT / COUNT(*) wrappers some filters add.

The plan for that per-row subquery also depends on the statistics of the authentik_core_groupancestry materialized 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 ANALYZE of the users list with ~10k users:

Before After
Annotation subplan SubPlan, once per row hashed SubPlan x2, once per query
Last page ~190 ms ~70 ms
Last page, stale ancestry view stats ~20 s ~75 ms
Superuser group with large child groups, page 20 ~32 s ~12 ms

In a real deployment, the mean users list query time dropped by ~78%.

How was this tested?

  • New tests in authentik/core/tests/test_users_api.py:
    • test_list_is_superuser: list and detail is_superuser for direct, child and grandchild superuser group members, a member of a child of a non-superuser group, and a user without groups
    • test_list_is_superuser_query_count: more users don't add group queries (fails if the annotation is removed)
  • Existing users and groups API tests pass; black, ruff and mypy --strict are clean
  • On a real dataset: the old and new annotation and User.is_superuser agree for all users

The same code is on version-2026.5 and version-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

  • The project has been linted, built, and tested (make all)
  • The documentation has been updated and formatted (make docs)
  • I have read the AI usage policy.

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.
@ryanpesek
ryanpesek requested a review from a team as a code owner September 20, 2026 02:09
@ryanpesek

Copy link
Copy Markdown
Contributor Author

@gergosimonyi

@netlify

netlify Bot commented Sep 20, 2026

Copy link
Copy Markdown

Deploy Preview for authentik-docs ready!

Name Link
🔨 Latest commit c0a2a43
🔍 Latest deploy log https://app.netlify.com/projects/authentik-docs/deploys/6aaf404268754f00086f188f
😎 Deploy Preview https://deploy-preview-26275--authentik-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Sep 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.97%. Comparing base (bafc5e1) to head (c0a2a43).
⚠️ Report is 21 commits behind head on main.

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              
Flag Coverage Δ
conformance 33.47% <2.17%> (-0.02%) ⬇️
e2e 38.23% <2.17%> (-0.03%) ⬇️
integration 29.76% <2.17%> (-0.47%) ⬇️
rust 42.89% <ø> (ø)
unit 93.56% <100.00%> (+<0.01%) ⬆️
unit-migrate 93.58% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@rissson rissson moved this to Needs review in authentik Core Sep 21, 2026
@github-project-automation github-project-automation Bot moved this from Needs review to In Progress in authentik Core Sep 21, 2026
@BeryJu BeryJu changed the title core/api: split users list superuser annotation into two EXISTS subqueries core: split users list superuser annotation in API into two EXISTS subqueries Sep 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants