Welcome to Django REST Framework Delegated Permissions’s documentation!¶
Contents:
Getting started¶
For this sample we will have an Invoice containing an Address. We want to set up permissions on Address so that the user that holds permissions to Invoice instance can view/modify the associated Address instance.
Preparation¶
Install django>=1.11
, djangorestframework
and django-rest-delegated-permissions
virtualenv --python=python3 venv
source venv/bin/activate
pip install django djangorestframework django-rest-delegated-permissions
Create your models:
class Address(models.Model):
... address fields
class Meta:
permissions = (
('view_address', 'Can view address'),
)
class Invoice(models.Model):
address = models.OneToOneKey(Address, related_name='invoice')
class Meta:
permissions = (
('view_invoice', 'Can view invoice'),
)
Note: we have added declaration of an extra permission in the format of appname.view_model
, users with this
permission will be able to have a read access to the model instances (via REST’s GET method either on collection
of models or instance of a model).
And REST viewsets and serializers:
class AddressViewSet(ModelViewSet):
queryset = Address.objects.all()
serializer = AddressSerializer
...
class InvoiceViewSet(ModelViewSet):
queryset = Invoice.objects.all()
serializer = InvoiceSerializer
...
Add routing to your urls.py, generate a couple of instances of Invoice
and Address
and check that it works so far:
router = DefaultRouter()
router.register(r'invoice', InvoiceViewSet)
router.register(r'address', AddressViewSet)
urlpatterns = [
url(r'^admin/', admin.site.urls),
url('', include(router.urls))
]
Adding permissions¶
To add secure the viewset endpoints, you traditionally have to:
- secure the modification operations via permissions defined on the viewset class
- override its filter_queryset method to include only those objects that user has rights to (for example for resource listing)
django-rest-related-permissions
groups these two cases into one rest_delegated_permissions.BasePermission
class
and as a convenience provides rest_delegated_permissions.DjangoCombinedPermission
that contains these two steps
for django model and django guardian permissions.
- To be able to use permissions from related class (referenced via ForeignKey, m2m, etc) one needs to:
- remove permissions from viewsets
- link them to model classes so that Address’s permission checker has an access to the Invoice permission checker
- put them into one registry that than composes queryset filters (so it is able to provide the
filter_queryset
implementation) and validates if user has rights to perform operation on related class. - override
filter_queryset
andget_permissions
methods with implementation that uses the permissions from the registry
These steps are implemented inside the rest_delegated_permissions.RestPermissions
class. Let’s use it:
perms = RestPermissions(
add_django_permissions=True
)
# apply the same permissions that are on invoice
# to address + add django and django guardian permissions
# defined on Address model
@perms.apply(permissions=DelegatedPermission(perms, 'invoice'))
class AddressViewSet(ModelViewSet):
queryset = Address.objects.all()
serializer = AddressSerializer
...
# apply only django and django guardian permissions on this model
@perms.apply()
class InvoiceViewSet(ModelViewSet):
queryset = Invoice.objects.all()
serializer = InvoiceSerializer
...
The code above is a shortcut - it at first extracts model class from ViewSet queryset
field,
associates the permissions given inside the decorator call with this class and sets up filter_queryset
and get_permissions
. More explicitly, the code can be rewritten as:
perms = RestPermissions(
add_django_permissions=True
)
# the permissions below are combined via OR operator
perms.set_model_permissions(Address, [
DelegatedPermission(perms, 'invoice'),
DjangoCombinedPermission()
])
perms.set_model_permissions(Invoice, [
DjangoCombinedPermission()
])
@perms.apply()
class AddressViewSet(ModelViewSet):
...
@perms.apply()
class InvoiceViewSet(ModelViewSet):
...
Note: Each model can be registered into RestPermissions only once. If you need multiple viewsets with different settings of permissions, use multiple instances of RestPermissions.
You can even use rest-condition
package to combine permissions via AND or OR:
perms = RestPermissions(
add_django_permissions=False
)
# the permissions below must both hold to give access
perms.set_model_permissions(Address, [
Condition.And(
DelegatedPermission(perms, 'invoice'),
DjangoCombinedPermission())
])
If you try the code now, you will not have the access to any objects and listing will return an empty answer.
curl -u username:password http://localhost:8000/address
If you set up django permissions on User and try again, you will the access:
user = User.objects.get(...)
user.user_permissions.add(
Permission.objects.get('app.view_invoice'))
curl -u username:password http://localhost:8000/address
# returns the previously created addresses
Similarly, you can add django guardian permission on a singe invoice and then see the address of that single invoice.
Custom permissions¶
To implement a custom permission, base it on rest_delegated_permissions.BasePermission
. For example the
following permission grants access to the user that is the owner of an invoice:
...
class Invoice(models.Model):
owner = models.ForeingKey(User, ...)
...
class OwnerPermission(BasePermission):
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
def filter(self, rest_permissions, filtered_queryset,
user, action):
yield filtered_queryset.filter(owner=user)
@perms.apply(permissions=OwnerPermission())
class InvoiceViewSet(ModelViewSet):
...
Now, when you GET .../invoice
, you’ll receive only those to which you are the owner (or have django/django guardian)
permissions. If you GET .../address
, you’ll receive those addresses that belong only to the invoices to which you have
the rights.