From 9e62c71d721f22d0a2c0a596da89e80200355813 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 4 Jan 2019 14:59:32 -0500 Subject: [PATCH 01/14] Add PermissionObjectType --- graphene_django/types.py | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/graphene_django/types.py b/graphene_django/types.py index aa8b5a3..d383f02 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -133,3 +133,91 @@ class DjangoObjectType(ObjectType): return cls._meta.model.objects.get(pk=id) except cls._meta.model.DoesNotExist: return None + + +class DjangoPermissionObjectType(DjangoObjectType): + """ + DjangoObjectType inheritance to handle field authorization + Accepts field's permissions description as: + + class Meta: + + field_to_permission = { + 'restricted_field': ('permission1', 'permission2') + } + + permission_to_field = { + 'permission': ('restricted_field_1', 'restricted_field_2') + } + + At least one of the permissions must be accomplished in order to resolve the field. + """ + + class Meta(object): + """Meta Class""" + abstract = True + + @classmethod + def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_field=None, model=None, registry=None, + **options): # pylint: disable=W0221 + + cls._field_permissions = field_to_permission if field_to_permission else {} + + if permission_to_field: + cls._get_permission_to_fields(permission_to_field) + + for field_name, field_permissions in cls._field_permissions.items(): + attr = 'resolve_{}'.format(field_name) + resolver = getattr(cls, attr, None) + + if not hasattr(field_permissions, '__iter__'): + field_permissions = tuple(field_permissions) + + setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + + if cls._field_permissions: + cls._set_as_nullable(model, registry) + + super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) + + # pylint: disable=W0212 + @classmethod + def _set_as_nullable(cls, model, registry): + """Set restricted fields as nullable""" + django_fields = yank_fields_from_attrs( + construct_fields(model, registry, cls._field_permissions.keys(), ()), + _as=Field, + ) + for name, field in django_fields.items(): + if isinstance(field._type, NonNull): + field._type = field._type._of_type # pylint: disable=W0212 + setattr(cls, name, field) + + @classmethod + def _get_permission_to_fields(cls, permission_to_field): + """ + Accepts a dictionary like + { + permission: [fields] + } + :return: Mapping of fields to permissions + """ + for permission, fields in permission_to_field.items(): + for field in fields: + cls._set_permission_to_field(field, (permission,)) + + @classmethod + def _set_permission_to_field(cls, field, permissions): + """Add list permissions to field""" + cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions + + @classmethod + def set_auth_resolver(cls, name, permissions, resolver=None): + """ + Set middleware resolver to handle field permissions + :param name: Field name + :param permissions: List of permissions + :param resolver: Field resolver + :return: Middleware resolver to check permissions + """ + return partial(auth_resolver, resolver, name, permissions, None, False) From d1391db45aab35ce2c3144b7511f529ee0c9f09d Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Sun, 6 Jan 2019 11:13:44 -0500 Subject: [PATCH 02/14] Add permission type --- graphene_django/types.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index d383f02..4f0755a 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -1,7 +1,8 @@ from collections import OrderedDict +from functools import partial from django.utils.functional import SimpleLazyObject -from graphene import Field +from graphene import Field, NonNull from graphene.relay import Connection, Node from graphene.types.objecttype import ObjectType, ObjectTypeOptions from graphene.types.utils import yank_fields_from_attrs @@ -159,7 +160,8 @@ class DjangoPermissionObjectType(DjangoObjectType): @classmethod def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_field=None, model=None, registry=None, - **options): # pylint: disable=W0221 + **options): + super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) cls._field_permissions = field_to_permission if field_to_permission else {} @@ -173,14 +175,11 @@ class DjangoPermissionObjectType(DjangoObjectType): if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) - setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + cls.set_auth_resolver(field_name, field_permissions, cls._meta.fields[field_name], resolver) if cls._field_permissions: cls._set_as_nullable(model, registry) - super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) - - # pylint: disable=W0212 @classmethod def _set_as_nullable(cls, model, registry): """Set restricted fields as nullable""" @@ -190,7 +189,7 @@ class DjangoPermissionObjectType(DjangoObjectType): ) for name, field in django_fields.items(): if isinstance(field._type, NonNull): - field._type = field._type._of_type # pylint: disable=W0212 + field._type = field._type._of_type setattr(cls, name, field) @classmethod @@ -212,12 +211,13 @@ class DjangoPermissionObjectType(DjangoObjectType): cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions @classmethod - def set_auth_resolver(cls, name, permissions, resolver=None): + def set_auth_resolver(cls, name, permissions, field, resolver=None): """ Set middleware resolver to handle field permissions :param name: Field name :param permissions: List of permissions + :param field: Meta's field :param resolver: Field resolver :return: Middleware resolver to check permissions """ - return partial(auth_resolver, resolver, name, permissions, None, False) + field.resolver = partial(auth_resolver, field.resolver or resolver, name, permissions, None, False) From 0b34a215adb28fc750965c8e6dfc95119b656333 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 07:46:00 -0500 Subject: [PATCH 03/14] Set resolver as class attr --- graphene_django/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index 4f0755a..b78c7ea 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -175,7 +175,7 @@ class DjangoPermissionObjectType(DjangoObjectType): if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) - cls.set_auth_resolver(field_name, field_permissions, cls._meta.fields[field_name], resolver) + setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) if cls._field_permissions: cls._set_as_nullable(model, registry) From 106d3e063dfb1deb7b11ea160e7171411713777c Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 7 Jan 2019 08:25:19 -0500 Subject: [PATCH 04/14] Add test to permission type --- graphene_django/fields.py | 2 +- graphene_django/tests/test_types.py | 102 +++++++++++++++++++++++++++- graphene_django/types.py | 10 +-- graphene_django/utils.py | 21 +++++- 4 files changed, 126 insertions(+), 9 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 41132e2..ae18e98 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -164,4 +164,4 @@ class DjangoPermissionField(Field): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(auth_resolver, self.resolver or parent_resolver, self.permissions, True) + return partial(auth_resolver, self.resolver or parent_resolver, self.permissions, None, None, True) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8a8643b..8ad1465 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -1,10 +1,10 @@ from mock import patch -from graphene import Interface, ObjectType, Schema, Connection, String +from graphene import Interface, ObjectType, Schema, Connection, String, Field from graphene.relay import Node from .. import registry -from ..types import DjangoObjectType, DjangoObjectTypeOptions +from ..types import DjangoObjectType, DjangoObjectTypeOptions, DjangoPermissionObjectType from .models import Article as ArticleModel from .models import Reporter as ReporterModel @@ -224,3 +224,101 @@ def test_django_objecttype_exclude_fields(): fields = list(Reporter._meta.fields.keys()) assert "email" not in fields + + +def extra_field_resolver(root, info, **kwargs): + return 'extra field' + + +class PermissionArticle(DjangoPermissionObjectType): + """Basic Type to test""" + + class Meta(object): + """Meta Class""" + field_to_permission = { + 'headline': ('content_type.permission1',), + 'pub_date': ('content_type.permission2',) + } + permission_to_field = { + 'content_type.permission3': ('headline', 'reporter', 'extra_field',) + } + 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 + + +def test_permission_resolver(): + MyType = object() + + 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_headline(MyType, Info()) + assert resolved == 'headline' + + +def test_resolver_without_permission(): + MyType = object() + + class Viewer(object): + def has_perm(self, perm): + return False + + class Info(object): + class Context(object): + user = Viewer() + context = Context() + + resolved = PermissionArticle.resolve_headline(MyType, Info()) + assert resolved is None + + +def test_permission_resolver_to_field(): + MyType = object() + + 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 == 'extra field' + + +def test_resolver_to_field_without_permission(): + MyType = object() + + 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 diff --git a/graphene_django/types.py b/graphene_django/types.py index b78c7ea..3699308 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -9,7 +9,7 @@ from graphene.types.utils import yank_fields_from_attrs from .converter import convert_django_field_with_choices from .registry import Registry, get_global_registry -from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model +from .utils import DJANGO_FILTER_INSTALLED, get_model_fields, is_valid_django_model, auth_resolver def construct_fields(model, registry, only_fields, exclude_fields): @@ -170,7 +170,7 @@ class DjangoPermissionObjectType(DjangoObjectType): for field_name, field_permissions in cls._field_permissions.items(): attr = 'resolve_{}'.format(field_name) - resolver = getattr(cls, attr, None) + resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) @@ -188,7 +188,7 @@ class DjangoPermissionObjectType(DjangoObjectType): _as=Field, ) for name, field in django_fields.items(): - if isinstance(field._type, NonNull): + if hasattr(field, '_type') and isinstance(field._type, NonNull): field._type = field._type._of_type setattr(cls, name, field) @@ -211,7 +211,7 @@ class DjangoPermissionObjectType(DjangoObjectType): cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions @classmethod - def set_auth_resolver(cls, name, permissions, field, resolver=None): + def set_auth_resolver(cls, name, permissions, resolver=None): """ Set middleware resolver to handle field permissions :param name: Field name @@ -220,4 +220,4 @@ class DjangoPermissionObjectType(DjangoObjectType): :param resolver: Field resolver :return: Middleware resolver to check permissions """ - field.resolver = partial(auth_resolver, field.resolver or resolver, name, permissions, None, False) + return partial(auth_resolver, resolver, permissions, name, None, False) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 55581c4..bbc984b 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -6,6 +6,7 @@ from django.db.models.manager import Manager # from graphene.utils import LazyList +from graphene.types.resolver import get_default_resolver from graphene.utils.get_unbound_function import get_unbound_function @@ -110,11 +111,27 @@ def resolve_bound_resolver(resolver, root, info, **args): return resolver(root, info, **args) -def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **args): +def resolve_default_resolver(attname, default_value, root, info, **args): + """ + Resolve field with default resolver + :param attname: Field name + :param default_value: Field default value + :param root: Schema root + :param info: Schema info + :param args: Schema args + :return: Resolved field + """ + resolver = get_default_resolver() + return resolver(attname, default_value, root, info, **args) + + +def auth_resolver(parent_resolver, permissions, attname, default_value, raise_exception, root, info, **args): """ Middleware resolver to check viewer's permissions :param parent_resolver: Field resolver :param permissions: Field permissions + :param attname: Field name + :param default_value: Default value to field if no resolver is provided :param raise_exception: If True a PermissionDenied is raised :param root: Schema root :param info: Schema info @@ -127,6 +144,8 @@ def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **a if parent_resolver: # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args) + # Get default resolver + return resolve_default_resolver(attname, default_value, root, info, **args) elif raise_exception: raise PermissionDenied() return None From 7b86824afa4fcaced6e80e07e4d6f01c32b890f5 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Mon, 18 Feb 2019 15:00:35 -0500 Subject: [PATCH 05/14] Mixing DjangoObjectType and DjangoPermissionObjectType --- graphene_django/fields.py | 3 +- graphene_django/tests/test_types.py | 6 +- graphene_django/types.py | 163 +++++++++++++--------------- 3 files changed, 81 insertions(+), 91 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index cff3810..23ddd9f 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -165,4 +165,5 @@ class DjangoPermissionField(Field): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.AUTH_RESOLVER), self.resolver or parent_resolver, self.permissions,None, None, True) + return partial(get_unbound_function(self.AUTH_RESOLVER), self.resolver or parent_resolver, self.permissions, + None, None, True) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 8ad1465..2732781 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -4,7 +4,7 @@ from graphene import Interface, ObjectType, Schema, Connection, String, Field from graphene.relay import Node from .. import registry -from ..types import DjangoObjectType, DjangoObjectTypeOptions, DjangoPermissionObjectType +from ..types import DjangoObjectType, DjangoObjectTypeOptions from .models import Article as ArticleModel from .models import Reporter as ReporterModel @@ -230,7 +230,7 @@ def extra_field_resolver(root, info, **kwargs): return 'extra field' -class PermissionArticle(DjangoPermissionObjectType): +class PermissionArticle(DjangoObjectType): """Basic Type to test""" class Meta(object): @@ -257,7 +257,7 @@ def test_django_permissions(): 'reporter': ('content_type.permission3',), 'extra_field': ('content_type.permission3',), } - assert PermissionArticle._field_permissions == expected + assert PermissionArticle._meta.field_permissions == expected def test_permission_resolver(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 3699308..17ba330 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -42,6 +42,22 @@ class DjangoObjectTypeOptions(ObjectTypeOptions): class DjangoObjectType(ObjectType): + """ + DjangoObjectType inheritance to handle field authorization + Accepts field's permissions description as: + + class Meta: + + field_to_permission = { + 'restricted_field': ('permission1', 'permission2') + } + + permission_to_field = { + 'permission': ('restricted_field_1', 'restricted_field_2') + } + + At least one of the permissions must be accomplished in order to resolve the field. + """ @classmethod def __init_subclass_with_meta__( cls, @@ -55,6 +71,8 @@ class DjangoObjectType(ObjectType): connection_class=None, use_connection=None, interfaces=(), + field_to_permission=None, + permission_to_field=None, _meta=None, **options ): @@ -109,9 +127,67 @@ class DjangoObjectType(ObjectType): _meta=_meta, interfaces=interfaces, **options ) + permissions = field_to_permission if field_to_permission else {} + + if permission_to_field: + permissions.update(cls.__get_permission_to_fields__(permission_to_field)) + + cls.field_permissions = permissions + + for field_name, field_permissions in permissions.items(): + attr = 'resolve_{}'.format(field_name) + resolver = getattr(_meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) + + if not hasattr(field_permissions, '__iter__'): + field_permissions = tuple(field_permissions) + + setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + + if permissions: + cls.__set_as_nullable__(model, registry) + if not skip_registry: registry.register(cls) + @classmethod + def __set_as_nullable__(cls, model, registry): + """Set restricted fields as nullable""" + django_fields = yank_fields_from_attrs( + construct_fields(model, registry, cls.field_permissions.keys(), ()), + _as=Field, + ) + for name, field in django_fields.items(): + if hasattr(field, '_type') and isinstance(field._type, NonNull): + field._type = field._type._of_type + setattr(cls, name, field) + + @classmethod + def __get_permission_to_fields__(cls, permission_to_field): + """ + Accepts a dictionary like + { + permission: [fields] + } + :return: Mapping of fields to permissions + """ + permissions = {} + for permission, fields in permission_to_field.items(): + for field in fields: + permissions[field] = permissions.get(field, ()) + (permission,) + return permissions + + @classmethod + def set_auth_resolver(cls, name, permissions, resolver=None): + """ + Set middleware resolver to handle field permissions + :param name: Field name + :param permissions: List of permissions + :param field: Meta's field + :param resolver: Field resolver + :return: Middleware resolver to check permissions + """ + return partial(auth_resolver, resolver, permissions, name, None, False) + def resolve_id(self, info): return self.pk @@ -134,90 +210,3 @@ class DjangoObjectType(ObjectType): return cls._meta.model.objects.get(pk=id) except cls._meta.model.DoesNotExist: return None - - -class DjangoPermissionObjectType(DjangoObjectType): - """ - DjangoObjectType inheritance to handle field authorization - Accepts field's permissions description as: - - class Meta: - - field_to_permission = { - 'restricted_field': ('permission1', 'permission2') - } - - permission_to_field = { - 'permission': ('restricted_field_1', 'restricted_field_2') - } - - At least one of the permissions must be accomplished in order to resolve the field. - """ - - class Meta(object): - """Meta Class""" - abstract = True - - @classmethod - def __init_subclass_with_meta__(cls, field_to_permission=None, permission_to_field=None, model=None, registry=None, - **options): - super(DjangoPermissionObjectType, cls).__init_subclass_with_meta__(model=model, registry=registry, **options) - - cls._field_permissions = field_to_permission if field_to_permission else {} - - if permission_to_field: - cls._get_permission_to_fields(permission_to_field) - - for field_name, field_permissions in cls._field_permissions.items(): - attr = 'resolve_{}'.format(field_name) - resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) - - if not hasattr(field_permissions, '__iter__'): - field_permissions = tuple(field_permissions) - - setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) - - if cls._field_permissions: - cls._set_as_nullable(model, registry) - - @classmethod - def _set_as_nullable(cls, model, registry): - """Set restricted fields as nullable""" - django_fields = yank_fields_from_attrs( - construct_fields(model, registry, cls._field_permissions.keys(), ()), - _as=Field, - ) - for name, field in django_fields.items(): - if hasattr(field, '_type') and isinstance(field._type, NonNull): - field._type = field._type._of_type - setattr(cls, name, field) - - @classmethod - def _get_permission_to_fields(cls, permission_to_field): - """ - Accepts a dictionary like - { - permission: [fields] - } - :return: Mapping of fields to permissions - """ - for permission, fields in permission_to_field.items(): - for field in fields: - cls._set_permission_to_field(field, (permission,)) - - @classmethod - def _set_permission_to_field(cls, field, permissions): - """Add list permissions to field""" - cls._field_permissions[field] = cls._field_permissions.get(field, tuple()) + permissions - - @classmethod - def set_auth_resolver(cls, name, permissions, resolver=None): - """ - Set middleware resolver to handle field permissions - :param name: Field name - :param permissions: List of permissions - :param field: Meta's field - :param resolver: Field resolver - :return: Middleware resolver to check permissions - """ - return partial(auth_resolver, resolver, permissions, name, None, False) From 715fbbe1dd1985f625b3bc357e5b80c7fea6cbb1 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Tue, 19 Feb 2019 13:02:06 -0500 Subject: [PATCH 06/14] Update permission to type --- graphene_django/tests/test_types.py | 2 +- graphene_django/types.py | 25 ++++++++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/graphene_django/tests/test_types.py b/graphene_django/tests/test_types.py index 2732781..c9245ae 100644 --- a/graphene_django/tests/test_types.py +++ b/graphene_django/tests/test_types.py @@ -257,7 +257,7 @@ def test_django_permissions(): 'reporter': ('content_type.permission3',), 'extra_field': ('content_type.permission3',), } - assert PermissionArticle._meta.field_permissions == expected + assert PermissionArticle.field_permissions == expected def test_permission_resolver(): diff --git a/graphene_django/types.py b/graphene_django/types.py index 17ba330..b969ae4 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -127,28 +127,35 @@ class DjangoObjectType(ObjectType): _meta=_meta, interfaces=interfaces, **options ) - permissions = field_to_permission if field_to_permission else {} + if cls.field_permissions: + cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) + if not skip_registry: + registry.register(cls) + + @classmethod + def __set_permissions__(cls, field_to_permission, permission_to_field): + """Combines permissions from meta""" + permissions = field_to_permission if field_to_permission else {} if permission_to_field: - permissions.update(cls.__get_permission_to_fields__(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 cls.field_permissions = permissions for field_name, field_permissions in permissions.items(): attr = 'resolve_{}'.format(field_name) - resolver = getattr(_meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) + resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) - if permissions: - cls.__set_as_nullable__(model, registry) - - if not skip_registry: - registry.register(cls) - @classmethod def __set_as_nullable__(cls, model, registry): """Set restricted fields as nullable""" From 142f5141ea51fd0277da789e94f86c6e907b2b1b Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Tue, 19 Feb 2019 13:31:19 -0500 Subject: [PATCH 07/14] Add permission to class --- graphene_django/types.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/graphene_django/types.py b/graphene_django/types.py index b969ae4..adf0eb6 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -127,6 +127,8 @@ class DjangoObjectType(ObjectType): _meta=_meta, interfaces=interfaces, **options ) + cls.__set_permissions__(field_to_permission, permission_to_field) + if cls.field_permissions: cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) From f357d7ebfbe8c5ca4b8818e47179b6edd4ad03cf Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 10:48:30 -0500 Subject: [PATCH 08/14] Remove field resolver --- graphene_django/utils.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index bbc984b..2d70742 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -6,8 +6,8 @@ from django.db.models.manager import Manager # from graphene.utils import LazyList +from django.utils.six import get_unbound_function from graphene.types.resolver import get_default_resolver -from graphene.utils.get_unbound_function import get_unbound_function class LazyList(object): @@ -111,20 +111,6 @@ def resolve_bound_resolver(resolver, root, info, **args): return resolver(root, info, **args) -def resolve_default_resolver(attname, default_value, root, info, **args): - """ - Resolve field with default resolver - :param attname: Field name - :param default_value: Field default value - :param root: Schema root - :param info: Schema info - :param args: Schema args - :return: Resolved field - """ - resolver = get_default_resolver() - return resolver(attname, default_value, root, info, **args) - - def auth_resolver(parent_resolver, permissions, attname, default_value, raise_exception, root, info, **args): """ Middleware resolver to check viewer's permissions @@ -145,7 +131,7 @@ def auth_resolver(parent_resolver, permissions, attname, default_value, raise_ex # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args) # Get default resolver - return resolve_default_resolver(attname, default_value, root, info, **args) + return get_default_resolver(attname, default_value, root, info, **args) elif raise_exception: raise PermissionDenied() return None From 10d28ba5d0dc7ad7665c065081d75c30abf54303 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 15:30:36 -0500 Subject: [PATCH 09/14] Refactor DjangoObjectType --- graphene_django/types.py | 68 ++++++++++++++++++++++------------------ graphene_django/utils.py | 2 +- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index adf0eb6..80d4ba5 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -33,6 +33,17 @@ def construct_fields(model, registry, only_fields, exclude_fields): return fields +def get_auth_resolver(cls, name, permissions, resolver=None): + """ + Get middleware resolver to handle field permissions + :param name: Field name + :param permissions: List of permissions + :param resolver: Field resolver + :return: Middleware resolver to check permissions + """ + return partial(auth_resolver, resolver, permissions, name, None, False) + + class DjangoObjectTypeOptions(ObjectTypeOptions): model = None # type: Model registry = None # type: Registry @@ -127,16 +138,17 @@ class DjangoObjectType(ObjectType): _meta=_meta, interfaces=interfaces, **options ) - cls.__set_permissions__(field_to_permission, permission_to_field) + cls.field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) if cls.field_permissions: + cls.__set_permissions_resolvers__(cls.field_permissions) cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) if not skip_registry: registry.register(cls) @classmethod - def __set_permissions__(cls, field_to_permission, permission_to_field): + def __get_field_permissions__(cls, field_to_permission, permission_to_field): """Combines permissions from meta""" permissions = field_to_permission if field_to_permission else {} if permission_to_field: @@ -147,8 +159,29 @@ class DjangoObjectType(ObjectType): else: permissions[field] = perms - cls.field_permissions = permissions + return permissions + @classmethod + def __get_permission_to_fields__(cls, permission_to_field): + """ + Accepts a dictionary like + { + permission: [fields] + } + :return: Mapping of fields to permissions like + { + field: [permissions] + } + """ + permissions = {} + for permission, fields in permission_to_field.items(): + for field in fields: + permissions[field] = permissions.get(field, ()) + (permission,) + return permissions + + @classmethod + def __set_permissions_resolvers__(cls, permissions): + """Set permission resolvers""" for field_name, field_permissions in permissions.items(): attr = 'resolve_{}'.format(field_name) resolver = getattr(cls._meta.fields[field_name], 'resolver', None) or getattr(cls, attr, None) @@ -156,7 +189,7 @@ class DjangoObjectType(ObjectType): if not hasattr(field_permissions, '__iter__'): field_permissions = tuple(field_permissions) - setattr(cls, attr, cls.set_auth_resolver(field_name, field_permissions, resolver)) + setattr(cls, attr, get_auth_resolver(field_name, field_permissions, resolver)) @classmethod def __set_as_nullable__(cls, model, registry): @@ -170,33 +203,6 @@ class DjangoObjectType(ObjectType): field._type = field._type._of_type setattr(cls, name, field) - @classmethod - def __get_permission_to_fields__(cls, permission_to_field): - """ - Accepts a dictionary like - { - permission: [fields] - } - :return: Mapping of fields to permissions - """ - permissions = {} - for permission, fields in permission_to_field.items(): - for field in fields: - permissions[field] = permissions.get(field, ()) + (permission,) - return permissions - - @classmethod - def set_auth_resolver(cls, name, permissions, resolver=None): - """ - Set middleware resolver to handle field permissions - :param name: Field name - :param permissions: List of permissions - :param field: Meta's field - :param resolver: Field resolver - :return: Middleware resolver to check permissions - """ - return partial(auth_resolver, resolver, permissions, name, None, False) - def resolve_id(self, info): return self.pk diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 2d70742..71093fc 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -131,7 +131,7 @@ def auth_resolver(parent_resolver, permissions, attname, default_value, raise_ex # A resolver is provided in the class return resolve_bound_resolver(parent_resolver, root, info, **args) # Get default resolver - return get_default_resolver(attname, default_value, root, info, **args) + return get_default_resolver()(attname, default_value, root, info, **args) elif raise_exception: raise PermissionDenied() return None From 1adb13dff8ba68d0725e7359c3ef8cff04433dd4 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 16:35:01 -0500 Subject: [PATCH 10/14] Set function as unbound --- graphene_django/fields.py | 3 ++- graphene_django/types.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 0e15255..801445e 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -165,4 +165,5 @@ class DjangoField(Field): def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, self.permissions,None, None, True) + return partial(get_unbound_function(self.permissions_resolver), self.resolver or parent_resolver, + self.permissions, None, None, True) diff --git a/graphene_django/types.py b/graphene_django/types.py index 80d4ba5..8eb91a1 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -33,7 +33,7 @@ def construct_fields(model, registry, only_fields, exclude_fields): return fields -def get_auth_resolver(cls, name, permissions, resolver=None): +def get_auth_resolver(name, permissions, resolver=None): """ Get middleware resolver to handle field permissions :param name: Field name From ef3cd819593caea020b29e404b1e2c30b4efd232 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 17:21:26 -0500 Subject: [PATCH 11/14] Reorder field_permissions --- graphene_django/types.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index 8eb91a1..b8f6899 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -134,15 +134,18 @@ 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__(model, registry) + super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options ) - cls.field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) + if field_permissions: + cls.__set_permissions_resolvers__(field_permissions) - if cls.field_permissions: - cls.__set_permissions_resolvers__(cls.field_permissions) - cls.__set_as_nullable__(cls._meta.model, cls._meta.registry) + cls.field_permissions = field_permissions if not skip_registry: registry.register(cls) From 7272d34950bd8e5f0d88b8009caa7d6ab64896d0 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 17:51:30 -0500 Subject: [PATCH 12/14] Pass field_permissions as variable to __set_as_nullable__ method --- graphene_django/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_django/types.py b/graphene_django/types.py index b8f6899..6ccd3c8 100644 --- a/graphene_django/types.py +++ b/graphene_django/types.py @@ -136,7 +136,7 @@ class DjangoObjectType(ObjectType): field_permissions = cls.__get_field_permissions__(field_to_permission, permission_to_field) if field_permissions: - cls.__set_as_nullable__(model, registry) + cls.__set_as_nullable__(field_permissions, model, registry) super(DjangoObjectType, cls).__init_subclass_with_meta__( _meta=_meta, interfaces=interfaces, **options @@ -195,10 +195,10 @@ class DjangoObjectType(ObjectType): setattr(cls, attr, get_auth_resolver(field_name, field_permissions, resolver)) @classmethod - def __set_as_nullable__(cls, model, registry): + def __set_as_nullable__(cls, field_permissions, model, registry): """Set restricted fields as nullable""" django_fields = yank_fields_from_attrs( - construct_fields(model, registry, cls.field_permissions.keys(), ()), + construct_fields(model, registry, field_permissions.keys(), ()), _as=Field, ) for name, field in django_fields.items(): From 3eee96c9adb5b549c15143fc4b150a45fc021960 Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Thu, 28 Feb 2019 22:48:00 -0500 Subject: [PATCH 13/14] Update get_unbound_function import --- graphene_django/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphene_django/utils.py b/graphene_django/utils.py index 71093fc..d3c1e89 100644 --- a/graphene_django/utils.py +++ b/graphene_django/utils.py @@ -6,8 +6,8 @@ from django.db.models.manager import Manager # from graphene.utils import LazyList -from django.utils.six import get_unbound_function from graphene.types.resolver import get_default_resolver +from graphene.utils.get_unbound_function import get_unbound_function class LazyList(object): From 1e1e849f4a9877a721cc45a88a818a31d53a951b Mon Sep 17 00:00:00 2001 From: Olivia Rodriguez Valdes Date: Fri, 1 Mar 2019 14:34:54 -0500 Subject: [PATCH 14/14] Rename to DjangoField --- graphene_django/fields.py | 6 +++--- graphene_django/tests/test_fields.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/graphene_django/fields.py b/graphene_django/fields.py index 48a7b1e..e078ba4 100644 --- a/graphene_django/fields.py +++ b/graphene_django/fields.py @@ -154,18 +154,18 @@ class DjangoConnectionField(ConnectionField): ) -class PermissionField(Field): +class DjangoField(Field): """Class to manage permission for fields""" def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs): """Get permissions to access a field""" - super(PermissionField, self).__init__(type, *args, **kwargs) + super(DjangoField, self).__init__(type, *args, **kwargs) self.permissions = permissions self.permissions_resolver = permissions_resolver def get_resolver(self, parent_resolver): """Intercept resolver to analyse permissions""" - parent_resolver = super(PermissionField, self).get_resolver(parent_resolver) + parent_resolver = super(DjangoField, self).get_resolver(parent_resolver) if self.permissions: return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, None, True) return parent_resolver diff --git a/graphene_django/tests/test_fields.py b/graphene_django/tests/test_fields.py index 23cce7f..e0478bd 100644 --- a/graphene_django/tests/test_fields.py +++ b/graphene_django/tests/test_fields.py @@ -1,6 +1,6 @@ from unittest import TestCase from django.core.exceptions import PermissionDenied -from graphene_django.fields import PermissionField +from graphene_django.fields import DjangoField class MyInstance(object): @@ -14,7 +14,7 @@ class PermissionFieldTests(TestCase): def test_permission_field(self): MyType = object() - field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(None) class Viewer(object): @@ -30,7 +30,7 @@ class PermissionFieldTests(TestCase): def test_permission_field_without_permission(self): MyType = object() - field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver') + field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver') resolver = field.get_resolver(field.resolver) class Viewer(object):