Managing user permissions

A user gets his permissions directly or from one of the auth Groups he is a member of

Setting permissions

import grainy.const

# literal namespace with integer permission flag
user.grainy_permissions.add_permission("a.b.c", grainy.const.READ)

# literal namespace with string permission flag
user.grainy_permissions.add_permission("a.b.c", "r")

# same for groups
group.grainy_permissions.add_permission("a.b.c", "r")

Setting permissions in bulk

import grainy.const
import grainy.core

# set from PermissionSet instance
user.grainy_permissions.add_permission_set(
    grainy.core.PermissionSet({
        "a.b.c" : grainy.const.PERM_READ
    })
)

# set from dict (allows string permissions)
user.grainy_permissions.add_permission_set(
    {
        "a.b.c" : "r"
    }
)

Checking permissions

import grainy.const
from django_grainy.util import Permissions

user.grainy_permissions.add_permission_set(
    {
        "a.b.c" : "r"
    }
)

# we use the Permissions() wrapper as that allows
# us to do repeated permission checks for a user or group
# without having requery permissions for each check

perms = Permissions(user)

perms.check("a.b.c", grainy.const.PERM_READ) #True
perms.check("a.b.c.d", grainy.const.PERM_READ) #True
perms.check("a.b.c.d", grainy.const.PERM_READ | grainy.const.PERM_UPDATE) #False
perms.check("z.y.x", grainy.const.PERM_READ) #False
perms.check("a.b.c", "r") # True
perms.check("a.b.c.d", "r") # True
perms.check("a.b.c.d", "ru") # False
perms.check("x.y.z", "r") # False

# The `explicit` option allows us to require that excplicit
# permissions need to exist for a check to succeed, meaning
# having permissions to `a.b.c` will not grant permissions
# to `a.b.c.d` if `explicit`=True
perms.check("a.b.c.d", "r", explicit=True) # False

Custom permission holders

Sometimes you want something else than a user or group model to hold permissions - an APIkey implementation for example

from django.db import modelsi
from django_grainy.models import GrainyMixin, Permission, PermissionManager

class APIKey(models.Model):
    key = models.CharField(max_length=255)

class APIKeyPermission(Permission):
    # The `grainy_permissions` related name is important
    # so that we can pass instances of this model to
    # util.Permissions
    api_key = models.ForeignKey(APIKey, related_name="grainy_permissions", on_delete=models.CASCADE)

    # use the augmented object manager for permission handling
    objects = PermissionManager()



from django_grainy.util import Permissions

api_key = APIKey.objects.create(key="test")
api_key.grainy_permissions.add_permission("a.b.c", "r")

perms = Permissions(api_key)
assert api_key.check("a.b.c", "r")

Grainy Models

A django model can be initialized for grainy permissions using the grainy_model decorator.

from django.db import models
from grainy.decorators import grainy_model

# initialize grainy permissions for a model
# with automatic namespacing
@grainy_model()
class TestModelA(models.Model):
    name = models.CharField(max_length=255)

# initialize grainy permissions for a model
# with manual namespacing
@grainy_model(namespace="a.b.c")
class TestModelB(models.Model)
    name = models.CharField(max_length=255)

# initialize grainy permissions for a model
# with manual namespacing for both class
# and instance namespace
@grainy_model(
    # we want the same base namespace as model b
    namespace=TestModelB.Grainy.namespace(),

    # when checking against instances we want to
    # nest inside b
    namespace_instance = u"{namespace}.{instance.b.id}.b.{instance.id}"
)
class TestModelC(models.Model):
    b = models.ForeignKey(TestModelB)



Afterwards the model can be used directly to set or check permissions to it

from django_grainy.util import Permissions

# give user full permissions to model (any instance)
user.grainy_permissions.add(TestModelA, "crud")

# give user full permissions to a specific instance
instance = TestModelA.objects.get(id=1)
user.grainy_permissions.add(instance, "crud")

# check user permission on model class
perms = Permissions(user)
perms.check(TestModelA, "r") # True

# check user permission on instance
perms.check(instance, "r") # True

# check permissions to the name field
perms.check( (instance, "name"), "r")

# return all instances of the model according to permissions
instances = perms.instances(TestModelA, "r")

# this could also take a queryset
instances = perms.instances(TestModelA.objects.filter(id__gt=10), "r")

In the grainy_model decorator you can also specify if you want grainy to treat the model as a child of another grainy model by using the parent parameter.

This allows you to quickly chain namespaces with the child getting it's namespace prefixed with the parent's namespace

# starting with 1.7 you can also use the `parent` argument
# to quickly setup namespace inheritance for models

@grainy_model(namespace="x")
class ModelX(ModelA):
    pass

# We set parent to `x`, to indicate that we want to inherit
# the namespacing from there. It needs to point to ForeignKey or OneToOne
# field on the model that points to a model that is also grainy (ModelX
# in this example)
#
# ModelY will end up with the following instance namespace:
# "x.{x.pk}.custom.{pk}"
@grainy_model(namespace="custom", parent="x")
class ModelY(ModelA):
    # field name == grainy `parent` value
    x = models.ForeignKey(ModelX, related_name="y", on_delete=models.CASCADE)


# ModelZ will end up with the following instance namespace:
# "x.{y.x.pk}.custom.{y.pk}.z.{pk}"
@grainy_model(namespace="z", parent="y")
class ModelZ(ModelA):
    # field name == grainy `parent` value
    y = models.ForeignKey(ModelY, related_name="z", on_delete=models.CASCADE)

Grainy views

A django view can be initialized for grainy permissions using the grainy_view decorator.

When a view is made grainy it will automatically check for apropriate permissions to the specified namespace depending on the request method.

from django_grainy.decorators import grainy_view
from django.views import View as BaseView

# grainy function view
@grainy_view(namespace="a.b.c")
def view(request):
    return HttpResponse()

# grainy class view
@grainy_view(namespace="a.b.c")
class View(BaseView):

    # will check for READ perms to "a.b.c", otherwise fails with 403
    def get(self, request):
        return HttpResonse()

    # will check for CREATE perms to "a.b.c", otherwise fails with 403
    def post(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    def put(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    def patch(self, request):
        return HttpResponse()

    # will check for DELETE perms to "a.b.c", otherwise fails with 403
    def delete(self, request):
        return HttpResponse()


# grainy view with formatted namespace
@grainy_view(namespace="detail.{id}")
def detail_view(request, id):
    return HttpResponse()

# you can also pass through flags for permissions checks
@grainy_view(
    namespace="detail.{id}",
    # require that the user has explicitly set permissions for the namespace
    explicit=True,
    # ignore the user's superuser priviledges
    ignore_grant_all=True
)
def detail_view(request, id):
    return HttpResponse()

Manually decorate view response handlers

The grainy_view decorator simply calls the apropriate response decorator on all the response handlers in the view.

It follows that

from django.views import BaseView
from django_grainy.decorators import grainy_view

# grainy class view
@grainy_view(namespace="a.b.c")
class View(BaseView):

    # will check for READ perms to "a.b.c", otherwise fails with 403
    def get(self, request):
        return HttpResonse()

    # will check for CREATE perms to "a.b.c", otherwise fails with 403
    def post(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    def put(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    def patch(self, request):
        return HttpResponse()

    # will check for DELETE perms to "a.b.c", otherwise fails with 403
    def delete(self, request):
        return HttpResponse()

is the same as

from django.views import BaseView
from django_grainy.decorators import grainy_view_response

# grainy class view
class View(BaseView):

    # will check for READ perms to "a.b.c", otherwise fails with 403
    @grainy_view_response(namespace="a.b.c")
    def get(self, request):
        return HttpResonse()

    # will check for CREATE perms to "a.b.c", otherwise fails with 403
    @grainy_view_response(namespace="a.b.c")
    def post(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    @grainy_view_response(namespace="a.b.c")
    def put(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    @grainy_view_response(namespace="a.b.c")
    def patch(self, request):
        return HttpResponse()

    # will check for DELETE perms to "a.b.c", otherwise fails with 403
    @grainy_view_response(namespace="a.b.c")
    def delete(self, request):
        return HttpResponse()

You may also use both decorators

from django.views import BaseView
from django_grainy.decorators import grainy_view, grainy_view_response

# grainy class view
@grainy_view(namespace="a.b.c")
class View(BaseView):

    # will check for READ perms to "a.b.c", otherwise fails with 403
    def get(self, request):
        return HttpResonse()

    # will check for CREATE perms to "x.y.z", otherwise fails with 403
    @grainy_view_response(namespace="x.y.z")
    def post(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    def put(self, request):
        return HttpResponse()

    # will check for UPDATE perms to "a.b.c", otherwise fails with 403
    def patch(self, request):
        return HttpResponse()

    # will check for DELETE perms to "a.b.c", otherwise fails with 403
    def delete(self, request):
        return HttpResponse()

Rest Framework Integration

Use the grainy_rest_viewset decorator to apply grainy permissions to the output of a django_rest_framework ViewSet. This means any content that the user does not have permission to view will be dropped from the api response.

from rest_framework import serializers
from django_grainy.decorators import grainy_rest_viewset
from .models import TestModelA


# A serializer to test with
class ModelASerializer(serializers.HyperlinkedModelSerializer):

    # to test applying of permissions in nested data
    nested_dict = serializers.SerializerMethodField(required=False)

    class Meta:
        model = TestModelA
        fields = ('name',"id", "nested_dict")

    def get_nested_dict(self, obj):
        return {
            "secret" : {
                "hidden" : "data"
            },
            "something" : "public"
        }


@grainy_rest_viewset(
    namespace = "api.a",
    handlers = {
        # with application handlers we can tell grainy that this
        # namespace needs to have explicit permissions in order
        # to be accessed
        "nested_dict.secret" : { "explicit" : True }
    }
)
class ModelAViewSet(viewsets.ModelViewSet):
    queryset = TestModelA.objects.all()
    serializer_class = ModelASerializer

A user with READ permissions to api.a accessing this rest viewset would get this response

[{"name":"Test model 1","id":1,"nested_dict":{"something":"public"}}]

While a user with READ permissions to api.a and READ permissions to api.a.*.nested_dict.secret would get this response

[{"name":"Test model 1","id":1,"nested_dict":{"secret":{"hidden":"data"},"something":"public"}}]

Remote permission provider

This functionality is still a work in progress and subject to change.

Here is a quick and dirty example on how to set up a django project to be a remote grainy permission provider for another django project.

provider

Set up the grainy endpoints on the grainy permission provider instance

from django_grainy.remote import ProvideGet, ProvideLoad, Authenticator

class GrainyRequestAuthenticator(Authenticator):
    def authenticate(self, request):
        # pseudo-code for handling a token authentication
        handle_token_authentication(request)

urlpatterns += [
    # grainy
    path("grainy/get/<str:namespace>/", ProvideGet.as_view(authenticator_cls=GrainyRequestAuthenticator), name="grainy-get"),
    path("grainy/load/", ProvideLoad.as_view(authenticator_cls=GrainyRequestAuthenticator), name="grainy-load"),
]

For the sake of this example it is assumed that the provider instance runs at localhost:8000

receiver

In order to correctly setup authentication from the receiver django instance the provider django instance you will want to implement an authentication protocol. In this example we go with a straight forward token authentication. Note that the actual authentication logic is omitted as that is not really in the scope of this example.

from django.conf import settings
import django_grainy.remote

class RemotePermissions(django_grainy.remote.Permissions):

    def __init__(self, obj):
        super().__init__(
            obj,
            url_load="http://localhost:8000/grainy/load",
            url_get="http://localhost:8000/grainy/get",
        )

    def prepare_request(self, params, headers):
        try:
            key = self.obj.key_set.first().key
            headers.update(Authorization=f"token {key}")
        except AttributeError:
            pass

Then use it like you would the normal Permissions util

perms = RemotePermissions(user)
perms.check("a.b.c", PREAM_READ)