Skip to content

feat(guardian): Downstream permissions targets to groups - #84

Open
josegomezr wants to merge 4 commits into
suse-2025.12from
downstream_permissions_targets_to_groups
Open

feat(guardian): Downstream permissions targets to groups#84
josegomezr wants to merge 4 commits into
suse-2025.12from
downstream_permissions_targets_to_groups

Conversation

@josegomezr

Copy link
Copy Markdown

feat(guardian): Extend permission targets with parent relationships

Allow granting permission towards a parent model (z.B: Group) and
downstream it to all children.

TL;DR: Grant view_group to a parent, and it grants view_group to all
children.

Additionally took the chance to rewrite a RawSQL statement into native django annotations

@josegomezr
josegomezr requested a review from a team August 25, 2026 13:33
Comment thread authentik/suse/guardian/shortcuts.py Outdated
pk_field = "object_pk"

# Compute codenames, app_label, ctype
for perm in perms:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my personal taste this method is too long.
It mixes setup (parsing the permissions) with normalization and then execution.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, however kept it 1:1 possible with the upstream code cuz the customization comes at the veeery end of the code rather than the beginning, and as usual nothing is encapsulated into overridable code chunks :c

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So upstream having too long methods actually bricks inheritance and the ability to override small parts....
You see that's another reason for short methods....

We should have a preprocessor that just inlines upstream code 😸

# query _and_ matching the parents.
values_list = perms_queryset.values_list(normalized_pk_field, flat=True)
perms_queryset = (
queryset.model.objects.annotate(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you check the generated SQL?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to read cursed SQL?

Baseline (upcasting to target type [uuid, int, varchar]):

SELECT 
"authentik_core_user"."id", "authentik_core_user"."password", "authentik_core_user"."last_login",
"authentik_core_user"."username", "authentik_core_user"."first_name", "authentik_core_user"."last_name",
"authentik_core_user"."email", "authentik_core_user"."is_active", "authentik_core_user"."date_joined",
"authentik_core_user"."attributes", "authentik_core_user"."uuid", "authentik_core_user"."name",
"authentik_core_user"."path", "authentik_core_user"."type", "authentik_core_user"."password_change_date",
"authentik_core_user"."last_updated"
FROM   "authentik_core_user"
WHERE  (
  NOT ("authentik_core_user"."username" = %s)
  AND "authentik_core_user"."id" IN (
    SELECT ("permission_subquery"."object_pk")::bigint AS "object_pk"
    FROM (
      SELECT     "guardian_roleobjectpermission"."object_pk" AS "object_pk"
      FROM "guardian_roleobjectpermission"
      inner join "auth_permission"
      ON ("guardian_roleobjectpermission"."permission_id" = "auth_permission"."id")
      WHERE (
        "guardian_roleobjectpermission"."role_id" IN
        (
          SELECT DISTINCT w0."uuid"
          FROM  "authentik_rbac_role" w0
          left outer join "authentik_core_user_roles" w1
          ON    (w0."uuid" = w1."role_id")
          left outer join "authentik_core_group_roles" w3
          ON    (w0."uuid" = w3."role_id")
          WHERE (w1."user_id" = %s OR w3."group_id" IN 
            (
             SELECT DISTINCT v0."group_uuid"
             FROM  "authentik_core_group" v0
             left outer join "authentik_core_groupancestry" v1
             ON ( v0."group_uuid" = v1."ancestor_id")
             WHERE (v0."group_uuid" IN
              (
                SELECT     u0."group_uuid" AS "pk"
                FROM  "authentik_core_group" u0
                inner join "authentik_core_user_ak_groups" u1
                ON    (u0."group_uuid" = u1."group_id")
                WHERE      u1."user_id" = %s
              )
              OR v1."descendant_id" IN
              (
                SELECT     u0."group_uuid" AS "pk"
                FROM  "authentik_core_group" u0
                inner join "authentik_core_user_ak_groups" u1
                ON    (u0."group_uuid" = u1."group_id")
                WHERE      u1."user_id" = %s
              )
            )
          )
        )
      )
    AND   "auth_permission"."content_type_id" = %s
    AND   "auth_permission"."codename" IN (%s))
  ) "permission_subquery" offset 0)
)

This change (downcasting from target type [all varchars]):

SELECT
 "authentik_core_user"."id", "authentik_core_user"."password", "authentik_core_user"."last_login",
 "authentik_core_user"."username", "authentik_core_user"."first_name", "authentik_core_user"."last_name",
 "authentik_core_user"."email", "authentik_core_user"."is_active", "authentik_core_user"."date_joined",
 "authentik_core_user"."attributes", "authentik_core_user"."uuid", "authentik_core_user"."name",
 "authentik_core_user"."path", "authentik_core_user"."type", "authentik_core_user"."password_change_date",
 "authentik_core_user"."last_updated",( "authentik_core_user"."id" ) :: VARCHAR AS "t__normalized_id"
FROM   "authentik_core_user"
WHERE (
  NOT ( "authentik_core_user"."username" = %s )
  AND ( "authentik_core_user"."id" ) :: VARCHAR IN (
    SELECT ( X0."object_pk" ) :: VARCHAR AS "t__normalized_id"
    FROM "guardian_roleobjectpermission" X0
    inner join "auth_permission" X2
    ON ( X0."permission_id" = X2."id" )
    WHERE  (
      X0."role_id" IN (
        SELECT DISTINCT W0."uuid"
        FROM   "authentik_rbac_role" W0
        left outer join "authentik_core_user_roles" W1
        ON ( W0."uuid" = W1."role_id" )
        left outer join "authentik_core_group_roles" W3
        ON ( W0."uuid" = W3."role_id" )
        WHERE
          ( W1."user_id" = %s OR W3."group_id" IN (
            SELECT DISTINCT V0."group_uuid"
            FROM   "authentik_core_group" V0
            left outer join "authentik_core_groupancestry" V1
            ON (V0."group_uuid" = V1."ancestor_id")
            WHERE ( V0."group_uuid" IN (
              SELECT U0."group_uuid" AS "pk"
              FROM "authentik_core_group" U0
              inner join "authentik_core_user_ak_groups" U1
              ON ( U0."group_uuid" = U1."group_id" )
              WHERE U1."user_id" = %s
          )
          OR V1."descendant_id" IN (
            SELECT U0."group_uuid" AS "pk"
            FROM "authentik_core_group" U0
            inner join "authentik_core_user_ak_groups" U1
            ON ( U0."group_uuid" = U1."group_id" )
            WHERE U1."user_id" = %s
            )
          )
        )
      )
    )
    AND X2."content_type_id" = %s AND X2."codename" IN ( %s )
    )
  )
) 

Comment thread authentik/suse/tests/test_rbac_permission_group_descendants.py Outdated
Comment thread authentik/suse/tests/test_rbac_permission_group_descendants.py Outdated
`get_objects_for_user` relayed on a subquery for "duck-typing"
heterogeneus primary key types and fencing the query planner
optimizations.

Now it's implemented with downcasting (casting from * -> varchar) using
django orm `.annotate` calls.

No need for RawSQL nor OFFSET 0

This behavior is toggled by the suse.use_custom_guardian bool flag
Allow granting permission towards a parent model (z.B: Group) and
downstream it to all children.

TL;DR: Grant view_group to a parent, and it grants view_group to all
children.
@josegomezr
josegomezr force-pushed the downstream_permissions_targets_to_groups branch from 5e6dae4 to 8140c7b Compare September 4, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants