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

Update access_view_set_mixin.py #75

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

Update access_view_set_mixin.py #75

wants to merge 1 commit into from

Conversation

helderlgoliveira
Copy link
Contributor

Included get_queryset method to return the scope_queryset if it has been explicity declared in the access_policy.
It calls super().get_queryset() instead of directly self.queryset to allow work with anothers mixins.
That way, won't be necessary to call the scope_queryset everytime in the view.

Included get_queryset method to return the scope_queryset if it has been explicity declared in the access_policy.
It calls super().get_queryset() instead of directly self.queryset to allow work with anothers mixins.
That way, won't be necessary to call the scope_queryset everytime in the view.
@micurbanski
Copy link

micurbanski commented Oct 19, 2021

At first I thought it's a good idea, but after consideration there is a downside to that, if I'm correct. Let's say we have

# policies.py
class CustomAccessViewSetMixin(AccessViewSetMixin):
    def get_queryset(self):
        queryset = super().get_queryset()
        scope_queryset = self.access_policy.scope_queryset(self.request, queryset)
        if scope_queryset.exists():
            return scope_queryset
        return queryset

class SomeAccessPolicy(AccessPolicy):
    @classmethod
    def scope_queryset(cls, request, qs):
        role = request.user.role
        if role == "author":
            return qs.filter(author=request.user)
        elif role == "reviewer":
            return qs.filter(reviewer=request.user)
        else:
            return qs.none()

# views.py
class SomeView(CustomAccessViewSetMixin, viewsets.ModelViewSet):
    queryset = SomeObject.objects.all()
    access_policy = SomeAccessPolicy

Then if scoped_queryset turns out empty (users role is outside of if-clauses or user is neither author nor reviewer of any requested object) , mixin will return the view's queryset which would be quite unfortunate.

I believe that here the default approach for overwriting get_queryset each time we are using scoped_queryset is safer.

@helderlgoliveira
Copy link
Contributor Author

Then if scoped_queryset turns out empty (users role is outside of if-clauses or user is neither author nor reviewer of any requested object) , mixin will return the view's queryset which would be quite unfortunate.

Good point, thanks! What you think about:

    def get_queryset(self):
        queryset = super().get_queryset()
        scope_queryset = self.access_policy.scope_queryset
        if scope_queryset.__name__ not in self.access_policy.__dict__:
            return queryset
        return scope_queryset(self.request, queryset)

That way, it always will return the scope_queryset if it has been overrided in the view's access_policy class.

@micurbanski
Copy link

I think that it will always evaluates to false by default:

In [20]: from rest_access_policy import AccessPolicy
In [21]: access_policy = AccessPolicy
In [22]: scope_queryset = access_policy.scope_queryset

In [23]: scope_queryset.__name__ not in access_policy.__dict__
Out[23]: False

The most stratightforward solution I see right now is to go with something like:

class AccessPolicy(permissions.BasePermission):
    "..."

    @classmethod
    def scope_queryset(cls, request, qs):
        raise NotImplementedError  # was: return qs.none()

class CustomAccessViewSetMixin(AccessViewSetMixin):
    def get_queryset(self):
        queryset = super().get_queryset()
        try:
            scope_queryset = self.access_policy.scope_queryset(self.request, queryset)
            return scope_queryset
        except NotImplementedError:
            return queryset

Or something in similar fashion. Raising NotImplementedError may not be the best solution for signalling that you're using the library default, but that's how I would go about it.

@helderlgoliveira
Copy link
Contributor Author

helderlgoliveira commented Oct 20, 2021

    def get_queryset(self):
        queryset = super().get_queryset()
        scope_queryset = self.access_policy.scope_queryset
        if scope_queryset.__name__ not in self.access_policy.__dict__:
            return queryset
        return scope_queryset(self.request, queryset)

I think that it will always evaluates to false by default

Yes, because of the not in. The idea is to only return the scope_queryset automatically if it has been explicity declared(overriden) in the Access Policy class:

from app.views_access_policies import ScopedQuerysetAccessPolicy, NotScopedQuerysetAccessPolicy

# AccessPolicy without scope_queryset:
NotScopedQuerysetAccessPolicy.scope_queryset.__name__ not in NotScopedQuerysetAccessPolicy.__dict__
# Out: True (view's queryset will be returned)

# AccessPolicy with scope_queryset:
ScopedQuerysetAccessPolicy.scope_queryset.__name__ not in ScopedQuerysetAccessPolicy.__dict__
# Out: False (scope_queryset will be returned)

That way, if I'm correct, I think it will remains optional to implement or not the scope_queryset in the Access Policy class. Once implemented, it'll be called.

Sorry if I didn't understand what you said.

@rsinger86
Copy link
Owner

It didn't occur to me that the mixin could call this automatically for the user. Thanks for the input.

scope_queryset will always be defined because the version in the base class returns an empty queryset:
https://github.com/rsinger86/drf-access-policy/blob/master/rest_access_policy/access_policy.py#L69

I guess I'd lean toward the viewset mixin always calling scope_queryset, which seems like the safest default because it will return zero rows, unless the user defines it.

@helderlgoliveira @micurbanski what do you think?

@micurbanski
Copy link

I'm fine with that, but just to be sure- it would got something like this:

class AccessPolicy(permissions.BasePermission):
    "..."

    @classmethod
    def scope_queryset(cls, request, qs):
        return qs.none()

###
class AccessViewSetMixin:
    "..."

    def get_queryset(self):
        queryset = super().get_queryset()
        return self.access_policy.scope_queryset(self.request, queryset)

And the worst case scenario it will return qs.none() if scope_queryset is not defined by user, right? That's what I'm fine with :)

@rsinger86
Copy link
Owner

yep, exactly :)

@helderlgoliveira
Copy link
Contributor Author

I agree, but it'll be a breaking change. Apps that didn't override scope_queryset in Access Policies classes will return the empty queryset instead of the ViewSet's queryset.

If I'm not wrong, maybe this could maintain backwards compability:

    def get_queryset(self):
        queryset = super().get_queryset()
        scope_queryset = self.access_policy.scope_queryset
        if scope_queryset.__name__ not in self.access_policy.__dict__:
            return queryset
        return scope_queryset(self.request, queryset)

Or changing the default scope_queryset to return the ViewSet's queryset, that way the method override won't be mandatory:

@classmethod
    def scope_queryset(cls, request, qs):
        return qs  # before: qs.none()

Regards

@rsinger86
Copy link
Owner

oh, good point about the breaking change.

What do you think about introducing a new mixin called StrictAccessViewSetMixin that includes the change and leave the existing mixin as-is?

@helderlgoliveira
Copy link
Contributor Author

I agree, good idea

@ihucos
Copy link

ihucos commented Aug 20, 2024

I did a PR for that: #105

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.

4 participants