Per Field Permission in Django REST Framework
Asked Answered
C

10

45

I am using Django REST Framework to serialize a Django model. I have a ListCreateAPIView view to list the objects and a RetrieveUpdateDestroyAPIView view to retrieve/update/delete individual objects. The model stores information that the users submit themselves. The information they submit contains some private information and some public information. I want all users to be able to list and retrieve the public information but I want only the owner to list/retrieve/update/delete the private information. Therefore, I need per-field permissions and not object permissions.

The closest suggestion I found was https://groups.google.com/forum/#!topic/django-rest-framework/FUd27n_k3U0 which changes the serializer based on the request type. This won't work for my situation because I don't have the queryset or object at that point to determine if it is owned by the user or not.

Of course, I have my frontend hiding the private information but smart people can still snoop the API requests to get the full objects. If code is needed, I can provide it but my request applies to vanilla Django REST Framework designs.

Crum answered 2/10, 2013 at 1:49 Comment(0)
H
71

How about switching serializer class based on user?

In documentation:

http://www.django-rest-framework.org/api-guide/generic-views/#get_serializer_classself

def get_serializer_class(self):
    if self.request.user.is_staff:
        return FullAccountSerializer
    return BasicAccountSerializer
Heteroousian answered 27/12, 2015 at 23:50 Comment(3)
Perfect finding. Thank you sir. You made my life easy.Clactonian
This is the wayOrangewood
Unfortunately this won't work when you need instance-level permissions (eg, only showing certain fields if user is object owner)Absentee
W
15

I had a similar problem the other day. Here is my approach:

This is a DRF 2.4 solution.

class PrivateField(serializers.Field):
    def field_to_native(self, obj, field_name):
        """
        Return null value if request has no access to that field
        """
        if obj.created_by == self.context.get('request').user:
            return super(PrivateField, self).field_to_native(obj, field_name)
        return None

#Usage
class UserInfoSerializer(serializers.ModelSerializer):
    private_field1 = PrivateField()
    private_field2 = PrivateField()

    class Meta:
        model = UserInfo

And a DRF 3.x solution:

class PrivateField(serializers.ReadOnlyField):

    def get_attribute(self, instance):
        """
        Given the *outgoing* object instance, return the primitive value
        that should be used for this field.
        """
        if instance.created_by == self.context['request'].user:
            return super(PrivateField, self).get_attribute(instance)
        return None

This time we extend ReadOnlyField only because to_representation is not implemented in the serializers.Field class.

Wangle answered 25/9, 2014 at 19:33 Comment(2)
field_to_native was removed in django rest framework 3.0.Vershen
@Vershen True, we finally got time to update to the new version of DRF, so a new solution provided.Wangle
C
6

I figured out a way to do it. In the serializer, I have access to both the object and the user making the API request. I can therefore check if the requestor is the owner of the object and return the private information. If they are not, the serializer will return an empty string.

class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
    private_field1 = serializers.SerializerMethodField('get_private_field1')

    class Meta:
        model = UserInfo
        fields = (
            'id',
            'public_field1',
            'public_field2',
            'private_field1',
        )
        read_only_fields = ('id')

    def get_private_field1(self, obj):
        # obj.created_by is the foreign key to the user model
        if obj.created_by != self.context['request'].user:
            return ""
        else:
            return obj.private_field1
Crum answered 2/10, 2013 at 21:43 Comment(2)
For a solution that allows reading and writing, see https://mcmap.net/q/366947/-per-field-permission-in-django-rest-frameworkVershen
For posts you must use validatorsMargravine
P
6

Here:

-- models.py:

class Article(models.Model):
    name = models.CharField(max_length=50, blank=False)
    author = models.CharField(max_length=50, blank=True)

    def __str__(self):
        return u"%s" % self.name

    class Meta:
        permissions = (
            # name
            ('read_name_article', "Read article's name"),
            ('change_name_article', "Change article's name"),

            # author
            ('read_author_article', "Read article's author"),
            ('change_author_article', "Change article's author"),
        )

-- serializers.py:

class ArticleSerializer(serializers.ModelSerializer):

    class Meta(object):
        model = Article
        fields = "__all__"

    def to_representation(self, request_data):
        # get the original representation
        ret = super(ArticleSerializer, self).to_representation(request_data)
        current_user = self.context['request'].user
        for field_name, field_value in sorted(ret.items()):
            if not current_user.has_perm(
                'app_name.read_{}_article'.format(field_name)
            ):
                ret.pop(field_name)  #  remove field if it's not permitted

        return ret

    def to_internal_value(self, request_data):
        errors = {}
        # get the original representation
        ret = super(ArticleSerializer, self).to_internal_value(request_data)
        current_user = self.context['request'].user
        for field_name, field_value in sorted(ret.items()):
            if field_value and not current_user.has_perm(
                'app_name.change_{}_article'.format(field_name)
            ):
                errors[field_name] = ["Field not allowed to change"]  # throw error if it's not permitted

        if errors:
            raise ValidationError(errors)

        return ret
Pisciform answered 12/3, 2018 at 2:42 Comment(0)
V
3

For a solution that allows both reading and writing, do this:

class PrivateField(serializers.Field):
    def get_attribute(self, obj):
        # We pass the object instance onto `to_representation`,
        # not just the field attribute.
        return obj

    def to_representation(self, obj):
        # for read functionality
        if obj.created_by != self.context['request'].user:
            return ""
        else:
            return obj.private_field1

    def to_internal_value(self, data):
        # for write functionality
        # check if data is valid and if not raise ValidationError


class UserInfoSerializer(serializers.HyperlinkedModelSerializer):
    private_field1 = PrivateField()
    ...

See the docs for an example.

Vershen answered 30/8, 2015 at 23:16 Comment(0)
C
3

This is an old question, but the topic is still relevant.

DRF recommends to create different serializers for different permission. But this approach only works, if you have only a few permissions or groups.

restframework-serializer-permissions is a drop in replacement for drf serializers. Instead of importing the serializers and fields from drf, you are importing them from serializer_permissions.

Installation:

$ pip install restframework-serializer-permissions

Example Serializers:

# import permissions from rest_framework
from rest_framework.permissions import AllowAny, IsAuthenticated

# import serializers from serializer_permissions instead of rest_framework
from serializer_permissions  import serializers

# import you models
from myproject.models import ShoppingItem, ShoppingList


class ShoppingItemSerializer(serializers.ModelSerializer):

    item_name = serializers.CharField()

    class Meta:
        # metaclass as described in drf docs
        model = ShoppingItem
        fields = ('item_name', )


class ShoppingListSerializer(serializers.ModelSerializer):

    # Allow all users to list name
    list_name = serializers.CharField(permission_classes=(AllowAny, ))

    # Only allow authenticated users to retrieve the comment
    list_comment = serializers.CharField(permissions=(IsAuthenticated, ))

    # show owner only, when the current user has 'auth.view_user' permission
    owner = serializers.CharField(permissions=('auth.view_user', ), hide=True)

    # serializer which is only available, when the user is authenticated
    items = ShoppingItemSerializer(many=True, permissions=(IsAuthenticated, ), hide=True)

    class Meta:
        # metaclass as described in drf docs
        model = ShoppingItem
        fields = ('list_name', 'list_comment', 'owner', 'items', )

Disclosure: I'm the author of this extension

Climate answered 6/4, 2021 at 14:19 Comment(0)
G
2

In case you are performing only READ operations, you can just pop the fields in to_representation method of the serializer.

def to_representation(self,instance):
    ret = super(YourSerializer,self).to_representation(instance)
    fields_to_pop = ['field1','field2','field3']
    if instance.created_by != self.context['request'].user.id:
        [ret.pop(field,'') for field in fields_to_pop]
    return ret

This should be enough to hide sensitive fields.

Griseous answered 9/12, 2016 at 11:27 Comment(0)
P
2

Just share another possible solution

For example, to make email only show for oneself.

On UserSerializer, add:

email = serializers.SerializerMethodField('get_user_email')

Then implement get_user_email like this:

def get_user_email(self, obj):
    user = None
    request = self.context.get("request")
    if request and hasattr(request, "user"):
        user = request.user
    return obj.email if user.id == obj.pk else 'HIDDEN'
Palladin answered 27/6, 2019 at 2:22 Comment(0)
M
0

I solved it using a serializer Mixin:

class FieldPermissionModelSerializerMixin(serializers.ModelSerializer):
    """
    A mixin that allows you to specify what fields will be returned based on field level permissions
    """

    permission_fields = []

    def get_field_names(self, declared_fields, info) -> List:
        """Determine the fields to apply."""
        fields = getattr(self.Meta, "fields", [])
        for permission_field in self.permission_fields:
            app_name = getattr(self.Meta, "model", None)._meta.app_label
            permission_name = f"can_view_field_{permission_field}"
            full_permission_name = f"{app_name}.{permission_name}"

            if self.context["request"].user.has_perm(full_permission_name):
                fields.append(permission_field)

        return fields

Then you can use this serializer with base fields and permissionable fields.

POSITION_BASE_FIELDS = [
    "id",
    "name",
    "level",
    "role",
    "sort",
]

POSITION_PERMISSION_FIELDS = ["market_salary", "recommended_rate_per_hour"]


class PositionListSerializer(FieldPermissionModelSerializerMixin):
    permission_fields = POSITION_PERMISSION_FIELDS

    class Meta:
        model = Position
        fields = POSITION_BASE_FIELDS + []

This is then based on field level permissions defined on the model.

class Position(models.Model):
    name = models.CharField(max_length=255, db_index=True)
    level = models.CharField(max_length=255, null=True, blank=True)
    sort = models.IntegerField(blank=True, default=0)
    market_salary = models.DecimalField(max_digits=19, decimal_places=2, default=0.00)
    recommended_rate_per_hour = models.DecimalField(
        max_digits=7, decimal_places=2, null=True, blank=True
    )

    class Meta:
        ordering = ["name", "sort"]
        unique_together = ("name", "level")
        permissions = (
            ("can_view_field_market_salary", "Can view field: market_salary"),
            (
                "can_view_field_recommended_rate_per_hour",
                "Can view field: recommended_rate_per_hour",
            ),
        )
Matinee answered 20/7, 2022 at 14:31 Comment(0)
S
0

I used this library https://github.com/starnavi-team/drf_serializer_fields_permissions to implement this.

link to pypi https://pypi.org/project/drf-serializer-fields-permissions/0.2.0/

example of usage:

from rest_framework import serializers
from rest_framework import permissions

from .models import Project

from fields_permissions.mixins import FieldPermissionMixin


class ProjectSerializer(FieldPermissionMixin, serializers.ModelSerializer):

    class Meta:
        model = Project
        fields = ('id', 'name', 'status', 'description', 'team_lead_user')

        show_only_for = {
            'fields': ('team_lead_user',),
            'permission_classes': (permissions.IsAdminUser,)
        }
        write_only_for = {
            'fields': ('status', 'description'),
            'permission_classes': (permissions.IsAdminUser,)
        }
Sabulous answered 24/1 at 8:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.