Skip to main content

View Permissions

The View Permissions package modifies Django's behaviour such that all views are "private" by default (i.e. inaccessible to all, whether authenticated or not) as opposed to "public" (i.e. accessible by anyone, whether authenticated or not), which is Django's default behaviour. This acts as a layer of defense against coding mistakes or misconfigurations, forcing developers to explicitly define permissions for each view before they can be accessed.

Access is provided using Django's built-in authentication and authorization system (e.g. using decorators such as django.contrib.auth.decorators.login_required), which should be familiar to existing Django developers.

Installation

The View Permissions package can be installed using the command below:

pip install govtech-csg-xcg-viewpermissions

Source code for the package can be found at https://github.com/GovTech-CSG/govtech-csg-xcg-viewpermissions.

Setup

This package depends on Django's built-in auth and admin apps, which in turn depend on the contenttypes app. As a pre-requisite, ensure that these apps are contained within the INSTALLED_APPS list in your Django settings file (settings.py). If your settings.py file was generated using the command django-admin startproject, it should contain these three apps by default.

./<PROJECT_NAME>/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
# Other apps...
]

Next, in order to activate the view permissions package, perform the following two steps:

  1. Add the govtech_csg_xcg.viewpermissions app to the INSTALLED_APPS list in settings.py.
./<PROJECT_NAME>/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
# Other apps...
'govtech_csg_xcg.viewpermissions',
]
  1. Add govtech_csg_xcg.viewpermissions.middleware.ViewPermissionsMiddleware to the MIDDLEWARE list in settings.py, placing it after django.contrib.auth.middleware.AuthenticationMiddleware.
./<PROJECT_NAME>/settings.py
MIDDLEWARE = [
# Other middleware...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'govtech_csg_xcg.viewpermissions.middleware.ViewPermissionsMiddleware',
# Other middleware...
]
Automatic security of Django views

The View Permissions package automatically prevents access to all Django views. If any views have not been explicitly marked as "public" or configured with a permission level, users will not be able to access the view.

Logging

The package uses its own logger with the name viewpermissions. By default, this logger uses a stream handler and emits messages with level INFO and above. The log message format looks something like this:

2023-06-22 09:50:43,348 [INFO][XCG][viewpermissions]: This is an informational message.

To safely override the default logging configuration, you can write code in settings.py to import the logger from the package and modify it directly. The example below sets the logger's level to DEBUG:

settings.py
from govtech_csg_xcg.viewpermissions import logger
import logging

logger.setLevel(logging.DEBUG)

For more information on the Python logging library, see the official documentation.

Usage

Basics

Once activated, all Django views will be "private" by default, which means no user (whether anonymous or authenticated, and regardless of permissions) will be able to access them. From a user perspective, an unauthenticated user attempting to access a "private" view will be redirected to the login page, while an authenticated user will receive a 403 Forbidden error. This is in contrast to Django's default behaviour, which treats each view as public unless declared otherwise.

To declare a view as "public" (i.e. any user, whether authenticated or unauthenticated, can access the view), import and add the govtech_csg_xcg.viewpermissions.decorators.public decorator to any function/class-based view.

./<APP_NAME>/views.py
from django.http import HttpResponse
from django.views import View
from django.utils.decorators import method_decorator
from govtech_csg_xcg.viewpermissions.decorators import public

# For function-based views
@public
def test_function_view(request):
return HttpResponse('Hello world!')

# For class-based views
@method_decorator(public, name='dispatch')
class TestClassView(View):

def test_view(self, request, *args, **kwargs):
return HttpResponse('Hello world!')

To apply the appropriate access control measures on each of your application's views, you can make use of Django's built-in authorization system. More specifically, Django provides ways to:

  1. Ensure only authenticated users can access a view (see official docs).
  2. Ensure only authenticated users with one or more specific permissions can access a view (see official docs).
  3. Ensure only authenticated users that pass a custom test can access a view (see official docs).

If any of these methods are used, your view will no longer be "private" (i.e. inaccessible to all users), but will instead be accessible to users who meet the conditions you set. A summary of the possible behaviours are shown in the table below:

No decoratorpublic@login_required@permission_required('perm_1')@permission_required('perm_2')
Unauthenticated user
User with perm_1
User with perm_2
No decorator

Note that "No decorator" itself is not a permission level, but rather a view that has not been secured with a decorator from the govtech-csg-xcg-viewpermissions XCG package.

Excluding specific views

If you want to exclude specific URLs from the effects of viewpermissions - for example, to whitelist a number of URL paths as public, rather than use the public decorator for each view - you can add the XCG_PERMISSION_IGNORE_PATHS setting in settings.py. The value of this setting should be a list of regex patterns. Any requests for paths that match these patterns will then be ignored by the middleware.

./<PROJECT_NAME>/settings.py
XCG_PERMISSION_IGNORE_PATHS = [
r'/example/raw/path',
r'/accounts/login/$',
r'/accounts/logout/$',
r'/accounts/signup/$',
r'^/admin/.*',
r'^/static/.*'
]

Alternatively, you can also exclude specific views by name rather than by path via the setting XCG_PERMISSION_IGNORE_VIEW_NAMES. The value of this setting should be a list of strings that correspond to view names. To be precise, a view's "name" here refers to the value of the name parameter when defining URL paths in urls.py (e.g. path('hello/', views.hello, name='hello-view'). Any requests for these views will be then be ignored by the middleware.

./<PROJECT_NAME>/settings.py
XCG_PERMISSION_IGNORE_VIEW_NAMES = [
'home',
'admin:index',
'admin:login',
'namespace:url_name',
]
Supplement existing permission levels

Take note that even if a view is excluded from View Permissions, any existing built-in access control mechanisms for your view still applies.

For example, if you have a view that is excluded using either XCG_PERMISSION_IGNORE_PATHS or XCG_PERMISSION_IGNORE_VIEW_NAMES, and the view function has the login_required decorator applied, the user still has to be authenticated in order to access your view.

Specifying the URL of your login page

As mentioned in the section on basic usage, activating viewpermissions causes unauthenticated users to be redirected to the login page. If left unspecified, Django will use the default login URL path, which is /accounts/login/. To override the default, assign the value of LOGIN_URL in settings.py to your app's login URL path.

./<PROJECT_NAME>/settings.py
LOGIN_URL = '/myapp/login/'
tip

This URL path will be automatically ignored by the viewpermissions middleware even if you don't explicitly include it in settings.XCG_PERMISSIONS_IGNORE_PATHS, so that your login page view remains public by default.

Specifying a 403 Forbidden HTML template

As mentioned in the section on basic usage, activating viewpermissions can cause authenticated users to be shown a 403 Forbidden error if they attempt to access a "private" view. This is achieved through the middleware raising a django.core.exceptions.PermissionDenied exception, which causes Django to look for and render a template named 403.html in your Django app's template directory (./<APP_NAME>/templates/). If Django cannot find such a template, it will serve a page with just the text "403 Forbidden".

You can create your own custom 403 error page by creating the 403.html template and placing it in the your app's template directory. It is also possible to place the template in your project's root template directory (./<PROJECT_NAME>/templates/) by following the instructions in the official Django documentation.