From f32a4a33db68fb43ee16a822ac9b4b4858f86eca Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Sun, 5 Mar 2017 11:59:21 -0500 Subject: [PATCH] Django permissions required at Nodes and Mutations --- docs/authorization.rst | 82 ++++++++++ graphene_django/auth/__init__.py | 0 graphene_django/auth/mixins.py | 64 ++++++++ graphene_django/tests/test_auth_mixins.py | 175 ++++++++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 graphene_django/auth/__init__.py create mode 100644 graphene_django/auth/mixins.py create mode 100644 graphene_django/tests/test_auth_mixins.py diff --git a/docs/authorization.rst b/docs/authorization.rst index 88f6b6a..0ddbf36 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -147,3 +147,85 @@ After this, you can use the new ``PrivateGraphQLView`` in ``urls.py``: ] .. _LoginRequiredMixin: https://docs.djangoproject.com/en/1.10/topics/auth/default/#the-loginrequired-mixin + +Adding permissions to Nodes +--------------------------- +If you want to user the auth django permissions to access a node, we need to inheritance +from ``AuthNodeMixin`` and define a required permissions in the node. This will return +a ``PermissionDenied`` is the user does not have the required permissions. + +.. code:: python + + from graphene_django.types import DjangoObjectType + from graphene_django.auth.mixins import AuthNodeMixin + from .models import Post + + class PostNode(AuthNodeMixin, DjangoObjectType): + _permission = 'app.add_post' + + class Meta: + model = Post + only_fields = ('title', 'content') + interfaces = (relay.Node, ) + +We can set multiple required permissions like this: + +.. code:: python + + from graphene_django.types import DjangoObjectType + from graphene_django.auth.mixins import AuthNodeMixin + from .models import Post + + class PostNode(AuthNodeMixin, DjangoObjectType): + _permission = ('app.add_post', 'app.delete_post',) + + class Meta: + model = Post + only_fields = ('title', 'content') + interfaces = (relay.Node, ) + +Adding permissions to Mutations +--------------------------- +If you want to user the auth django permissions to execute a mutation, we need to inheritance +from ``AuthMutationMixin`` and define a required permissions in the node. This will return +a ``PermissionDenied`` is the user does not have the required permissions. + +.. code:: python + + class CreatePet(AuthMutationMixin, graphene.Mutation): + _permission = 'app.create_pet' + pet = graphene.Field(PetNode) + + class Input: + name = graphene.String(required=True) + + @classmethod + def mutate(cls, root, input, context, info): + # Auth Required Virification + if cls.has_permision(context) is not True: + return cls.has_permision(context) + # End Auth + pet_name = input.get('name') + pet = Pet.objects.create(name=pet_name) + return CreatePet(pet=pet) + +We can set multiple required permissions like this: + +.. code:: python + + class CreatePet(AuthMutationMixin, graphene.Mutation): + _permission = ('app.add_pet', 'app.delete_pet') + pet = graphene.Field(PetNode) + + class Input: + name = graphene.String(required=True) + + @classmethod + def mutate(cls, root, input, context, info): + # Auth Required Virification + if cls.has_permision(context) is not True: + return cls.has_permision(context) + # End Auth + pet_name = input.get('name') + pet = Pet.objects.create(name=pet_name) + return CreatePet(pet=pet) diff --git a/graphene_django/auth/__init__.py b/graphene_django/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/graphene_django/auth/mixins.py b/graphene_django/auth/mixins.py new file mode 100644 index 0000000..6371835 --- /dev/null +++ b/graphene_django/auth/mixins.py @@ -0,0 +1,64 @@ +from django.core.exceptions import PermissionDenied + + +class AuthNodeMixin(): + _permission = '' + + @classmethod + def get_node(cls, id, context, info): + + def has_perm(object_instance): + if context is None: + return PermissionDenied('Permission Denied') + if type(context) is dict: + user = context.get('user', None) + if user is None: + return PermissionDenied('Permission Denied') + else: + user = context.user + if user.is_authenticated() is False: + return PermissionDenied('Permission Denied') + + if type(cls._permission) is tuple: + for permission in cls._permission: + if not user.has_perm(permission): + return False + if type(cls._permission) is str: + if not user.has_perm(cls._permission): + return False + return True + + try: + object_instance = cls._meta.model.objects.get(id=id) + except cls._meta.model.DoesNotExist: + return None + + if has_perm(object_instance): + return object_instance + return PermissionDenied('Permission Denied') + + +class AuthMutationMixin(): + _permission = '' + + @classmethod + def has_permision(cls, context): + user = None + if type(context) is dict: + user = context.get('user', None) + if user is None: + return PermissionDenied('Permission Denied') + else: + user = context.user + if user.is_authenticated() is False: + return PermissionDenied('Permission Denied') + + if type(cls._permission) is tuple: + for permission in cls._permission: + if not user.has_perm(permission): + return PermissionDenied('Permission Denied') + return True + if type(cls._permission) is str: + if user.has_perm(cls._permission): + return True + return PermissionDenied('Permission Denied') diff --git a/graphene_django/tests/test_auth_mixins.py b/graphene_django/tests/test_auth_mixins.py new file mode 100644 index 0000000..94bfd3a --- /dev/null +++ b/graphene_django/tests/test_auth_mixins.py @@ -0,0 +1,175 @@ +import graphene +from graphene import Schema, relay, ObjectType +from ..filter import DjangoFilterConnectionField +from django.test import TestCase, RequestFactory +from ..types import DjangoObjectType +from .models import Pet +from ..auth.mixins import AuthNodeMixin, AuthMutationMixin + + +class PetNode(AuthNodeMixin, DjangoObjectType): + _permission = 'app.view_pet' + + class Meta: + model = Pet + interfaces = (relay.Node, ) + + +class CreatePet(AuthMutationMixin, graphene.Mutation): + """ + Mutation for create user + example mutation: + mutation { + createPet(name: "Mila") { + pet { + id + name + } + } + } + """ + _permission = 'app.create_pet' + pet = graphene.Field(PetNode) + + class Input: + name = graphene.String(required=True) + + @classmethod + def mutate(cls, root, input, context, info): + if cls.has_permision(context) is not True: + return cls.has_permision(context) + pet_name = input.get('name') + pet = Pet.objects.create(name=pet_name) + return CreatePet(pet=pet) + + +class QueryRoot(ObjectType): + pet = relay.Node.Field(PetNode) + pets = DjangoFilterConnectionField(PetNode) + + +class MutationRoot(ObjectType): + create_pet = CreatePet.Field() + +schema = Schema(query=QueryRoot, mutation=MutationRoot) + + +class MockUserContext(object): + + def __init__(self, authenticated=True, is_staff=False, superuser=False, perms=()): + self.user = self + self.authenticated = authenticated + self.is_staff = is_staff + self.is_superuser = superuser + self.perms = perms + + def is_authenticated(self): + return self.authenticated + + def has_perm(self, check_perms): + if check_perms not in self.perms: + return False + return True + + +class AuthorizationTests(TestCase): + """ + This TestCase auth. + """ + + @classmethod + def setUpClass(cls): + super(AuthorizationTests, cls).setUpClass() + cls.schema = schema + cls.query_mutation = ''' + mutation {{ + createPet(name: "{name}") {{ + pet{{ + id + name + }} + }} + }} + ''' + cls.query_node = ''' + query { + pet(id: "UGV0Tm9kZTox"){ + id + name + } + } + ''' + + def setUp(self): + self.factory = RequestFactory() + pet_names = ['Mila', 'Kira'] + for name in pet_names: + Pet.objects.create(name=name) + self.anonymous = MockUserContext( + authenticated=False + ) + self.luke = MockUserContext( + authenticated=True, + perms=('app.view_pet', 'app.create_pet',) + ) + self.anakin = MockUserContext( + authenticated=True, + perms=('app.view_pet',) + ) + self.storm_tropper = MockUserContext( + authenticated=True, + perms=() + ) + + def test_mutation_anonymous(self): + """ + Making mutation with anonymous user + """ + print(self.luke.user) + result = self.schema.execute(self.query_mutation.format(name='Mila'), context_value={'user': self.anonymous}) + self.assertNotEqual(result.errors, []) + self.assertEqual(result.errors[0].message, 'Permission Denied') + + def test_mutation_non_permission(self): + """ + Making mutation with an user who does not have the permission + """ + result = self.schema.execute(self.query_mutation.format(name='Mila'), context_value={'user': self.anakin}) + self.assertNotEqual(result.errors, []) + self.assertEqual(result.errors[0].message, 'Permission Denied') + + def test_mutation_has_permission(self): + """ + Making mutation with an user who has the permission + """ + result = self.schema.execute(self.query_mutation.format(name='Mila'), context_value={'user': self.luke}) + self.assertEqual(result.errors, []) + + def test_query_anonymous(self): + """ + Making query with anonymous user + """ + result = self.schema.execute(self.query_node, context_value={'user': self.anonymous}) + print(result.errors) + print(result.data) + self.assertNotEqual(result.errors, []) + self.assertEqual(result.errors[0].message, 'Permission Denied') + + def test_query_non_permission(self): + """ + Making query with an user who does not have the permission + """ + result = self.schema.execute(self.query_node, context_value={'user': self.storm_tropper}) + print(result.errors) + print(result.data) + self.assertNotEqual(result.errors, []) + self.assertEqual(result.errors[0].message, 'Permission Denied') + + def test_query_has_permission(self): + """ + Making query with an user who has the permission + """ + result = self.schema.execute(self.query_node, context_value={'user': self.luke}) + print(result.errors) + print(result.data) + self.assertEqual(result.errors, [])