From 133a597f6a78b65346810b69245bf2cefff5a668 Mon Sep 17 00:00:00 2001 From: alejandronunez Date: Fri, 11 Dec 2020 07:56:10 -0700 Subject: [PATCH] Agregar permisos por Type (#29) * Adding permission_to_all_fields to DjangoObjectType meta data * Adding permission_to_all_fields to DjangoObjectType meta data * Adding permission_to_all_fields to DjangoObjectType meta data * change raise_exception * Adding custom PermissionDenied * Adding custom PermissionDenied * Adding custom PermissionDenied * adding prints * adding prints * Removing prints * Adding prints * Adding prints * Change permissions * Fixing pylint * Change __get_field_permissions__ method * Revert __set_as_nullable__ method * Change permissions * Fixing resolvers * Merge django fields with meta fields * In get_resolver take parent_resolver when it have added permissions * In get_resolver take parent_resolver when it have added permissions * Change user_logged param for user * Merging * Fixing tests * adding prints * adding prints * revert * revert * Testing * Testing * Fixing tests * Fixing tests * Testing * Fixing bugs * Fixing pylint * Adding resolve_fields from interfaces * Adding resolve_fields from interfaces Co-authored-by: ariel1899 --- graphene_django/fields.py | 5 +- graphene_django/tests/test_fields.py | 2 +- graphene_django/tests/test_types.py | 116 +++++++++++++-------------- graphene_django/types.py | 106 +++++++++++++++++++----- graphene_django/utils/utils.py | 29 ++++++- 5 files changed, 175 insertions(+), 83 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 718e970..bdf2667 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -219,7 +219,10 @@ class DjangoField(Field): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - parent_resolver = super(DjangoField, self).get_resolver(parent_resolver) + + if getattr(parent_resolver, "func", None) != auth_resolver: + parent_resolver = super(DjangoField, self).get_resolver(parent_resolver) + if self.permissions: return partial( get_unbound_function(self.permissions_resolver), diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 4abaf15..2cd4c2a 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -5,7 +5,7 @@ import pytest from graphene import List, NonNull, ObjectType, Schema, String from mock import mock from unittest import TestCase -from django.core.exceptions import PermissionDenied +from graphene_django.utils.utils import PermissionDenied from graphene_django.fields import DjangoField, DataLoaderField from promise.dataloader import DataLoader from promise import Promise diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 74e98ec..de14684 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -3,11 +3,13 @@ from textwrap import dedent import pytest from django.db import models +from django.test import TestCase from mock import patch from graphene import Interface, ObjectType, Schema, Connection, String, Field from graphene.relay import Node +from graphene_django.utils.utils import PermissionDenied from .. import registry from ..settings import graphene_settings from ..types import DjangoObjectType, DjangoObjectTypeOptions @@ -581,7 +583,9 @@ def extra_field_resolver(root, info, **kwargs): class PermissionArticle(DjangoObjectType): """Basic Type to test""" - class Meta(object): + extra_field = Field(String, resolver=extra_field_resolver) + + class Meta: """Meta Class""" field_to_permission = { @@ -593,85 +597,81 @@ class PermissionArticle(DjangoObjectType): } model = ArticleModel - extra_field = Field(String, resolver=extra_field_resolver) - def resolve_headline(self, info, **kwargs): return "headline" -def test_django_permissions(): - expected = { - "headline": ("content_type.permission1", "content_type.permission3"), - "pub_date": ("content_type.permission2",), - "reporter": ("content_type.permission3",), - "extra_field": ("content_type.permission3",), - } - assert PermissionArticle.field_permissions == expected +class PermissionTypesTests(TestCase): + def test_django_permissions(self): + expected = { + "headline": ("content_type.permission1", "content_type.permission3"), + "pub_date": ("content_type.permission2",), + "reporter": ("content_type.permission3",), + "extra_field": ("content_type.permission3",), + } + self.assertEqual(PermissionArticle.field_permissions, expected) -def test_permission_resolver(): - MyType = object() + def test_permission_resolver(self): + MyType = object() - class Viewer(object): - def has_perm(self, perm): - return perm == "content_type.permission3" + class Viewer(object): + def has_perm(self, perm): + return perm == "content_type.permission3" - class Info(object): - class Context(object): - user = Viewer() + class Info(object): + class Context(object): + user = Viewer() - context = Context() + context = Context() - resolved = PermissionArticle.resolve_headline(MyType, Info()) - assert resolved == "headline" + resolved = PermissionArticle.resolve_headline(MyType, Info()) + self.assertEqual(resolved, "headline") + def test_resolver_without_permission(self): + MyType = object() -def test_resolver_without_permission(): - MyType = object() + class Viewer(object): + def has_perm(self, perm): + return False - class Viewer(object): - def has_perm(self, perm): - return False + class Info(object): + class Context(object): + user = Viewer() - class Info(object): - class Context(object): - user = Viewer() + context = Context() - context = Context() + with self.assertRaises(PermissionDenied): + PermissionArticle.resolve_headline(MyType, Info()) - resolved = PermissionArticle.resolve_headline(MyType, Info()) - assert resolved is None + def test_permission_resolver_to_field(self): + MyType = object() + class Viewer(object): + def has_perm(self, perm): + return perm == "content_type.permission3" -def test_permission_resolver_to_field(): - MyType = object() + class Info(object): + class Context(object): + user = Viewer() - class Viewer(object): - def has_perm(self, perm): - return perm == "content_type.permission3" + context = Context() - class Info(object): - class Context(object): - user = Viewer() + resolved = PermissionArticle.resolve_extra_field(MyType, Info()) + self.assertEqual(resolved, "extra field") - context = Context() + def test_resolver_to_field_without_permission(self): + MyType = object() - resolved = PermissionArticle.resolve_extra_field(MyType, Info()) - assert resolved == "extra field" + class Viewer(object): + def has_perm(self, perm): + return perm != "content_type.permission3" + class Info(object): + class Context(object): + user = Viewer() -def test_resolver_to_field_without_permission(): - MyType = object() + context = Context() - class Viewer(object): - def has_perm(self, perm): - return perm != "content_type.permission3" - - class Info(object): - class Context(object): - user = Viewer() - - context = Context() - - resolved = PermissionArticle.resolve_extra_field(MyType, Info()) - assert resolved is None + resolved = PermissionArticle.resolve_extra_field(MyType, Info()) + self.assertIsNone(resolved) diff --git a/graphene_django/types.py b/graphene_django/types.py index 01838e4..1581e9d 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -19,7 +19,6 @@ from .registry import Registry, get_global_registry from .settings import graphene_settings from .utils import ( DJANGO_FILTER_INSTALLED, - camelize, get_model_fields, is_valid_django_model, ) @@ -27,7 +26,6 @@ from .utils import ( if six.PY3: from typing import Type - ALL_FIELDS = "__all__" @@ -133,15 +131,27 @@ def validate_fields(type_, model, fields, only_fields, exclude_fields): ) -def get_auth_resolver(name, permissions, resolver=None): +def get_auth_resolver( + name, permissions, resolver=None, raise_exception=False, permission_classes=None +): """ Get middleware resolver to handle field permissions :param name: Field name :param permissions: List of permissions :param resolver: Field resolver + :param raise_exception: If True a PermissionDenied is raised + :param permission_classes: Permission for user :return: Middleware resolver to check permissions """ - return partial(auth_resolver, resolver, permissions, name, None, False) + return partial( + auth_resolver, + resolver, + permissions, + name, + None, + raise_exception, + permission_classes=permission_classes, + ) class DjangoObjectTypeOptions(ObjectTypeOptions): @@ -173,6 +183,7 @@ class DjangoObjectType(ObjectType): convert_choices_to_enum=True, field_to_permission=None, permission_to_field=None, + permission_to_all_fields=None, _meta=None, **options ): @@ -273,21 +284,26 @@ class DjangoObjectType(ObjectType): _meta.fields = django_fields _meta.connection = connection - field_permissions = cls.__get_field_permissions__( - field_to_permission, permission_to_field - ) - if field_permissions: - cls.__set_as_nullable__(field_permissions, model, registry) + permission_classes = getattr(cls, "permission_classes", None) super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) + field_permissions, fields_raise_exception = cls.__get_field_permissions__( + field_to_permission, + permission_to_field, + permission_to_all_fields, + permission_classes, + ) + # Validate fields validate_fields(cls, model, _meta.fields, fields, exclude) if field_permissions: - cls.__set_permissions_resolvers__(field_permissions) + cls.__set_permissions_resolvers__( + field_permissions, fields_raise_exception, permission_classes + ) cls.field_permissions = field_permissions @@ -295,18 +311,44 @@ class DjangoObjectType(ObjectType): registry.register(cls) @classmethod - def __get_field_permissions__(cls, field_to_permission, permission_to_field): + def __get_field_permissions__( + cls, + field_to_permission, + permission_to_field, + permission_to_all_fields, + permission_classes, + ): """Combines permissions from meta""" permissions = field_to_permission if field_to_permission else {} - if permission_to_field: - perm_to_field = cls.__get_permission_to_fields__(permission_to_field) - for field, perms in perm_to_field.items(): - if field in permissions: - permissions[field] += perms - else: - permissions[field] = perms + perm_to_field = cls.__get_permission_to_fields__( + permission_to_field if permission_to_field else {} + ) + fields_raise_exception = {} - return permissions + for name, field in cls._meta.fields.items(): + if name == "id": + continue + + if permission_classes: + permissions[name] = () + + if name in perm_to_field: + if name in permissions: + permissions[name] += perm_to_field[name] + else: + permissions[name] = perm_to_field[name] + + if permission_to_all_fields: + permissions[name] = tuple( + set(permissions.get(name, ()) + permission_to_all_fields) + ) + + if name in permissions: + fields_raise_exception[name] = hasattr(field, "_type") and isinstance( + field._type, NonNull + ) + + return permissions, fields_raise_exception @classmethod def __get_permission_to_fields__(cls, permission_to_field): @@ -327,19 +369,41 @@ class DjangoObjectType(ObjectType): return permissions @classmethod - def __set_permissions_resolvers__(cls, permissions): + def __set_permissions_resolvers__( + cls, permissions, fields_raise_exception, permission_classes + ): """Set permission resolvers""" for field_name, field_permissions in permissions.items(): + raise_exception = fields_raise_exception.get(field_name, False) attr = "resolve_{}".format(field_name) resolver = getattr( cls._meta.fields[field_name], "resolver", None ) or getattr(cls, attr, None) + if not resolver: + + for interface in cls._meta.interfaces: + + resolver = getattr( + interface._meta.fields.get(field_name, None), "resolver", None + ) or getattr(interface, attr, None) + + if resolver: + break + if not hasattr(field_permissions, "__iter__"): field_permissions = tuple(field_permissions) setattr( - cls, attr, get_auth_resolver(field_name, field_permissions, resolver) + cls, + attr, + get_auth_resolver( + field_name, + field_permissions, + resolver, + raise_exception, + permission_classes, + ), ) @classmethod diff --git a/graphene_django/utils/utils.py b/graphene_django/utils/utils.py index 4bbfdd6..a0e0472 100644 --- a/graphene_django/utils/utils.py +++ b/graphene_django/utils/utils.py @@ -1,9 +1,10 @@ import inspect import six -from django.core.exceptions import PermissionDenied from django.db import models from django.db.models.manager import Manager +from django.utils.translation import ugettext_lazy as _ +from graphql.error import GraphQLError # from graphene.utils import LazyList from graphene.types.resolver import get_default_resolver @@ -43,6 +44,20 @@ def camelize(data): return data +class PermissionDenied(GraphQLError): + """Exception for permission denied. This exception must be used when a user does not have access to a resource""" + + message = _("Permission denied.") + code = "permission-denied" + + def __init__( + self, nodes=None, stack=None, source=None, positions=None, locations=None + ): + super(PermissionDenied, self).__init__( + self.__class__.message, nodes, stack, source, positions, locations + ) + + def get_reverse_fields(model, local_field_names): for name, attr in model.__dict__.items(): # Don't duplicate any local fields @@ -159,7 +174,17 @@ def auth_resolver( raise PermissionDenied() user = info.context.user - if has_permissions(user, permissions): + permission_classes = args.pop("permission_classes", None) + + if has_permissions(user, permissions) and ( + not permission_classes + or all( + ( + perm.has_permission(user=user, instance=root, **args) + for perm in permission_classes + ) + ) + ): if parent_resolver: # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args)