Package: django-denied
django-deniedNone shall pass.The Black Knightdjango-denied is an authorization system
for the Django web framework.
With django-denied,
every Django viewmust be explicitly allowed.
This design means
that developers have to make a choice
about authorization
for a view to work.In other words,
django-denied makes authorization a requirement
for every view in a Django project.Who should use this?This package is well suited for Django projects
that need to protect pages against unauthorized access normally.
If you are making a service
that requires user's to login
and restricts which data a user sees,
then django-denied may be a good fit for you.If your web application is meant to be open
for a large audience,
especially with lots of anonymous users,
then this package may be overkill for your needs.
A blog or content management system may not be a good candidate.InstallGet the package.pip install django-denieddjango-denied uses Django's built-inauthandadminapps.
These apps also depend on thecontenttypesapp.
Ensure that these apps are in yourINSTALL_APPSin your Django settings file.INSTALLED_APPS=["django.contrib.admin","django.contrib.auth","django.contrib.contenttypes",...,]Add theDeniedMiddleware.
This middleware does all the authorization checking.
The middleware depends on therequest.user,
so be sure to include itaftertheAuthenticationMiddleware.MIDDLEWARE=[...,"django.contrib.auth.middleware.AuthenticationMiddleware","denied.middleware.DeniedMiddleware",...,]Now you're ready to start.Usagedjango-denied has two primary modes
for handling views.allowauthorizeThese decorators are the main interface
of the package
and are described in the sections below.By default,
django-denied assumes that all users should be authenticated,
with the exception of allowed views or login pages.The login pages areThe page defined bysettings.LOGIN_URLandThe Django admin login defined at theadmin:loginroute.If you setLOGIN_URL,
django-denied expects the path form
of the setting
(e.g.,/accounts/login/)
rather than theurlname
(e.g.,accounts:login).Allowing viewsEvery app is likely to have views
that should be made accessible to unauthenticated users.
A company's about page, terms of service, and privacy policy are all good examples.Theallowdecorator is for marking a Django view as exempt
from the authorization checking done
by theDeniedMiddleware.This is an example of how you might create a terms of service view.# application/views.pyfromdenied.decoratorsimportallowfromdjango.shortcutsimportrender@allowdefterms_of_service(request):returnrender(request,"tos.html",{})Theallowdecorator has a secondary function.
Aside from allowing a single view,
the decorator can allow a set of views
that you would use withdjango.urls.path.This is necessary to permit third party apps
that have other views,
but are unaware of the django-denied system.This is an example of usingallowto permit the Django admin views
as well as the popular app,django-allauth.# project/urls.pyfromdenied.decoratorsimportallowfromdjango.contribimportadminfromdjango.urlsimportinclude,pathurlpatterns=[path("accounts/",allow(include("allauth.urls"))),path("admin/",allow(admin.site.urls)),]Note:
Even if you includeallowon a view or a set of views,
that does not mean that what you've allowed will suddenly
bypass any existing authentication or authorization checking.This is a feature, not a bug!login_required,permission_required,
and any other authentication or authorization checking
that pre-exist on views will remain.django-denied does not disable the security features
of other third party libraries.Authorizing viewsWith django-denied,
a Django view is authorized with theauthorizedecorator
and anauthorizerfunction.
An authorizer has a function signature offromdjango.httpimportHttpRequestdefexample_authorizer(request:HttpRequest,**view_kwargs:dict)->bool:...The authorizer evaluates the incoming request and view information
and should returnTrueif the request is authorized
orFalseis the request is not authorized.
Theview_kwargsinclude any data that was parsed out of the URL route.The authorizer acts as a declarative way
of showing what is authorized
for the view.fromdenied.decoratorsimportauthorizefrom.authorizersimportexample_authorizer@authorize(example_authorizer)defexample_view(request):...To useauthorizeon a class-based view,
you must attach the decorator to thedispatchmethod.fromdenied.decoratorsimportauthorizefromdjango.utils.decoratorsimportmethod_decoratorfromdjango.views.genericimportDetailViewfrom.authorizersimportexample_authorizerfrom.modelsimportExample@method_decorator(authorize(example_authorizer),"dispatch")classExampleDetail(DetailView):queryset=Example.objects.all()Built-in authorizersThe library includes built-in authorizers
for common cases.denied.authorizers.any_authorizedThis authorizer always evaluates toTrueand is the logical equivalent
tologin_requiredsince django-denied always enforces authentication checking.denied.authorizers.staff_authorizedThis authorizer only permits access whenuser.is_staff == True.staff_authorizedis equivalent tostaff_member_requiredfrom the Djangoadminapp.Authorizer exampleThis section shows a more complete example
of an authorizer
to give you a sense
of how django-denied works in practice.For our example,
we'll consider a project tracking application.
This is little more than a TODO list
that groups the tasks into projects.Here are the models.# application/models.pyfromdjango.contrib.auth.modelsimportUserfromdjango.dbimportmodelsclassProject(models.Model):owner=models.ForeignKey(User,on_delete=models.CASCADE)classTask(models.Model):project=models.ForeignKey(Project,on_delete=models.CASCADE)description=models.TextField()completed=models.BooleanField(default=False)For this simple system,
only project owners can do anything
with a task.
Let's create the authorizer for that.# application/authorizers.pydeftask_authorized(request,**view_kwargs):returnTask.objects.filter(project__owner=request.user,pk=view_kwargs["pk"],).exists()These are the URLs we want to support
with this authorizer.# application/urls.pyfromdjango.urlsimportpathfrom.viewsimporttask_detail,task_editurlpatterns=[path("tasks/<int:pk>/",task_detail,name="task_detail"),path("tasks/<int:pk>/edit/",task_detail,name="task_edit"),]Now we can set our views
and set their authorization.# application/views.pyfromdenied.decoratorsimportauthorizefromdjango.shortcutsimportrenderfrom.authorizersimporttask_authorizedfrom.modelsimportTask@authorize(task_authorized)deftask_detail(request,pk):task=Task.objects.get(pk=pk)returnrender(request,"task_detail.html",{"task":task})@authorize(task_authorized)deftask_edit(request,pk):task=Task.objects.get(pk=pk)returnrender(request,"task_edit.html",{"task":task})Since the authorizer handles the access control,
we can be confident that the task is safe to fetch
by its key alone.
Access control is pushed to the boundary of the view
so that the view's internal logic is about as simple
as you can make it.