drf-confidential is a package to help you control how a model's sensitive data is shared across your API.
pip install drf-confidential
Imagine you have the following models declared as:
from django.contrib.auth.models import AbstractUser
from django.db import models
class Profile(AbstractUser):
email = models.EmailField(unique=True)
employee_profile = models.OneToOneField(
"Employee",
null=True,
on_delete=models.PROTECT,
related_name='login_account'
)
class Employee(models.Model):
first_name = models.CharField(max_length=64)
last_name = models.CharField(max_length=64)
address_1 = models.CharField(max_length=256)
address_2 = models.CharField(max_length=256, blank=True)
country = models.CharField(max_length=64)
city = models.CharField(max_length=64)
phone_number = models.CharField(max_length=16)
Every field except for first_name
and last_name
in the Employee
model is considered sensitive data. This means that only the Profile
user with the linked employee_profile
, or a user with elevated privileges (e.g. an admin or HR staff), can access those fields.
Unfortunately, there is no simple way to control permissions down to the field level in DRF. Enter drf-confidential.
Let's suppose there are 2 users:
- amazhong is just a regular user without elevated privileges
- googe is a staff/admin with elevated privileges
GET /api/employees/ | |
amazhong | googe |
200 OK
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"first_name": "Ama",
"last_name": "Zhong",
"address_1": "440 Terry Ave N",
"address_2": "",
"country": "US",
"city": "Seattle",
"phone_number": "+12062661000"
},
{
"id": 2,
"first_name": "Goo",
"last_name": "Ge"
}
]
} |
200 OK
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"first_name": "Ama",
"last_name": "Zhong",
"address_1": "440 Terry Ave N",
"address_2": "",
"country": "US",
"city": "Seattle",
"phone_number": "+12062661000"
},
{
"id": 2,
"first_name": "Goo",
"last_name": "Ge",
"address_1": "1600 Amphitheatre Pkwy",
"address_2": "",
"country": "US",
"city": "Mountain View",
"phone_number": "+16502530000"
}
]
} |
GET /api/employees/1/ | |
amazhong | googe |
200 OK
{
"id": 1,
"first_name": "Ama",
"last_name": "Zhong",
"address_1": "440 Terry Ave N",
"address_2": "",
"country": "US",
"city": "Seattle",
"phone_number": "+12062661000"
} |
200 OK
{
"id": 1,
"first_name": "Ama",
"last_name": "Zhong",
"address_1": "440 Terry Ave N",
"address_2": "",
"country": "US",
"city": "Seattle",
"phone_number": "+12062661000"
} |
GET /api/employees/2/ | |
amazhong | googe |
200 OK
{
"id": 2,
"first_name": "Goo",
"last_name": "Ge"
} |
200 OK
{
"id": 2,
"first_name": "Goo",
"last_name": "Ge",
"address_1": "1600 Amphitheatre Pkwy",
"address_2": "",
"country": "US",
"city": "Mountain View",
"phone_number": "+16502530000"
} |
POST /api/employees/
{
"first_name": "Ah",
"last_name": "Poh",
"address_1": "One Apple Park Way",
"address_2": "",
"country": "US",
"city": "Cupertino",
"phone_number": "+14089961010"
} | |
amazhong | googe |
403 FORBIDDEN |
201 CREATED
{
"id": 3,
"first_name": "Ah",
"last_name": "Poh",
"address_1": "One Apple Park Way",
"address_2": "",
"country": "US",
"city": "Cupertino",
"phone_number": "+14089961010"
} |
PATCH /api/employees/1/
{
"address_1": "123 New Drive",
"phone_number": "+13214567890"
} | |
amazhong | googe |
200 OK
{
"id": 1,
"first_name": "Ama",
"last_name": "Zhong",
"address_1": "123 New Drive",
"address_2": "",
"country": "US",
"city": "Seattle",
"phone_number": "+13214567890"
} |
200 OK
{
"id": 1,
"first_name": "Ama",
"last_name": "Zhong",
"address_1": "123 New Drive",
"address_2": "",
"country": "US",
"city": "Seattle",
"phone_number": "+13214567890"
} |
PATCH /api/employees/2/
{
"address_1": "123 New Drive",
"phone_number": "+13214567890"
} | |
amazhong | googe |
403 FORBIDDEN |
200 OK
{
"id": 2,
"first_name": "Goo",
"last_name": "Ge",
"address_1": "123 New Drive",
"address_2": "",
"country": "US",
"city": "Mountain View",
"phone_number": "+13214567890"
} |
DELETE /api/employees/1/ | |
amazhong | googe |
204 NO CONTENT |
204 NO CONTENT |
DELETE /api/employees/2/ | |
amazhong | googe |
403 FORBIDDEN |
204 NO CONTENT |
Create a confidential permission on your model and python manage.py migrate
.
class Employee(models.Model):
... # fields defined earlier above
class Meta:
permissions = (
("view_sensitive_employee", "Can view employees' sensitive data"),
)
Add the ConfidentialFieldsMixin
to your serializer and define your confidential_fields
and user_relation
lookup.
from rest_framework import serializers
from drf_confidential.mixins import ConfidentialFieldsMixin
class EmployeeSerializer(ConfidentialFieldsMixin, serializers.ModelSerializer):
class Meta:
model = Employee
fields = "__all__"
confidential_fields = (
"address_1",
"address_2",
"country",
"city",
"phone_number",
)
user_relation = "login_account"
ConfidentialFieldsMixin
is configured to look for cases where either the request user is the model instance, the request user owns the model instance, the request user has a relation to the model instance, or the request user has the elevated permissions. The confidential_fields
meta attribute specifies which fields are considered sensitive. The user_relation
lookup specifies the relation of the model to the user model. In the model definitions above, the relation to the Profile
model from the Employee
model is through the back-reference, login_account
.
Add the ConfidentialFieldsPermission
as a permission class to the viewset.
from rest_framework.viewsets import ModelViewSet
from drf_confidential.permissions import ConfidentialFieldsPermission
class EmployeeViewSet(ModelViewSet):
serializer_class = EmployeeSerializer
queryset = Employee.objects.all()
permission_classes = [
... # your default permissions, e.g. IsAuthenticated
ConfidentialFieldsPermission
]
The permission follows the logic that a user must have either elevated permissions, have ownership, or have a relation to the model instance if they want to update
, partial_update
, or delete
. For create
, only users with elevated permissions are allowed. For retrieve
and list
, all users are allowed.