Package: django-generic-api-permissions
Django Generic API PermissionsDjango Generic API Permissions (DGAP) is a framework to make your Django Rest
Framework API user-extensible for common use cases. Specifically, it provides
a simple API for your users to define specific visibilities, permissions, and
validations.Assume you have an API service that implements a blogging backend. You have
aPost, and aCommentmodel.When deployed as a public blog, you want admins users to be able to post, and
authenticated users be able to comment. Everybody is allowed to read posts and
comments.But the same software should also be used in a company-internal deployment where
the rules are different: Anonymous users should not see anything, and you have
specific rules as to who can write posts.DGAP makes it easy to implement the blog once, and make the permissions,
visibilities, and validations custom to each deployment.ConceptsDGAP provides you with three configuration settings: Visibilities, Permissions,
and Validations.Thevisibilitiesare run when getting data from the API. They define, on a
per-user base, who can see which data.Thevalidationsare run on create and update operations, so
data can be checked and modified before the update takes place.Thepermissionsthen define what a user can do with a given (visible) piece of
data.Installation for app developersIf you want to integrate DGAP into your app, these are the steps you need.
Install DGAP (and add to your requirements files etc) first.pipinstalldjango-generic-api-permissionsThen, addgeneric_permissions.apps.GenericPermissionsConfigto yourINSTALLED_APPS:INSTALLED_APPS=(..."generic_permissions.apps.GenericPermissionsConfig",...)Visibility subsystemThe visibility part defines what you can see at all. Anything you cannot
see, you're implicitly also not allowed to modify. The visibility classes
define what you see depending on your roles, permissions, etc. Building
on top of this follow the permission classes (see below) that define
what you can do with the data you see.For the visibilities, extend your DRFViewSetclasses with theVisibilityViewMixin:# views.pyfromrest_framework.viewsetsimportModelViewSetfromgeneric_permissions.visibilitiesimportVisibilityViewMixinclassMyModelViewset(VisibilityViewMixin,ModelViewSet):serializer_class=MyModelSerializersqueryset=...Data leaks in REST Framework may happen if you use includes (or even only references) from a model the user may see to something that should be hidden.
To avoid such leaks, make sure to use a subclassed related field (either by creating your own using the providedVisibilityRelatedFieldMixin, or by using one of the provided types. See example below).
To set it for every relation in the serializer use DRFsserializer_related_fieldattribute in the serializer.It's important to be aware of potential issues when updating (PATCH) existing relationships, especially when some relationships are hidden due to visibility settings. If not handled correctly, the hidden relationships may be unintentionally removed during an update, resulting in only the new relationships being set.To avoid this, you must incorporate theVisibilitySerializerMixininto your serializer where you're using theVisibilityRelatedFieldMixinfor the relationship field. This ensures that hidden relationships are properly accounted for during updates.Remember to define theVisibilitySerializerMixinafter theValidatorMixin. This order is crucial because it ensures that validations are performed first, and only then are the relationships updated.This step is vital to maintain the integrity of your data and prevent accidental loss of hidden relationships.# serializers.pyfromrest_framework.viewsetsimportModelSerializerfromgeneric_permissions.visibilitiesimportVisibilityPrimaryKeyRelatedField,VisibilitySerializerMixinclassMyModelSerializers(VisibilitySerializerMixin,ModelSerializer):serializer_related_field=VisibilityPrimaryKeyRelatedFieldA few subclassed fields are provided for different types ofRelatedField:VisibilityPrimaryKeyRelatedFieldVisibilityResourceRelatedFieldVisibilitySerializerMethodResourceRelatedFieldIf a different relation field variation is needed extend it withVisibilityRelatedFieldMixin:fromgeneric_permissions.visibilitiesimportVisibilityRelatedFieldMixinclassCustomRelationField(VisibilityRelatedFieldMixin):passPermission subsystemSimilarly, for the permissions system, add thePermissionViewMixinto your
views:# views.pyfromrest_framework.viewsetsimportModelViewSetfromgeneric_permissions.permissionsimportPermissionViewMixinclassMyModelViewset(PermissionViewMixin,VisibilityViewMixin,ModelViewSet):serializer_class=...queryset=...You may use only one of the two mixins, or both, depending on your needs.Validation subsystemLast, for the validation system, you extend yourserializerwith a mixin:# serializers.pyfromrest_framework.serializersimportModelSerializerfromgeneric_permissions.serializersimportPermissionSerializerMixinfromgeneric_permissions.validationimportValidatorMixinfrommyappimportmodelsclassMyModelSerializer(ValidatorMixin,ModelSerializer):# my field definitions...classMeta:model=models.MyModelfields="__all__"Usage - for people deploying a DGAP-equipped appSay you have an blog you want to deploy that uses DGAP. You want public blog
posts, but the comment section should only be visible for authenticated users.
For this, you would define a custom visibility class that limits access
accordingly.VisibilitiesFirst, let's define the visibility class:# my_custom_visibilities.pyfromgeneric_permissions.visibilitiesimportfilter_queryset_forfrommy_app.modelsimportPost,CommentclassCustomVisibility:@filter_queryset_for(Post)deffilter_posts(self,queryset,request):# no filtering on blog postsreturnqueryset@filter_queryset_for(Comment)deffilter_comments(self,queryset,request):# Only authenticated users shall see commentsifrequest.user.is_authenticated:returnquerysetelse:returnqueryset.none()Once done, opensettings.pyand point theGENERIC_PERMISSIONS_VISIBILITY_CLASSESsetting to the class you just defined. It is a list of strings that name the
visibility classes.GENERIC_PERMISSIONS_VISIBILITY_CLASSES=['my_custom_visibilities.CustomVisibility']Note: The setting may be defined using env variables depending on the
project. In that case, set the value via that way instead.Some times, you have visibilities that you want to combine: Say one visibility
class provides read access for user group A, another class provides access for
user group B. You want to combine those in a simple way. For this, we have
provided you theUnionvisibility:formgeneric_permissions.visibilitiesimportUnionclassMyFirstVisibility:#  ...classMySecondVisibility:#  ...classResultingVisibility(Union):# Define a property `visibility_classes`. Those# will then be checked both, and if either one allows# an object to be seen, it will be visible to the user.visibility_classes=[MyFirstVisibility,MySecondVisibility]PermissionsPermission classes define who may perform which data mutation. They can be configured
viaGENERIC_PERMISSIONS_PERMISSION_CLASSES.To write custom permission classes, you create a simple class, and decorate the
methods that define the permissions accordingly.There are two types of methods in the permissions system:permission_for: Marks methods that define generic access permissions for a
given model. They are always checked first.
Those methods will receive one positional argument, namely therequestobjectobject_permission_for: Define whether access to a specific object shall be
granted. This called for all other operationsexceptcreation.
These methods will receive two positional arguments: First, therequestobject, and second, the model instance that is being accessed in the request.The following example carries on the Blog concept from above. We want only
admins to edit/update blog posts, and authenticated users to comment.
Nobody should be able to edit their comments.We also show the concept of combining two permission classes here. DGAP looks at
the whole inheritance tree to figure out the permissions, so you can leverage
that to avoid code duplication.You can find more information about therequestobject in theDjango documentationfromgeneric_permissions.permissionsimportpermission_for,object_permission_forfrommy_app.modelsimportPost,CommentclassOnlyAuthenticated:@permission_for(object)defhas_permission_default(self,request):# No permission is granted for any non-authenticated usersreturnrequest.user.is_authenticatedclassBlogPermissions:@permission_for(Comment)defhas_permission_for_comment(self,request):# comments can be added, but not updatedreturnrequest.method=='POST'@permission_for(Post)defhas_permission_for_post(self,request,instance):# Only admins can work on Postsreturn'admin'inrequest.user.groups@object_permission_for(Post)defhas_object_permission_for_post(self,request,instance):# Of the admins, changing a Post is only allowed to the author.returninstance.author==request.userThe following pre-defined classes are available:generic_permissions.permissions.AllowAny: allow any users to perform any mutation (default)generic_permissions.permissions.DenyAll: deny all operations to any object.
You can use this as a base class for your permissions - as long as you don't
allow something, it will be denied.Data validationOnce the permission to access or modify an object is granted, you may want to
apply some custom validation as well.In the example we're using here, we assume some user registration form. We want to
ensure that the username contains only lowercase letters.For this, you can use theGENERIC_PERMISSIONS_VALIDATION_CLASSESsetting. The settings is a
list of strings, representing a list of class names).Here's an example validator class that ensures the username is lower case.fromgeneric_permissions.validationimportvalidator_forfrommy_app.modelsimportUserclassLowercaseUsername:@validator_for(User)deflowercase_username(self,data,context):data["username"]=data["username"].lower()returndataThe@validator_fordecorator tells DGAP that the method shall
be called when aUseris modified. The data passed in is already
parsed and validated by the REST framework, and it is expected that
the method returns adictwith a compatible structure. You may alsoraise ValidationError("some message")if you don't want the validation
to succeed.
The second parameter,context, is adictcontaining the DRF context: Accesscontext['request']to get the request (if validation depends on the user,
for example).