Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST API: Make field read-only for certain permission level

How make some fields read-only for particular user permission level?

There is a Django REST API project. There is an Foo serializer with 2 fields - foo and bar. There are 2 permissions - USER and ADMIN.

Serializer is defined as:

class FooSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = FooModel
        fields = ['foo', 'bar']

How does one makes sure 'bar' field is read-only for USER and writable for ADMIN?

I would use smth like:

class FooSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = FooModel
        fields = ['foo', 'bar']
        read_only_fields = ['bar']

But how to make it conditional (depending on permission)?

like image 894
0leg Avatar asked Dec 28 '16 17:12

0leg


3 Answers

You can use get_serializer_class() method of the view to use different serializers for different users:

class ForUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = ExampleModel
        fields = ('id', 'name', 'bar')
        read_only_fields =  ('bar',)

class ForAdminSerializer(serializers.ModelSerializer):
    class Meta:
        model = ExampleModel
        fields = ('id', 'name', 'bar', 'for_admin_only_field')

class ExampleView(viewsets.ModelViewSet):    
    ...
    def get_serializer_class(self):
        if self.request.user.is_admin:
            return ForAdminSerializer
        return ForUserSerializer
like image 107
Fine Avatar answered Sep 28 '22 00:09

Fine


Although Fian's answer does seem to be the most obviously documented way there is an alternative that draws on other documented code and which enables passing arguments to the serializer as it is instantiated.

The first piece of the puzzle is the documentation on dynamically modifying a serializer at the point of instantiation. That documentation doesn't explain how to call this code from a viewset or how to modify the readonly status of fields after they've been initated - but that's not very hard.

The second piece - the get_serializer method is also documented - (just a bit further down the page from get_serializer_class under 'other methods') so it should be safe to rely on (and the source is very simple, which hopefully means less chance of unintended side effects resulting from modification). Check the source under the GenericAPIView (the ModelViewSet - and all the other built in viewset classes it seems - inherit from the GenericAPIView which, defines get_serializer.

Putting the two together you could do something like this:

In a serializers file (for me base_serializers.py):

class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""

def __init__(self, *args, **kwargs):
    # Don't pass the 'fields' arg up to the superclass
    fields = kwargs.pop('fields', None)

    # Adding this next line to the documented example
    read_only_fields = kwargs.pop('read_only_fields', None)

    # Instantiate the superclass normally
    super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

    if fields is not None:
        # Drop any fields that are not specified in the `fields` argument.
        allowed = set(fields)
        existing = set(self.fields)
        for field_name in existing - allowed:
            self.fields.pop(field_name)

    # another bit we're adding to documented example, to take care of readonly fields 
    if read_only_fields is not None:
        for f in read_only_fields:
            try:
                self.fields[f].read_only = True
            exceptKeyError:
                #not in fields anyway
                pass

Then in your viewset you might do something like this:

class MyUserViewSet(viewsets.ModelViewSet):
    # ...permissions and all that stuff

    def get_serializer(self, *args, **kwargs):

        # the next line is taken from the source
        kwargs['context'] = self.get_serializer_context()

        # ... then whatever logic you want for this class e.g:
        if self.request.user.is_staff and self.action == "list":
            rofs = ('field_a', 'field_b')
            fs = ('field_a', 'field_c')
        #  add all your further elses, elifs, drawing on info re the actions, 
        # the user, the instance, anything passed to the method to define your read only fields and fields ...
        #  and finally instantiate the specific class you want (or you could just
        # use get_serializer_class if you've defined it).  
        # Either way the class you're instantiating should inherit from your DynamicFieldsModelSerializer
        kwargs['read_only_fields'] = rofs
        kwargs['fields'] = fs
        return MyUserSerializer(*args, **kwargs)

And that should be it! Using MyUserViewSet should now instantiate your UserSerializer with the arguments you'd like - and assuming your user serializer inherits from your DynamicFieldsModelSerializer, it should know just what to do.

Perhaps its worth mentioning that of course the DynamicFieldsModelSerializer could easily be adapted to do things like take in a read_only_exceptions list and use it to whitelist rather than blacklist fields (which I tend to do). I also find it useful to set the fields to an empty tuple if its not passed and then just remove the check for None ... and I set my fields definitions on my inheriting Serializers to 'all'. This means no fields that aren't passed when instantiating the serializer survive by accident and I also don't have to compare the serializer invocation with the inheriting serializer class definition to know what's been included...e.g within the init of the DynamicFieldsModelSerializer:

# ....
fields = kwargs.pop('fields', ())
# ...
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
# ....

NB If I just wanted two or three classes that mapped to distinct user types and/or I didn't want any specially dynamic serializer behaviour, I might well probably stick with the approach mentioned by Fian.

However, in a case of mine I wanted to adjust the fields based both on the action as well as the admin level of the user making the request, which led to many long and annoying serializer class names. It began to feel ugly creating many serializer classes simply to tweak the list of fields and readonly fields. That approach also meant that the list of fields was separated from the relevant business logic in the view. It might be debatable whether thats a good thing but when the logic gets a tad more involved, I thought it would make the code less, rather than more, maintainable. Of course it makes even more sense to use the approach I've outlined above if you also want to do other 'dynamic' things on the initiation of the serializer.

like image 31
user1936977 Avatar answered Sep 28 '22 00:09

user1936977


You can extend the get_fields method in the serializer class. In your case it would look like this:

class FooSerializer(serializers.ModelSerializer):
    ...
    class Meta:
        model = FooModel
        fields = ["foo", "bar"]
    
    def get_fields(self):
        fields = super().get_fields()  # Python 3 syntax
        request = self.context.get("request", None)
        if request and request.user and request.user.is_superuser is False:
            fields["bar"].read_only = True
        return fields

like image 26
Agey Avatar answered Sep 28 '22 01:09

Agey