Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve performance of shortcuts #796

Open
ericmuijs opened this issue May 21, 2023 · 3 comments
Open

Improve performance of shortcuts #796

ericmuijs opened this issue May 21, 2023 · 3 comments

Comments

@ericmuijs
Copy link

ericmuijs commented May 21, 2023

Really like django guardian.

I noticed that the shortcuts use PK lists, which tend to be relatively slow for big groups. Can this be improved using a subquery?

Is this project still alive?

@ericmuijs
Copy link
Author

Example for improvement in guardian shortcuts for direct model:

if not any_perm and len(codenames): objects = queryset.filter(pk__in=Subquery(groups_obj_perms_queryset.values(fields[0]))) return objects

@iamMHZ
Copy link

iamMHZ commented Jul 6, 2023

@ericmuijs
Yeah, there are some performance pitfalls specially in the shortcuts, You can send a PR imporving that (Supprots are very wellcome for the project #603)

@vecchp
Copy link

vecchp commented Feb 7, 2024

Ended up writing a direct model only shortcut for another project. This may be useful for some people and maybe a PR can come out of it. Though I wasn't able to update the core library. Should result in a single query

from functools import reduce
from operator import and_, or_
from typing import List, TypeVar, Union, cast

from django.contrib.auth.models import AbstractBaseUser, AnonymousUser
from django.db.models import Exists, Model, OuterRef, Q, QuerySet
from guardian.utils import get_group_obj_perms_model, get_user_obj_perms_model

T = TypeVar("T", bound=Model)


def get_objects_for_user(
    user: Union[AbstractBaseUser, AnonymousUser],
    perms: List[str],
    klass: QuerySet[T],
    any_perm: bool = False,
) -> QuerySet[T]:
    """
    Fetches a queryset of objects for which the user has specified permissions.
    Acts as a replacement for Django Guardian's `get_objects_for_user`, aiming
    for flexible and efficient permission checks using Django's ORM.

    Args:
        user: User for whom to retrieve objects.
        perms: Permission strings to check.
        klass: Initial queryset of model objects.
        any_perm: If True, returns objects for any permissions. Else, all.

    Returns:
        A queryset of objects with the specified permissions for the user.

    Note:
        - Dynamically builds queries for user/group permissions.
        - Requires `klass` as a correct model type queryset and `perms` to be
          model-appropriate permission codenames.
        - Custom `UserObjectPermission` and `GroupObjectPermission` models
          associate permissions with model instances, enabling granular access
          control.
    """
    if not user.is_authenticated or not perms:
        return klass.none()

    user_permissions_field = get_user_obj_perms_model(
        klass.model
    ).permission.field.related_query_name()
    group_permissions_field = get_group_obj_perms_model(
        klass.model
    ).permission.field.related_query_name()

    qs = klass
    permission_filters = []

    for perm in perms:
        perm_codename = perm.split(".")[-1]
        user_perm_query = Q(
            **{
                f"{user_permissions_field}__permission__codename": perm_codename,
                f"{user_permissions_field}__user": user,
            }
        )
        group_perm_query = Q(
            **{
                f"{group_permissions_field}__permission__codename": perm_codename,
                f"{group_permissions_field}__group__user": user,
            }
        )
        permission_filters.append(
            Exists(klass.filter(user_perm_query | group_perm_query, pk=OuterRef("pk")))
        )

    if any_perm:
        combined_condition = reduce(or_, permission_filters)
    else:
        combined_condition = reduce(and_, permission_filters)

    return cast(
        QuerySet[T],
        qs.annotate(has_permission=combined_condition).filter(has_permission=True),
    )

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

No branches or pull requests

3 participants