# Django Perms

[![Build Status](https://travis-ci.org/PSU-OIT-ARC/django-perms.svg?branch=develop)](https://travis-ci.org/PSU-OIT-ARC/django-perms)

`django-perms` provides a simple way to add authorization checks to
Django views. It takes a different approach to authorization compared to
packages like `django-guardian`. `django-perms` doesn't store any
permission state in the database. Instead, you define functions that
return `True` or `False` depending on whether the user is authorized to
do something. You are free to implement these functions however you see
fit. This allows for coarse permissions such as `user.is_staff` and more
fine-grained permissions based on, e.g., whether a user has access to
a specific model instance.

## Install

Add `'django-perms'` to `install_requires` and/or to `requirements.txt`.

Or install it directly with pip:

    pip install django-perms

If you want to use permissions template filters, add `'permissions'` to
your project's `INSTALLED_APPS` setting.

## Usage

Create a module named `perms` in the top level of your Django project.
In this module, create a permissions registry:

    # project/package/perms.py
    from permissions import PermissionsRegistry
    permissions = PermissionsRegistry()

NOTE: You can create the permissions registry anywhere; creating it in
a module name `perms` is just a convention.

If you have some permissions that will be useful across more than one of
your Django apps, the top level `perms` module is a good place to define
them:

    @permissions.register
    def is_staff(user):
        return user.is_staff

The `is_staff` permission can be applied to a view like so:

    from package.perms import permissions

    @permissions.required('is_staff')
    def some_view_only_accessible_by_staff(request):
        pass  # Do important stuff

Note that all permissions functions *must* take a User model object as
their first argument.

App-specific permissions are typically defined in a module named `perms`
in the app's package. For example, let's say you have a `widgets` app in
your project, then you might define some permissions like so:

    # project/package/widgets/perms.py
    from package.perms import permissions

    @permissions.register
    def can_create_widget(user):
        if user.is_staff:
            return True
        num_widgets = Widget.objects.filter(created_by=user).count()
        return num_widgets <= MAX_WIDGETS

    @permissions.register(model=Widget)
    def can_edit_widget(user, widget):
        if user.is_staff:
            return True
        return user == widget.owner

    @permissions.register(model=Widget, allow_anonymous=True)
    def can_view_widget(user, widget):
        if user.is_staff:
            return True
        if user.is_anonymous:
            return widget.is_public
        return user == widget.owner

Those widget permissions can be applied to your widget views like this:

    # project/package/widgets/views.py
    from django.http import HttpResponse
    from package.perms import permissions
    from .models import Widget

    @permissions.require('can_create_widget')
    def create_widget(request):
        # Create a widget here
        return HttpResponse('Widget created')

    @permissions.require('can_edit_widget', model=Widget)
    def edit_widget(request, widget_id):
        widget = get_object_or_404(Widget, pk=widget_id)
        # Edit the widget here
        return HttpResponse('Edited %s' % widget)

    # If you want to look up widgets by a field other than the primary
    # key field, you can specify the `field` option:
    @permissions.require('can_edit_widget', field='name')
    def edit_widget(request, name):
        widget = get_object_or_404(Widget, name=name)
        return HttpResponse('Edited %s' % widget)

If you're using class-based views, you can apply a permission to a class
or to a method:

    @permissions.require('xyz')
    class MyView(View):

        # All methods require the 'xyz' permission.

        def get(request):
            pass

        def post(request):
            pass

    class MyOtherView(View):

        # Only the post method requires the 'xyz' permission. Other
        # methods are unprotected.

        def get(request):
            pass

        @permissions.require('xyz')
        def post(request):
            pass

When a permission is registered, a corresponding template filter is also
created. Given the permissions registered above, you can do this in your
templates:

    {% load permissions %}

    {% if user|is_staff %}
        Hello, staff user.
    {% endif %}

    {% if user|can_create_widget %}
        You can create widgets!
    {% endif %}

    {% if user|can_edit_widget:widget %}
        You can edit this widget!
    {% endif %}

## Permissions Registered with a Model

When registering a permission that operates on a model, it's assumed
that the second argument of any view function requiring that permission
is the model lookup field (typically a primary key):

    @permissions.register(model=Fruit)
    def can_eat_fruit(user, fruit):
        return not user.is_allergic_to(fruit)

    @permissions.require('can_eat_fruit')
    def consume_fruit_view(request, fruit_id):
        pass # Consume the fruit if allowed

The `fruit_id` passed to the `consume_fruit_view` will be used by the
permissions machinery to load a `Fruit` object, which will be passed
into the `can_eat_fruit` permission function.

When using class-based views, the `self` arg is skipped when looking for
the lookup field.

## Allowing Staff and/or Superusers Access to All Views by Default

If you find yourself writing `if user.is_staff: return True` at the top
of most or all your permission functions, you can allow staff permission
to access all views by default:

    permissions = PermissionsRegistry(is_staff=True)

If you have a view that shouldn't be accessible to staff for some
reason, you can override the default `is_staff` setting on a per-
permission basis:

    @permissions.register(is_staff=False)
    def is_superuser(user):
        return user.is_superuser

There's an analogous `is_superuser` option.

## Anonymous Users

By default, anonymous users will be redirected to the login page. In
some cases, it may make sense to give anonymous users a chance to access
certain views.

For example, if you have an `Article` model with an `is_published`
field, you may want to allow anonymous users to access articles that
have been published. To allow this, you'd define and require a
permission like this:

    # perms.py
    @permissions.register(model=Article, allow_anonymous=True)
    def can_view_article(user, article):
        if article.is_published:
            return True
        if user.is_anonymous:
            return False  # Redirect to login page
        return user == article.owner

    # views.py
    @permissions.require('can_view_article')
    def article_view(request, article_id):
        pass  # Show article

If the permission check fails for an anonymous user, they will be
redirected to the login page.
