dfunckt / django-rules

Awesome Django authorization, without the database

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

can I set a rule that only cares abt the attribtues of the object but not the user?

simkimsia opened this issue · comments

let's say I have a rule taht is the below

@rules.predicate
def is_assignable(user, instance):
    return instance.status == "gr_incomplete"

Can I simply have a rule that ignores the user? ANd do I still have to mock up a request and user in order to run unit tests for this?

Yes, user can be ignored. Regarding mocking, it depends on what you are testing. Do you have an example of such a test?

I might be over-elaborating here but I think might be good to provide context of my use case.

I am using rules to manifest if a quotation is_assignable, is_x-able and so on.

Based on whether a quotation is_assignable, etc, different form controls can be displayed.

I expect some of the is_x-able will eventually depend on both the object (the quotation) and the user.

However, at present most of the is_x-able depends on just the object.

I am using Django Rest Framework and therefore its serializers. I am using rules in the DRF serializers via the SerializerMethodField

class RetrieveBaseVendorQuotationSerializer(
    OrganizationOwnedMixin, PersonStampedMixin, DynamicModelSerializer
):
    quotation_number = serializers.CharField(read_only=True)

    deletable = DynamicMethodField()
    is_assignable = DynamicMethodField()
    class Meta:
        model = VendorQuotation
        name = "vendor_quotation"
        fields = (
            "id",
            "quotation_number",
            "status",
            "is_assignable",
            "deletable",
        )
        read_only_fields = fields

    def get_deletable(self, instance):
        user = self.context["request"].user
        return user.has_perm("vendors.delete_quotation", instance)

    def get_is_assignable(self, instance):
        user = self.context["request"].user
        return user.has_perm("vendors.assign_quotation", instance)

And therefore my test case starts with calling the endpoint that triggers this serializer. So far so good.

Now I reach a point where the serializers are not very fast for read operations. So I have decided to roll out my own serializers meant for read operations.

My own serializers are very very simple. They are simply functions taking an object and then map to a dictionary with string keys.

When I do that I can write unit tests (instead of endpoint tests) that only test my custom serializers taking in a specific instance and returning a dictionary.

But when I roll out my own serializers, i don't have methodfields. In order to use rules, I rewrote some of my custom serializer functions into classes.

Right now, one looks like this:

class ReadASPQuotationSerializer:

    def get_is_assignable(self, instance):
        user = self.context["request"].user
        return user.has_perm("vendors.assign_ir", instance)

    def __init__(self, context):
        self.context = context

    def get_deletable(self, instance):
        user = self.context["request"].user
        return user.has_perm("vendors.delete_quotation", instance)

    def serialize_asp_quotation_for_read(self, asp_quotation: VendorQuotation) -> Dict[str, Any]:
        # all decimals are coerced to string
        # to avoid loss of precision use "{0:f}".format(foo)
        # see https://stackoverflow.com/a/52978507/80353
        return {
            "id": asp_quotation.id,
            "display_quotation_number": asp_quotation.display_quotation_number,
            "quotation_date": "{:%Y-%m-%d}".format(asp_quotation.quotation_date),
             "is_assignable": self.get_is_assignable(asp_quotation),
            "deletable": self.get_deletable(asp_quotation),

now this works, but then I have to change my unit test this way.

def test_asp_quotation_for_po_released(self):

        asp_q = VendorQuotation.objects.get(id=self.draft_vendor_quotation.id)

        mocked_request = Mock(user=self.user)

        serializer = ReadASPQuotationSerializer({"request": mocked_request})

        serialized_asp = serializer.serialize_asp_quotation_for_read(asp_q)

My rules.py which remains the same regardless the new way of writing serializers or using DRF serializers has always been

@rules.predicate
def is_assignable(user, vendor_quotation):
    return vendor_quotation.status == "po_released"'

rules.add_perm("vendors.assign_ir", is_assignable)

As you can see, currently my get_is_assignable doesn't really require the user. But because of the way rules is written, I have to mock it so that I can write my own unit test for my own custom read serializers.

Of course, ideally i should not roll out my own serializers unfortunately, the speed of the read endpoints have reached a point where I have to roll my own. And to feel confident that it works, I have to write unit tests that just test this custom serializers. See https://hakibenita.com/django-rest-framework-slow for explanation when need to roll out own serializers for read operations.

if there's a way I can write my unit tests for my custom serializers, while keeping rules.py as it is, but without writing mocked users, that would be great. Otherwise I can live with this "ugly" way of writing unit tests.

Thank you

P.S.: let me know if I need to rewrite my issue for clarity. I know explaining technical issues is not my strength

I see. If you don't care about actually checking permissions (ie. permission backends other than the one provided by Rules) you could call has_perm directly from your serializers. Like so:

    def get_is_assignable(self, instance):
        return has_perm("vendors.assign_ir", None, instance)
    ...

Thanks for reply. I just tried this and I get an issue with has_perm being undefined. I tried to use ModelBackend but it assumes that the user is an actual object and not None.

See https://github.com/django/django/blob/3ab5235d1dc94f7c8fe37a98c4e2c2337a5e5548/django/contrib/auth/backends.py#L88

What can I do to make it such that the only check is the one provided by Rules and only that and doesn't require the user object?

Ok i got it to work

Just use the rules.has_perm

Figured it out from this section https://github.com/dfunckt/django-rules#managing-the-permissions-rule-set

my actual code is as belows:

import rules

class ReadASPQuotationSerializer:
    def get_pr_assignable(self, instance):
        return rules.has_perm("vendors.assign_pr", None, instance)