From 296e4aed1d83af44c85eb29d84007090d3b2bdf8 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Wed, 25 Oct 2017 22:14:50 -0500 Subject: [PATCH 1/9] Auth init --- graphene_django/auth/decorators.py | 35 ++ graphene_django/auth/fields.py | 25 + graphene_django/auth/utils.py | 42 ++ graphene_django/tests/test_auth.py | 791 +++++++++++++++++++++++++++++ 4 files changed, 893 insertions(+) create mode 100644 graphene_django/auth/decorators.py create mode 100644 graphene_django/auth/fields.py create mode 100644 graphene_django/auth/utils.py create mode 100644 graphene_django/tests/test_auth.py diff --git a/graphene_django/auth/decorators.py b/graphene_django/auth/decorators.py new file mode 100644 index 000000000..d2fe6ad58 --- /dev/null +++ b/graphene_django/auth/decorators.py @@ -0,0 +1,35 @@ +from functools import wraps +from django.core.exceptions import PermissionDenied + +from .utils import has_perm, is_authorized_to_mutate_object, is_related_to_user + + +def node_require_permission(permissions, user_field=None): + def require_permission_decorator(func): + @wraps(func) + def func_wrapper(cls, info, id): + if user_field: + user_field is not None + if is_authorized_to_mutate_object(cls._meta.model, info.context.user, user_field): + return func(cls, info, id) + if has_perm(permissions=permissions, context=info.context): + return func(cls, info, id) + return PermissionDenied('Permission Denied') + return func_wrapper + return require_permission_decorator + + +def mutation_require_permission(permissions, model=None, user_field=None): + def require_permission_decorator(func): + @wraps(func) + def func_wrapper(cls, root, info, **input): + if model or user_field: + assert model is not None and user_field is not None + object_instance = cls._meta.model.objects.get(pk=id) + if is_related_to_user(object_instance, info.context.user, user_field): + return func(cls, root, info, **input) + if has_perm(permissions=permissions, context=info.context): + return func(cls, root, info, **input) + return cls(errors=PermissionDenied('Permission Denied')) + return func_wrapper + return require_permission_decorator diff --git a/graphene_django/auth/fields.py b/graphene_django/auth/fields.py new file mode 100644 index 000000000..108ac03e1 --- /dev/null +++ b/graphene_django/auth/fields.py @@ -0,0 +1,25 @@ + +from django.core.exceptions import PermissionDenied + +from .utils import has_perm +from ..fields import DjangoConnectionField + + +class AuthDjangoConnectionField(DjangoConnectionField): + + @classmethod + def connection_resolver(cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args): + """ + Resolve the required connection if the user in context has the permission required. If the user + does not have the required permission then returns a *Permission Denied* to the request. + """ + assert self._permissions is not None + if has_perm(self._permissions, info.context) is not True: + print(DjangoConnectionField) + return DjangoConnectionField.connection_resolver( + resolver, connection, [PermissionDenied('Permission Denied'), ], max_limit, + enforce_first_or_last, root, info, **args) + return super(AuthDjangoConnectionField, self).connection_resolver( + cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args) diff --git a/graphene_django/auth/utils.py b/graphene_django/auth/utils.py new file mode 100644 index 000000000..7230c3357 --- /dev/null +++ b/graphene_django/auth/utils.py @@ -0,0 +1,42 @@ +"""" +Auth utils module. + +Define some functios to authorize user to user mutations or nodes. +""" + + +def is_related_to_user(object_instance, user, field): + """Return True when the object_instance is related to user.""" + user_instance = getattr(object_instance, field, None) + if user: + if user_instance == user: + return True + return False + + +def is_authorized_to_mutate_object(model, user, field): + """Return True when the when the user is unauthorized.""" + object_instance = model.objects.get(pk=id) + if is_related_to_user(object_instance, user, field): + return True + return False + + +def has_perm(permissions, context): + """ + Validates if the user in the context has the permission required. + """ + print("context", type(context)) + if context is None: + return False + user = context.user + if user.is_authenticated() is False: + return False + + if type(permissions) is tuple: + print("permissions", permissions) + for permission in permissions: + print("User has perm", user.has_perm(permission)) + if not user.has_perm(permission): + return False + return True diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py new file mode 100644 index 000000000..0aa5b1a87 --- /dev/null +++ b/graphene_django/tests/test_auth.py @@ -0,0 +1,791 @@ +import datetime + +import pytest +from unittest.mock import Mock +from django.db import models +from django.utils.functional import SimpleLazyObject +from py.test import raises + +import graphene +from graphene.relay import Node + +from ..utils import DJANGO_FILTER_INSTALLED +from ..compat import MissingType, JSONField +from ..fields import DjangoConnectionField +from ..types import DjangoObjectType +from ..settings import graphene_settings +from .models import Article, Reporter +from ..auth.decorators import node_require_permission + +pytestmark = pytest.mark.django_db + + +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): + print("FUCK", check_perms not in self.perms) + if check_perms not in self.perms: + print("NO PERMS") + return False + print("HAS PERMS") + return True + + +class Context(object): + + def __init__(self, user): + self.user = user + + +class Request(object): + + def __init__(self, user): + self.context = Context(user) + + +user_authenticated = MockUserContext(authenticated=True, perms=('can_view_foo',)) +user_anonymous = MockUserContext(authenticated=False) +user_with_permissions = MockUserContext(authenticated=True, perms=('can_view_foo', 'can_view_bar')) + + +def test_anonymous_user(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + only_fields = ('id', ) + + @classmethod + @node_require_permission(permissions=('can_view_foo', )) + def get_node(cls, info, id): + return super(ReporterType, cls).get_node(info, id) + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, info): + print("THIS IS INFO----", info.context) + print("THIS IS INFO----", info.context is None) + print("User----", info.context.user.authenticated is True) + return SimpleLazyObject(lambda: Reporter(id=1)) + + schema = graphene.Schema(query=Query) + query = ''' + query { + reporter { + id + } + } + ''' + request = Context(user=user_anonymous) + result = schema.execute(query, context_value=request) + ReporterType.get_node(request, 1) + assert not result.errors + assert result.data == { + 'reporter': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=' + } + } + + +def test_user_authenticated(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + @node_require_permission(permissions=('can_view_foo', )) + def get_node(self, info, id): + 1/0 + print("THIS SHIT IS CALLED") + return super(ReporterType, self).get_node(self, info, id) + + class Query(graphene.ObjectType): + reporter = graphene.Field(ReporterType) + + def resolve_reporter(self, info): + return Reporter(first_name='ABA', last_name='X') + + query = ''' + query ReporterQuery { + reporter { + firstName, + lastName, + email + } + } + ''' + expected = { + 'reporter': { + 'firstName': 'ABA', + 'lastName': 'X', + 'email': '' + } + } + schema = graphene.Schema(query=Query) + request = Context(user=user_anonymous) + result = schema.execute(query, context_value=request) + assert not result.errors + assert result.data == expected + + +@pytest.mark.skipif(JSONField is MissingType, + reason="RangeField should exist") +def test_should_query_postgres_fields(): + from django.contrib.postgres.fields import IntegerRangeField, ArrayField, JSONField, HStoreField + + class Event(models.Model): + ages = IntegerRangeField(help_text='The age ranges') + data = JSONField(help_text='Data') + store = HStoreField() + tags = ArrayField(models.CharField(max_length=50)) + + class EventType(DjangoObjectType): + + class Meta: + model = Event + + class Query(graphene.ObjectType): + event = graphene.Field(EventType) + + def resolve_event(self, info): + return Event( + ages=(0, 10), + data={'angry_babies': True}, + store={'h': 'store'}, + tags=['child', 'angry', 'babies'] + ) + + schema = graphene.Schema(query=Query) + query = ''' + query myQuery { + event { + ages + tags + data + store + } + } + ''' + expected = { + 'event': { + 'ages': [0, 10], + 'tags': ['child', 'angry', 'babies'], + 'data': '{"angry_babies": true}', + 'store': '{"h": "store"}', + }, + } + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_node(): + # reset_global_registry() + # Node._meta.registry = get_global_registry() + + class ReporterNode(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + @classmethod + def get_node(cls, info, id): + return Reporter(id=2, first_name='Cookie Monster') + + def resolve_articles(self, info, **args): + return [Article(headline='Hi!')] + + class ArticleNode(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + + @classmethod + def get_node(cls, info, id): + return Article(id=1, headline='Article node', pub_date=datetime.date(2002, 3, 11)) + + class Query(graphene.ObjectType): + node = Node.Field() + reporter = graphene.Field(ReporterNode) + article = graphene.Field(ArticleNode) + + def resolve_reporter(self, info): + return Reporter(id=1, first_name='ABA', last_name='X') + + query = ''' + query ReporterQuery { + reporter { + id, + firstName, + articles { + edges { + node { + headline + } + } + } + lastName, + email + } + myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") { + id + ... on ReporterNode { + firstName + } + ... on ArticleNode { + headline + pubDate + } + } + } + ''' + expected = { + 'reporter': { + 'id': 'UmVwb3J0ZXJOb2RlOjE=', + 'firstName': 'ABA', + 'lastName': 'X', + 'email': '', + 'articles': { + 'edges': [{ + 'node': { + 'headline': 'Hi!' + } + }] + }, + }, + 'myArticle': { + 'id': 'QXJ0aWNsZU5vZGU6MQ==', + 'headline': 'Article node', + 'pubDate': '2002-03-11', + } + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_query_connectionfields(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + only_fields = ('articles', ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return [Reporter(id=1)] + + schema = graphene.Schema(query=Query) + query = ''' + query ReporterConnectionQuery { + allReporters { + pageInfo { + hasNextPage + } + edges { + node { + id + } + } + } + } + ''' + result = schema.execute(query) + assert not result.errors + assert result.data == { + 'allReporters': { + 'pageInfo': { + 'hasNextPage': False, + }, + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=' + } + }] + } + } + + +def test_should_keep_annotations(): + from django.db.models import ( + Count, + Avg, + ) + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + only_fields = ('articles', ) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + filter_fields = ('lang', ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + all_articles = DjangoConnectionField(ArticleType) + + def resolve_all_reporters(self, info, **args): + return Reporter.objects.annotate(articles_c=Count('articles')).order_by('articles_c') + + def resolve_all_articles(self, info, **args): + return Article.objects.annotate(import_avg=Avg('importance')).order_by('import_avg') + + schema = graphene.Schema(query=Query) + query = ''' + query ReporterConnectionQuery { + allReporters { + pageInfo { + hasNextPage + } + edges { + node { + id + } + } + } + allArticles { + pageInfo { + hasNextPage + } + edges { + node { + id + } + } + } + } + ''' + result = schema.execute(query) + assert not result.errors + + +@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, + reason="django-filter should be installed") +def test_should_query_node_filtering(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + filter_fields = ('lang', ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + Article.objects.create( + headline='Article Node 1', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 2', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='en' + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters { + edges { + node { + id + articles(lang: "es") { + edges { + node { + id + } + } + } + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=', + 'articles': { + 'edges': [{ + 'node': { + 'id': 'QXJ0aWNsZVR5cGU6MQ==' + } + }] + } + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, + reason="django-filter should be installed") +def test_should_query_node_multiple_filtering(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + filter_fields = ('lang', 'headline') + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + Article.objects.create( + headline='Article Node 1', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 2', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 3', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='en' + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters { + edges { + node { + id + articles(lang: "es", headline: "Article Node 1") { + edges { + node { + id + } + } + } + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=', + 'articles': { + 'edges': [{ + 'node': { + 'id': 'QXJ0aWNsZVR5cGU6MQ==' + } + }] + } + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_enforce_first_or_last(): + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': None + } + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'You must provide a `first` or `last` value to properly ' + 'paginate the `allReporters` connection.' + ) + assert result.data == expected + + +def test_should_error_if_first_is_greater_than_max(): + graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters(first: 101) { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': None + } + + result = schema.execute(query) + assert len(result.errors) == 1 + assert str(result.errors[0]) == ( + 'Requesting 101 records on the `allReporters` connection ' + 'exceeds the `first` limit of 100 records.' + ) + assert result.data == expected + + graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False + + +def test_should_query_promise_connectionfields(): + from promise import Promise + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + def resolve_all_reporters(self, info, **args): + return Promise.resolve([Reporter(id=1)]) + + schema = graphene.Schema(query=Query) + query = ''' + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=' + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected + + +def test_should_query_dataloader_fields(): + from promise import Promise + from promise.dataloader import DataLoader + + def article_batch_load_fn(keys): + queryset = Article.objects.filter(reporter_id__in=keys) + return Promise.resolve([ + [article for article in queryset if article.reporter_id == id] + for id in keys + ]) + + article_loader = DataLoader(article_batch_load_fn) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + use_connection = True + + articles = DjangoConnectionField(ArticleType) + + def resolve_articles(self, info, **args): + return article_loader.load(self.id) + + class Query(graphene.ObjectType): + all_reporters = DjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + Article.objects.create( + headline='Article Node 1', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 2', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='en' + ) + + schema = graphene.Schema(query=Query) + query = ''' + query ReporterPromiseConnectionQuery { + allReporters(first: 1) { + edges { + node { + id + articles(first: 2) { + edges { + node { + headline + } + } + } + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=', + 'articles': { + 'edges': [{ + 'node': { + 'headline': 'Article Node 1', + } + }, { + 'node': { + 'headline': 'Article Node 2' + } + }] + } + } + }] + } + } + + result = schema.execute(query) + assert not result.errors + assert result.data == expected From 39252bf54f58dde3ea17ba61a158b68301f13341 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 07:31:16 -0500 Subject: [PATCH 2/9] Update mode permissions required --- graphene_django/auth/decorators.py | 4 +- graphene_django/auth/utils.py | 4 +- graphene_django/tests/test_auth.py | 741 ++++------------------------- 3 files changed, 88 insertions(+), 661 deletions(-) diff --git a/graphene_django/auth/decorators.py b/graphene_django/auth/decorators.py index d2fe6ad58..18b4a8d0e 100644 --- a/graphene_django/auth/decorators.py +++ b/graphene_django/auth/decorators.py @@ -12,9 +12,11 @@ def func_wrapper(cls, info, id): user_field is not None if is_authorized_to_mutate_object(cls._meta.model, info.context.user, user_field): return func(cls, info, id) + print("Has Perm Result", has_perm(permissions=permissions, context=info.context)) if has_perm(permissions=permissions, context=info.context): + print("Node has persmissions") return func(cls, info, id) - return PermissionDenied('Permission Denied') + raise PermissionDenied('Permission Denied') return func_wrapper return require_permission_decorator diff --git a/graphene_django/auth/utils.py b/graphene_django/auth/utils.py index 7230c3357..51ed60e54 100644 --- a/graphene_django/auth/utils.py +++ b/graphene_django/auth/utils.py @@ -26,13 +26,15 @@ def has_perm(permissions, context): """ Validates if the user in the context has the permission required. """ - print("context", type(context)) if context is None: return False user = context.user if user.is_authenticated() is False: return False + print("Username", user.username) + print("Username Auth", user.is_authenticated()) + if type(permissions) is tuple: print("permissions", permissions) for permission in permissions: diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py index 0aa5b1a87..04c26818f 100644 --- a/graphene_django/tests/test_auth.py +++ b/graphene_django/tests/test_auth.py @@ -8,6 +8,7 @@ import graphene from graphene.relay import Node +from rest_framework import serializers from ..utils import DJANGO_FILTER_INSTALLED from ..compat import MissingType, JSONField @@ -15,15 +16,17 @@ from ..types import DjangoObjectType from ..settings import graphene_settings from .models import Article, Reporter -from ..auth.decorators import node_require_permission +from ..auth.decorators import node_require_permission, mutation_require_permission +from ..rest_framework.mutation import SerializerMutation pytestmark = pytest.mark.django_db class MockUserContext(object): - def __init__(self, authenticated=True, is_staff=False, superuser=False, perms=()): + def __init__(self, username='carlosmart', authenticated=True, is_staff=False, superuser=False, perms=()): self.user = self + self.username = username self.authenticated = authenticated self.is_staff = is_staff self.is_superuser = superuser @@ -33,7 +36,7 @@ def is_authenticated(self): return self.authenticated def has_perm(self, check_perms): - print("FUCK", check_perms not in self.perms) + print(self.username, self.perms) if check_perms not in self.perms: print("NO PERMS") return False @@ -47,361 +50,41 @@ def __init__(self, user): self.user = user -class Request(object): - - def __init__(self, user): - self.context = Context(user) - - -user_authenticated = MockUserContext(authenticated=True, perms=('can_view_foo',)) +user_authenticated = MockUserContext(authenticated=True) user_anonymous = MockUserContext(authenticated=False) user_with_permissions = MockUserContext(authenticated=True, perms=('can_view_foo', 'can_view_bar')) -def test_anonymous_user(): - class ReporterType(DjangoObjectType): +# Mutations +class MyFakeModel(models.Model): + cool_name = models.CharField(max_length=50) - class Meta: - model = Reporter - interfaces = (Node, ) - only_fields = ('id', ) - @classmethod - @node_require_permission(permissions=('can_view_foo', )) - def get_node(cls, info, id): - return super(ReporterType, cls).get_node(info, id) +class MyModelSerializer(serializers.ModelSerializer): + class Meta: + model = MyFakeModel + fields = '__all__' - class Query(graphene.ObjectType): - reporter = graphene.Field(ReporterType) - def resolve_reporter(self, info): - print("THIS IS INFO----", info.context) - print("THIS IS INFO----", info.context is None) - print("User----", info.context.user.authenticated is True) - return SimpleLazyObject(lambda: Reporter(id=1)) +class MySerializer(serializers.Serializer): + text = serializers.CharField() + model = MyModelSerializer() - schema = graphene.Schema(query=Query) - query = ''' - query { - reporter { - id - } - } - ''' - request = Context(user=user_anonymous) - result = schema.execute(query, context_value=request) - ReporterType.get_node(request, 1) - assert not result.errors - assert result.data == { - 'reporter': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=' - } - } + def create(self, validated_data): + return validated_data -def test_user_authenticated(): +def test_node_anonymous_user(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node, ) - @node_require_permission(permissions=('can_view_foo', )) - def get_node(self, info, id): - 1/0 - print("THIS SHIT IS CALLED") - return super(ReporterType, self).get_node(self, info, id) - - class Query(graphene.ObjectType): - reporter = graphene.Field(ReporterType) - - def resolve_reporter(self, info): - return Reporter(first_name='ABA', last_name='X') - - query = ''' - query ReporterQuery { - reporter { - firstName, - lastName, - email - } - } - ''' - expected = { - 'reporter': { - 'firstName': 'ABA', - 'lastName': 'X', - 'email': '' - } - } - schema = graphene.Schema(query=Query) - request = Context(user=user_anonymous) - result = schema.execute(query, context_value=request) - assert not result.errors - assert result.data == expected - - -@pytest.mark.skipif(JSONField is MissingType, - reason="RangeField should exist") -def test_should_query_postgres_fields(): - from django.contrib.postgres.fields import IntegerRangeField, ArrayField, JSONField, HStoreField - - class Event(models.Model): - ages = IntegerRangeField(help_text='The age ranges') - data = JSONField(help_text='Data') - store = HStoreField() - tags = ArrayField(models.CharField(max_length=50)) - - class EventType(DjangoObjectType): - - class Meta: - model = Event - - class Query(graphene.ObjectType): - event = graphene.Field(EventType) - - def resolve_event(self, info): - return Event( - ages=(0, 10), - data={'angry_babies': True}, - store={'h': 'store'}, - tags=['child', 'angry', 'babies'] - ) - - schema = graphene.Schema(query=Query) - query = ''' - query myQuery { - event { - ages - tags - data - store - } - } - ''' - expected = { - 'event': { - 'ages': [0, 10], - 'tags': ['child', 'angry', 'babies'], - 'data': '{"angry_babies": true}', - 'store': '{"h": "store"}', - }, - } - result = schema.execute(query) - assert not result.errors - assert result.data == expected - - -def test_should_node(): - # reset_global_registry() - # Node._meta.registry = get_global_registry() - - class ReporterNode(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - - @classmethod - def get_node(cls, info, id): - return Reporter(id=2, first_name='Cookie Monster') - - def resolve_articles(self, info, **args): - return [Article(headline='Hi!')] - - class ArticleNode(DjangoObjectType): - - class Meta: - model = Article - interfaces = (Node, ) - @classmethod + @node_require_permission(permissions=('can_view_foo', )) def get_node(cls, info, id): - return Article(id=1, headline='Article node', pub_date=datetime.date(2002, 3, 11)) - - class Query(graphene.ObjectType): - node = Node.Field() - reporter = graphene.Field(ReporterNode) - article = graphene.Field(ArticleNode) - - def resolve_reporter(self, info): - return Reporter(id=1, first_name='ABA', last_name='X') - - query = ''' - query ReporterQuery { - reporter { - id, - firstName, - articles { - edges { - node { - headline - } - } - } - lastName, - email - } - myArticle: node(id:"QXJ0aWNsZU5vZGU6MQ==") { - id - ... on ReporterNode { - firstName - } - ... on ArticleNode { - headline - pubDate - } - } - } - ''' - expected = { - 'reporter': { - 'id': 'UmVwb3J0ZXJOb2RlOjE=', - 'firstName': 'ABA', - 'lastName': 'X', - 'email': '', - 'articles': { - 'edges': [{ - 'node': { - 'headline': 'Hi!' - } - }] - }, - }, - 'myArticle': { - 'id': 'QXJ0aWNsZU5vZGU6MQ==', - 'headline': 'Article node', - 'pubDate': '2002-03-11', - } - } - schema = graphene.Schema(query=Query) - result = schema.execute(query) - assert not result.errors - assert result.data == expected - - -def test_should_query_connectionfields(): - class ReporterType(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - only_fields = ('articles', ) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - def resolve_all_reporters(self, info, **args): - return [Reporter(id=1)] - - schema = graphene.Schema(query=Query) - query = ''' - query ReporterConnectionQuery { - allReporters { - pageInfo { - hasNextPage - } - edges { - node { - id - } - } - } - } - ''' - result = schema.execute(query) - assert not result.errors - assert result.data == { - 'allReporters': { - 'pageInfo': { - 'hasNextPage': False, - }, - 'edges': [{ - 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=' - } - }] - } - } - - -def test_should_keep_annotations(): - from django.db.models import ( - Count, - Avg, - ) - - class ReporterType(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - only_fields = ('articles', ) - - class ArticleType(DjangoObjectType): - - class Meta: - model = Article - interfaces = (Node, ) - filter_fields = ('lang', ) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - all_articles = DjangoConnectionField(ArticleType) - - def resolve_all_reporters(self, info, **args): - return Reporter.objects.annotate(articles_c=Count('articles')).order_by('articles_c') - - def resolve_all_articles(self, info, **args): - return Article.objects.annotate(import_avg=Avg('importance')).order_by('import_avg') - - schema = graphene.Schema(query=Query) - query = ''' - query ReporterConnectionQuery { - allReporters { - pageInfo { - hasNextPage - } - edges { - node { - id - } - } - } - allArticles { - pageInfo { - hasNextPage - } - edges { - node { - id - } - } - } - } - ''' - result = schema.execute(query) - assert not result.errors - - -@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, - reason="django-filter should be installed") -def test_should_query_node_filtering(): - class ReporterType(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - - class ArticleType(DjangoObjectType): - - class Meta: - model = Article - interfaces = (Node, ) - filter_fields = ('lang', ) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) + return super(ReporterType, cls).get_node(info, id) r = Reporter.objects.create( first_name='John', @@ -409,163 +92,38 @@ class Query(graphene.ObjectType): email='johndoe@example.com', a_choice=1 ) - Article.objects.create( - headline='Article Node 1', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='es' - ) - Article.objects.create( - headline='Article Node 2', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='en' - ) - - schema = graphene.Schema(query=Query) - query = ''' - query NodeFilteringQuery { - allReporters { - edges { - node { - id - articles(lang: "es") { - edges { - node { - id - } - } - } - } - } - } - } - ''' - - expected = { - 'allReporters': { - 'edges': [{ - 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=', - 'articles': { - 'edges': [{ - 'node': { - 'id': 'QXJ0aWNsZVR5cGU6MQ==' - } - }] - } - } - }] - } - } - - result = schema.execute(query) - assert not result.errors - assert result.data == expected - - -@pytest.mark.skipif(not DJANGO_FILTER_INSTALLED, - reason="django-filter should be installed") -def test_should_query_node_multiple_filtering(): - class ReporterType(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - - class ArticleType(DjangoObjectType): - - class Meta: - model = Article - interfaces = (Node, ) - filter_fields = ('lang', 'headline') class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - r = Reporter.objects.create( - first_name='John', - last_name='Doe', - email='johndoe@example.com', - a_choice=1 - ) - Article.objects.create( - headline='Article Node 1', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='es' - ) - Article.objects.create( - headline='Article Node 2', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='es' - ) - Article.objects.create( - headline='Article Node 3', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='en' - ) + reporter = Node.Field(ReporterType) schema = graphene.Schema(query=Query) query = ''' - query NodeFilteringQuery { - allReporters { - edges { - node { - id - articles(lang: "es", headline: "Article Node 1") { - edges { - node { - id - } - } - } - } - } - } + query { + reporter(id: "UmVwb3J0ZXJUeXBlOjE="){ + firstName + } } ''' - - expected = { - 'allReporters': { - 'edges': [{ - 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=', - 'articles': { - 'edges': [{ - 'node': { - 'id': 'QXJ0aWNsZVR5cGU6MQ==' - } - }] - } - } - }] - } + context = Context(user=user_anonymous) + request = Mock(context=context, user=user_anonymous) + result = schema.execute(query, context_value=request) + assert result.errors + assert result.data == { + 'reporter': None } - result = schema.execute(query) - assert not result.errors - assert result.data == expected - - -def test_should_enforce_first_or_last(): - graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True +def test_node_authenticated_user_no_permissions(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node, ) - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) + @classmethod + @node_require_permission(permissions=('can_view_foo', )) + def get_node(cls, info, id): + return super(ReporterType, cls).get_node(info, id) r = Reporter.objects.create( first_name='John', @@ -574,43 +132,37 @@ class Query(graphene.ObjectType): a_choice=1 ) + class Query(graphene.ObjectType): + reporter = Node.Field(ReporterType) + schema = graphene.Schema(query=Query) query = ''' - query NodeFilteringQuery { - allReporters { - edges { - node { - id - } - } - } + query { + reporter(id: "UmVwb3J0ZXJUeXBlOjE="){ + firstName + } } ''' - - expected = { - 'allReporters': None + context = Context(user=user_authenticated) + request = Mock(context=context, user=user_authenticated) + result = schema.execute(query, context_value=request) + assert result.errors + assert result.data == { + 'reporter': None } - result = schema.execute(query) - assert len(result.errors) == 1 - assert str(result.errors[0]) == ( - 'You must provide a `first` or `last` value to properly ' - 'paginate the `allReporters` connection.' - ) - assert result.data == expected - - -def test_should_error_if_first_is_greater_than_max(): - graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100 +def test_node_authenticated_user_with_permissions(): class ReporterType(DjangoObjectType): class Meta: model = Reporter interfaces = (Node, ) - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) + @classmethod + @node_require_permission(permissions=('can_view_foo', )) + def get_node(cls, info, id): + return super(ReporterType, cls).get_node(info, id) r = Reporter.objects.create( first_name='John', @@ -619,173 +171,44 @@ class Query(graphene.ObjectType): a_choice=1 ) - schema = graphene.Schema(query=Query) - query = ''' - query NodeFilteringQuery { - allReporters(first: 101) { - edges { - node { - id - } - } - } - } - ''' - - expected = { - 'allReporters': None - } - - result = schema.execute(query) - assert len(result.errors) == 1 - assert str(result.errors[0]) == ( - 'Requesting 101 records on the `allReporters` connection ' - 'exceeds the `first` limit of 100 records.' - ) - assert result.data == expected - - graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False - - -def test_should_query_promise_connectionfields(): - from promise import Promise - - class ReporterType(DjangoObjectType): - - class Meta: - model = Reporter - interfaces = (Node, ) - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - def resolve_all_reporters(self, info, **args): - return Promise.resolve([Reporter(id=1)]) + reporter = Node.Field(ReporterType) schema = graphene.Schema(query=Query) query = ''' - query ReporterPromiseConnectionQuery { - allReporters(first: 1) { - edges { - node { - id - } - } - } + query { + reporter(id: "UmVwb3J0ZXJUeXBlOjE="){ + firstName + } } ''' - - expected = { - 'allReporters': { - 'edges': [{ - 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=' - } - }] + context = Context(user=user_with_permissions) + request = Mock(context=context, user=user_with_permissions) + result = schema.execute(query, context_value=request) + assert not result.errors + assert result.data == { + 'reporter': { + 'firstName': 'John' } } - result = schema.execute(query) - assert not result.errors - assert result.data == expected - - -def test_should_query_dataloader_fields(): - from promise import Promise - from promise.dataloader import DataLoader - - def article_batch_load_fn(keys): - queryset = Article.objects.filter(reporter_id__in=keys) - return Promise.resolve([ - [article for article in queryset if article.reporter_id == id] - for id in keys - ]) - article_loader = DataLoader(article_batch_load_fn) - - class ArticleType(DjangoObjectType): +def test_mutate_and_get_payload_success(): + class MyMutation(SerializerMutation): class Meta: - model = Article - interfaces = (Node, ) + serializer_class = MySerializer - class ReporterType(DjangoObjectType): + @mutation_require_permission(permissions=('can_view_foo', )) + def mutate_and_get_payload(cls, root, info, **input): + return super(MyMutation, cls).mutate_and_get_payload(root, info, **input) - class Meta: - model = Reporter - interfaces = (Node, ) - use_connection = True - - articles = DjangoConnectionField(ArticleType) - - def resolve_articles(self, info, **args): - return article_loader.load(self.id) - - class Query(graphene.ObjectType): - all_reporters = DjangoConnectionField(ReporterType) - - r = Reporter.objects.create( - first_name='John', - last_name='Doe', - email='johndoe@example.com', - a_choice=1 - ) - Article.objects.create( - headline='Article Node 1', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='es' - ) - Article.objects.create( - headline='Article Node 2', - pub_date=datetime.date.today(), - reporter=r, - editor=r, - lang='en' - ) - - schema = graphene.Schema(query=Query) - query = ''' - query ReporterPromiseConnectionQuery { - allReporters(first: 1) { - edges { - node { - id - articles(first: 2) { - edges { - node { - headline - } - } - } - } - } - } - } - ''' - - expected = { - 'allReporters': { - 'edges': [{ - 'node': { - 'id': 'UmVwb3J0ZXJUeXBlOjE=', - 'articles': { - 'edges': [{ - 'node': { - 'headline': 'Article Node 1', - } - }, { - 'node': { - 'headline': 'Article Node 2' - } - }] - } - } - }] + context = Context(user=user_with_permissions) + request = Mock(context=context, user=user_with_permissions) + result = MyMutation.mutate_and_get_payload(None, request, **{ + 'text': 'value', + 'model': { + 'cool_name': 'other_value' } - } - - result = schema.execute(query) - assert not result.errors - assert result.data == expected + }) + assert result.errors is None From af4bb534154176faab8d7dba673102987622fd9e Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 12:03:28 -0500 Subject: [PATCH 3/9] created decorators to mutation and connection --- graphene_django/auth/decorators.py | 36 +++-- graphene_django/auth/fields.py | 25 --- graphene_django/auth/utils.py | 15 +- graphene_django/tests/test_auth.py | 252 ++++++++++++++++++++++++++++- 4 files changed, 276 insertions(+), 52 deletions(-) delete mode 100644 graphene_django/auth/fields.py diff --git a/graphene_django/auth/decorators.py b/graphene_django/auth/decorators.py index 18b4a8d0e..f0cbfb40f 100644 --- a/graphene_django/auth/decorators.py +++ b/graphene_django/auth/decorators.py @@ -1,37 +1,45 @@ from functools import wraps from django.core.exceptions import PermissionDenied +from ..fields import DjangoConnectionField -from .utils import has_perm, is_authorized_to_mutate_object, is_related_to_user +from .utils import has_perm -def node_require_permission(permissions, user_field=None): +def node_require_permission(permissions): def require_permission_decorator(func): @wraps(func) def func_wrapper(cls, info, id): - if user_field: - user_field is not None - if is_authorized_to_mutate_object(cls._meta.model, info.context.user, user_field): - return func(cls, info, id) - print("Has Perm Result", has_perm(permissions=permissions, context=info.context)) if has_perm(permissions=permissions, context=info.context): - print("Node has persmissions") return func(cls, info, id) raise PermissionDenied('Permission Denied') return func_wrapper return require_permission_decorator -def mutation_require_permission(permissions, model=None, user_field=None): +def mutation_require_permission(permissions): def require_permission_decorator(func): @wraps(func) def func_wrapper(cls, root, info, **input): - if model or user_field: - assert model is not None and user_field is not None - object_instance = cls._meta.model.objects.get(pk=id) - if is_related_to_user(object_instance, info.context.user, user_field): - return func(cls, root, info, **input) if has_perm(permissions=permissions, context=info.context): return func(cls, root, info, **input) return cls(errors=PermissionDenied('Permission Denied')) return func_wrapper return require_permission_decorator + + +def connection_require_permission(permissions): + def require_permission_decorator(func): + @wraps(func) + def func_wrapper( + cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args): + if has_perm(permissions=permissions, context=info.context): + print("Has Perms") + return func( + cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args) + return DjangoConnectionField.connection_resolver( + resolver, connection, [PermissionDenied('Permission Denied'), ], max_limit, + enforce_first_or_last, root, info, **args) + return func_wrapper + return require_permission_decorator diff --git a/graphene_django/auth/fields.py b/graphene_django/auth/fields.py deleted file mode 100644 index 108ac03e1..000000000 --- a/graphene_django/auth/fields.py +++ /dev/null @@ -1,25 +0,0 @@ - -from django.core.exceptions import PermissionDenied - -from .utils import has_perm -from ..fields import DjangoConnectionField - - -class AuthDjangoConnectionField(DjangoConnectionField): - - @classmethod - def connection_resolver(cls, resolver, connection, default_manager, max_limit, - enforce_first_or_last, root, info, **args): - """ - Resolve the required connection if the user in context has the permission required. If the user - does not have the required permission then returns a *Permission Denied* to the request. - """ - assert self._permissions is not None - if has_perm(self._permissions, info.context) is not True: - print(DjangoConnectionField) - return DjangoConnectionField.connection_resolver( - resolver, connection, [PermissionDenied('Permission Denied'), ], max_limit, - enforce_first_or_last, root, info, **args) - return super(AuthDjangoConnectionField, self).connection_resolver( - cls, resolver, connection, default_manager, max_limit, - enforce_first_or_last, root, info, **args) diff --git a/graphene_django/auth/utils.py b/graphene_django/auth/utils.py index 51ed60e54..38a90f712 100644 --- a/graphene_django/auth/utils.py +++ b/graphene_django/auth/utils.py @@ -14,7 +14,7 @@ def is_related_to_user(object_instance, user, field): return False -def is_authorized_to_mutate_object(model, user, field): +def is_authorized_to_mutate_object(model, user, id, field): """Return True when the when the user is unauthorized.""" object_instance = model.objects.get(pk=id) if is_related_to_user(object_instance, user, field): @@ -26,19 +26,14 @@ def has_perm(permissions, context): """ Validates if the user in the context has the permission required. """ + assert permissions if context is None: return False user = context.user if user.is_authenticated() is False: return False - print("Username", user.username) - print("Username Auth", user.is_authenticated()) - - if type(permissions) is tuple: - print("permissions", permissions) - for permission in permissions: - print("User has perm", user.has_perm(permission)) - if not user.has_perm(permission): - return False + for permission in permissions: + if not user.has_perm(permission): + return False return True diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py index 04c26818f..7adb883bb 100644 --- a/graphene_django/tests/test_auth.py +++ b/graphene_django/tests/test_auth.py @@ -16,7 +16,8 @@ from ..types import DjangoObjectType from ..settings import graphene_settings from .models import Article, Reporter -from ..auth.decorators import node_require_permission, mutation_require_permission +from ..auth.decorators import node_require_permission, mutation_require_permission, connection_require_permission +from ..auth.utils import is_related_to_user, is_authorized_to_mutate_object from ..rest_framework.mutation import SerializerMutation pytestmark = pytest.mark.django_db @@ -66,6 +67,12 @@ class Meta: fields = '__all__' +class ArticleSerializer(serializers.ModelSerializer): + class Meta: + model = Article + fields = '__all__' + + class MySerializer(serializers.Serializer): text = serializers.CharField() model = MyModelSerializer() @@ -74,6 +81,58 @@ def create(self, validated_data): return validated_data +def test_is_related_to_user(): + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + r2 = Reporter.objects.create( + first_name='Michael', + last_name='Doe', + email='mdoe@example.com', + a_choice=1 + ) + a = Article.objects.create( + headline='Article Node 1', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + result_1 = is_related_to_user(a, r, 'reporter') + result_2 = is_related_to_user(a, r2, 'reporter') + assert result_1 is True + assert result_2 is False + + +def test_is_authorized_to_mutate_object(): + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + r2 = Reporter.objects.create( + first_name='Michael', + last_name='Doe', + email='mdoe@example.com', + a_choice=1 + ) + Article.objects.create( + headline='Article Node 1', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + result_1 = is_authorized_to_mutate_object(Article, r, 1, 'reporter') + result_2 = is_authorized_to_mutate_object(Article, r2, 1, 'reporter') + assert result_1 is True + assert result_2 is False + + def test_node_anonymous_user(): class ReporterType(DjangoObjectType): @@ -113,6 +172,43 @@ class Query(graphene.ObjectType): } +def test_node_no_context(): + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + @classmethod + @node_require_permission(permissions=('can_view_foo', )) + def get_node(cls, info, id): + return super(ReporterType, cls).get_node(info, id) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + + class Query(graphene.ObjectType): + reporter = Node.Field(ReporterType) + + schema = graphene.Schema(query=Query) + query = ''' + query { + reporter(id: "UmVwb3J0ZXJUeXBlOjE="){ + firstName + } + } + ''' + result = schema.execute(query) + assert result.errors + assert result.data == { + 'reporter': None + } + + def test_node_authenticated_user_no_permissions(): class ReporterType(DjangoObjectType): @@ -193,22 +289,172 @@ class Query(graphene.ObjectType): } -def test_mutate_and_get_payload_success(): +def test_auth_mutate_and_get_payload_anonymous(): class MyMutation(SerializerMutation): class Meta: serializer_class = MySerializer + @classmethod + @mutation_require_permission(permissions=('can_view_foo', )) + def mutate_and_get_payload(cls, root, info, **input): + return super(MyMutation, cls).mutate_and_get_payload(root, info, **input) + + context = Context(user=user_anonymous) + request = Mock(context=context, user=user_anonymous) + result = MyMutation.mutate_and_get_payload(root=None, info=request, **{ + 'text': 'value', + 'model': { + 'cool_name': 'other_value' + } + }) + assert result.errors is not None + + +def test_auth_mutate_and_get_payload_autheticated(): + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MySerializer + + @classmethod + @mutation_require_permission(permissions=('can_view_foo', )) + def mutate_and_get_payload(cls, root, info, **input): + return super(MyMutation, cls).mutate_and_get_payload(root, info, **input) + + context = Context(user=user_authenticated) + request = Mock(context=context, user=user_authenticated) + result = MyMutation.mutate_and_get_payload(root=None, info=request, **{ + 'text': 'value', + 'model': { + 'cool_name': 'other_value' + } + }) + assert result.errors is not None + + +def test_auth_mutate_and_get_payload_with_permissions(): + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = MySerializer + + @classmethod @mutation_require_permission(permissions=('can_view_foo', )) def mutate_and_get_payload(cls, root, info, **input): return super(MyMutation, cls).mutate_and_get_payload(root, info, **input) context = Context(user=user_with_permissions) request = Mock(context=context, user=user_with_permissions) - result = MyMutation.mutate_and_get_payload(None, request, **{ + result = MyMutation.mutate_and_get_payload(root=None, info=request, **{ 'text': 'value', 'model': { 'cool_name': 'other_value' } }) assert result.errors is None + + +def test_auth_connection(): + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + class ArticleType(DjangoObjectType): + + class Meta: + model = Article + interfaces = (Node, ) + filter_fields = ('lang', 'headline') + + class MyAuthDjangoConnectionField(DjangoConnectionField): + + @classmethod + @connection_require_permission(permissions=('can_view_foo', )) + def connection_resolver(cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args): + return super(MyAuthDjangoConnectionField, cls).connection_resolver( + resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args) + + class Query(graphene.ObjectType): + all_reporters = MyAuthDjangoConnectionField(ReporterType) + + r = Reporter.objects.create( + first_name='John', + last_name='Doe', + email='johndoe@example.com', + a_choice=1 + ) + Article.objects.create( + headline='Article Node 1', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 2', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='es' + ) + Article.objects.create( + headline='Article Node 3', + pub_date=datetime.date.today(), + reporter=r, + editor=r, + lang='en' + ) + + schema = graphene.Schema(query=Query) + query = ''' + query NodeFilteringQuery { + allReporters { + edges { + node { + id + articles(lang: "es", headline: "Article Node 1") { + edges { + node { + id + } + } + } + } + } + } + } + ''' + + expected = { + 'allReporters': { + 'edges': [{ + 'node': { + 'id': 'UmVwb3J0ZXJUeXBlOjE=', + 'articles': { + 'edges': [{ + 'node': { + 'id': 'QXJ0aWNsZVR5cGU6MQ==' + } + }] + } + } + }] + } + } + + context = Context(user=user_with_permissions) + request = Mock(context=context, user=user_with_permissions) + result = schema.execute(query, context_value=request) + assert not result.errors + assert result.data == expected + + context = Context(user=user_anonymous) + request = Mock(context=context, user=user_anonymous) + result = schema.execute(query, context_value=request) + assert result.errors From 1faa41c63cd323636c6e9b0358babadff9346c53 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 12:14:40 -0500 Subject: [PATCH 4/9] Clean code --- graphene_django/auth/decorators.py | 1 - graphene_django/tests/test_auth.py | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/graphene_django/auth/decorators.py b/graphene_django/auth/decorators.py index f0cbfb40f..a211219d7 100644 --- a/graphene_django/auth/decorators.py +++ b/graphene_django/auth/decorators.py @@ -34,7 +34,6 @@ def func_wrapper( cls, resolver, connection, default_manager, max_limit, enforce_first_or_last, root, info, **args): if has_perm(permissions=permissions, context=info.context): - print("Has Perms") return func( cls, resolver, connection, default_manager, max_limit, enforce_first_or_last, root, info, **args) diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py index 7adb883bb..a875bf3e6 100644 --- a/graphene_django/tests/test_auth.py +++ b/graphene_django/tests/test_auth.py @@ -37,11 +37,8 @@ def is_authenticated(self): return self.authenticated def has_perm(self, check_perms): - print(self.username, self.perms) if check_perms not in self.perms: - print("NO PERMS") return False - print("HAS PERMS") return True @@ -145,7 +142,7 @@ class Meta: def get_node(cls, info, id): return super(ReporterType, cls).get_node(info, id) - r = Reporter.objects.create( + Reporter.objects.create( first_name='John', last_name='Doe', email='johndoe@example.com', @@ -184,7 +181,7 @@ class Meta: def get_node(cls, info, id): return super(ReporterType, cls).get_node(info, id) - r = Reporter.objects.create( + Reporter.objects.create( first_name='John', last_name='Doe', email='johndoe@example.com', @@ -221,7 +218,7 @@ class Meta: def get_node(cls, info, id): return super(ReporterType, cls).get_node(info, id) - r = Reporter.objects.create( + Reporter.objects.create( first_name='John', last_name='Doe', email='johndoe@example.com', @@ -260,7 +257,7 @@ class Meta: def get_node(cls, info, id): return super(ReporterType, cls).get_node(info, id) - r = Reporter.objects.create( + Reporter.objects.create( first_name='John', last_name='Doe', email='johndoe@example.com', From e7600f4301dd618b2cdd2ae08ddea31eb6be0f54 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 12:22:12 -0500 Subject: [PATCH 5/9] Fix import Mock --- graphene_django/tests/test_auth.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py index a875bf3e6..e2acb7cb5 100644 --- a/graphene_django/tests/test_auth.py +++ b/graphene_django/tests/test_auth.py @@ -1,7 +1,7 @@ import datetime +import sys import pytest -from unittest.mock import Mock from django.db import models from django.utils.functional import SimpleLazyObject from py.test import raises @@ -22,6 +22,11 @@ pytestmark = pytest.mark.django_db +if sys.version_info < (3, 0): + from unittest.mock import Mock +else: + from mock import Mock + class MockUserContext(object): From 50e1d6ebf44b62d096e71bfe4252a4400db913d3 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 14:08:41 -0500 Subject: [PATCH 6/9] Fix issue Python 2.X --- graphene_django/auth/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 graphene_django/auth/__init__.py diff --git a/graphene_django/auth/__init__.py b/graphene_django/auth/__init__.py new file mode 100644 index 000000000..e69de29bb From aff33bc54987f05c2bbf1a2123f21d13a1e29cbb Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 14:12:11 -0500 Subject: [PATCH 7/9] Fix issue with tests --- graphene_django/tests/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py index e2acb7cb5..e00438fa4 100644 --- a/graphene_django/tests/test_auth.py +++ b/graphene_django/tests/test_auth.py @@ -22,7 +22,7 @@ pytestmark = pytest.mark.django_db -if sys.version_info < (3, 0): +if sys.version_info > (3, 0): from unittest.mock import Mock else: from mock import Mock From b1a75d1230078b823755a072df8f3b1ab1730938 Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 14:58:37 -0500 Subject: [PATCH 8/9] Update auth documentation --- docs/authorization.rst | 84 ++++++++++++++++++++++++++++++ graphene_django/auth/__init__.py | 11 ++++ graphene_django/tests/test_auth.py | 1 - 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 1e2ec8164..04cb1e063 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -123,6 +123,90 @@ method to your ``DjangoObjectType``. return post return None +Require permissions +--------------------- + +If you want you can require Django permissions to access to *Nodes*, +*Mutations* and *Connections*. + +Node example: + +.. code:: python + from graphene_django.types import DjangoObjectType + from graphene_django.auth import node_require_permission + from .models import Reporter + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + @classmethod + @node_require_permission(permissions=('can_view_report',, 'can_edit_foo', )) + def get_node(cls, info, id): + return super(ReporterType, cls).get_node(info, id) + +Mutation example: + +.. code:: python + from rest_framework import serializers + from graphene_django.types import DjangoObjectType + from graphene_django.auth import node_require_permission + from graphene_django.rest_framework.mutation import SerializerMutation + from .models import Reporter + + + class ReporterSerializer(serializers.ModelSerializer): + class Meta: + model = Reporter + fields = '__all__' + + + class MyMutation(SerializerMutation): + class Meta: + serializer_class = ReporterSerializer + + @classmethod + @mutation_require_permission(permissions=('can_view_foo', 'can_edit_foo', )) + def mutate_and_get_payload(cls, root, info, **input): + return super(MyMutation, cls).mutate_and_get_payload(root, info, **input) + +Connection example: + +.. code:: python + import graphene + from graphene_django.fields import DjangoConnectionField + from graphene_django.auth import connection_require_permission, node_require_permission + from graphene_django.types import DjangoObjectType + from .models import Reporter + + class ReporterType(DjangoObjectType): + + class Meta: + model = Reporter + interfaces = (Node, ) + + @classmethod + @node_require_permission(permissions=('can_view_report',, 'can_edit_foo', )) + def get_node(cls, info, id): + return super(ReporterType, cls).get_node(info, id) + + class MyAuthDjangoConnectionField(DjangoConnectionField): + + @classmethod + @connection_require_permission(permissions=('can_view_foo', )) + def connection_resolver(cls, resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args): + return super(MyAuthDjangoConnectionField, cls).connection_resolver( + resolver, connection, default_manager, max_limit, + enforce_first_or_last, root, info, **args) + + class Query(graphene.ObjectType): + all_reporters = MyAuthDjangoConnectionField(ReporterType) + + + Adding login required --------------------- diff --git a/graphene_django/auth/__init__.py b/graphene_django/auth/__init__.py index e69de29bb..5cba383f7 100644 --- a/graphene_django/auth/__init__.py +++ b/graphene_django/auth/__init__.py @@ -0,0 +1,11 @@ +from .decorators import ( + node_require_permission, + mutation_require_permission, + connection_require_permission +) + +__all__ = [ + 'node_require_permission', + 'mutation_require_permission', + 'connection_require_permission' +] diff --git a/graphene_django/tests/test_auth.py b/graphene_django/tests/test_auth.py index e00438fa4..d10f26ec7 100644 --- a/graphene_django/tests/test_auth.py +++ b/graphene_django/tests/test_auth.py @@ -58,7 +58,6 @@ def __init__(self, user): user_with_permissions = MockUserContext(authenticated=True, perms=('can_view_foo', 'can_view_bar')) -# Mutations class MyFakeModel(models.Model): cool_name = models.CharField(max_length=50) From 0477c1a78adda765a99bf7774c3cc7a4a50afc9f Mon Sep 17 00:00:00 2001 From: Carlos Martinez Date: Thu, 26 Oct 2017 15:18:07 -0500 Subject: [PATCH 9/9] Fix typo in auth documentation --- docs/authorization.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/authorization.rst b/docs/authorization.rst index 04cb1e063..214cbc767 100644 --- a/docs/authorization.rst +++ b/docs/authorization.rst @@ -143,7 +143,7 @@ Node example: interfaces = (Node, ) @classmethod - @node_require_permission(permissions=('can_view_report',, 'can_edit_foo', )) + @node_require_permission(permissions=('can_view_report', 'can_edit_foo', )) def get_node(cls, info, id): return super(ReporterType, cls).get_node(info, id) @@ -188,7 +188,7 @@ Connection example: interfaces = (Node, ) @classmethod - @node_require_permission(permissions=('can_view_report',, 'can_edit_foo', )) + @node_require_permission(permissions=('can_view_report', 'can_edit_foo', )) def get_node(cls, info, id): return super(ReporterType, cls).get_node(info, id)