Refactor DjangoObjectType

This commit is contained in:
Olivia Rodriguez Valdes 2019-02-28 15:29:00 -05:00
commit 9a645f3ef1
4 changed files with 91 additions and 108 deletions

View File

@ -165,5 +165,4 @@ 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)

View File

@ -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.field_permissions == expected
def test_permission_resolver():

View File

@ -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,76 @@ 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)
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:
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(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))
@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 +219,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)

View File

@ -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