mirror of
https://github.com/graphql-python/graphene-django.git
synced 2025-04-26 12:03:47 +03:00
Merge pull request #6 from revolico/permission-to-type#5
Permission to type#5
This commit is contained in:
commit
49bb86f58d
|
@ -154,18 +154,18 @@ class DjangoConnectionField(ConnectionField):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PermissionField(Field):
|
class DjangoField(Field):
|
||||||
"""Class to manage permission for fields"""
|
"""Class to manage permission for fields"""
|
||||||
|
|
||||||
def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs):
|
def __init__(self, type, permissions=(), permissions_resolver=auth_resolver, *args, **kwargs):
|
||||||
"""Get permissions to access a field"""
|
"""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 = permissions
|
||||||
self.permissions_resolver = permissions_resolver
|
self.permissions_resolver = permissions_resolver
|
||||||
|
|
||||||
def get_resolver(self, parent_resolver):
|
def get_resolver(self, parent_resolver):
|
||||||
"""Intercept resolver to analyse permissions"""
|
"""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:
|
if self.permissions:
|
||||||
return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, True)
|
return partial(get_unbound_function(self.permissions_resolver), parent_resolver, self.permissions, None, None, True)
|
||||||
return parent_resolver
|
return parent_resolver
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from graphene_django.fields import PermissionField
|
from graphene_django.fields import DjangoField
|
||||||
|
|
||||||
|
|
||||||
class MyInstance(object):
|
class MyInstance(object):
|
||||||
|
@ -14,7 +14,7 @@ class PermissionFieldTests(TestCase):
|
||||||
|
|
||||||
def test_permission_field(self):
|
def test_permission_field(self):
|
||||||
MyType = object()
|
MyType = object()
|
||||||
field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver')
|
field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver')
|
||||||
resolver = field.get_resolver(None)
|
resolver = field.get_resolver(None)
|
||||||
|
|
||||||
class Viewer(object):
|
class Viewer(object):
|
||||||
|
@ -30,7 +30,7 @@ class PermissionFieldTests(TestCase):
|
||||||
|
|
||||||
def test_permission_field_without_permission(self):
|
def test_permission_field_without_permission(self):
|
||||||
MyType = object()
|
MyType = object()
|
||||||
field = PermissionField(MyType, permissions=['perm1', 'perm2'], source='resolver')
|
field = DjangoField(MyType, permissions=['perm1', 'perm2'], source='resolver')
|
||||||
resolver = field.get_resolver(field.resolver)
|
resolver = field.get_resolver(field.resolver)
|
||||||
|
|
||||||
class Viewer(object):
|
class Viewer(object):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from mock import patch
|
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 graphene.relay import Node
|
||||||
|
|
||||||
from .. import registry
|
from .. import registry
|
||||||
|
@ -224,3 +224,101 @@ def test_django_objecttype_exclude_fields():
|
||||||
|
|
||||||
fields = list(Reporter._meta.fields.keys())
|
fields = list(Reporter._meta.fields.keys())
|
||||||
assert "email" not in fields
|
assert "email" not in fields
|
||||||
|
|
||||||
|
|
||||||
|
def extra_field_resolver(root, info, **kwargs):
|
||||||
|
return 'extra field'
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionArticle(DjangoObjectType):
|
||||||
|
"""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
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from graphene import Field
|
from graphene import Field, NonNull
|
||||||
from graphene.relay import Connection, Node
|
from graphene.relay import Connection, Node
|
||||||
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
|
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
|
||||||
from graphene.types.utils import yank_fields_from_attrs
|
from graphene.types.utils import yank_fields_from_attrs
|
||||||
|
|
||||||
from .converter import convert_django_field_with_choices
|
from .converter import convert_django_field_with_choices
|
||||||
from .registry import Registry, get_global_registry
|
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):
|
def construct_fields(model, registry, only_fields, exclude_fields):
|
||||||
|
@ -32,6 +33,17 @@ def construct_fields(model, registry, only_fields, exclude_fields):
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_resolver(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):
|
class DjangoObjectTypeOptions(ObjectTypeOptions):
|
||||||
model = None # type: Model
|
model = None # type: Model
|
||||||
registry = None # type: Registry
|
registry = None # type: Registry
|
||||||
|
@ -41,6 +53,22 @@ class DjangoObjectTypeOptions(ObjectTypeOptions):
|
||||||
|
|
||||||
|
|
||||||
class DjangoObjectType(ObjectType):
|
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
|
@classmethod
|
||||||
def __init_subclass_with_meta__(
|
def __init_subclass_with_meta__(
|
||||||
cls,
|
cls,
|
||||||
|
@ -54,6 +82,8 @@ class DjangoObjectType(ObjectType):
|
||||||
connection_class=None,
|
connection_class=None,
|
||||||
use_connection=None,
|
use_connection=None,
|
||||||
interfaces=(),
|
interfaces=(),
|
||||||
|
field_to_permission=None,
|
||||||
|
permission_to_field=None,
|
||||||
_meta=None,
|
_meta=None,
|
||||||
**options
|
**options
|
||||||
):
|
):
|
||||||
|
@ -104,13 +134,78 @@ class DjangoObjectType(ObjectType):
|
||||||
_meta.fields = django_fields
|
_meta.fields = django_fields
|
||||||
_meta.connection = connection
|
_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)
|
||||||
|
|
||||||
super(DjangoObjectType, cls).__init_subclass_with_meta__(
|
super(DjangoObjectType, cls).__init_subclass_with_meta__(
|
||||||
_meta=_meta, interfaces=interfaces, **options
|
_meta=_meta, interfaces=interfaces, **options
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if field_permissions:
|
||||||
|
cls.__set_permissions_resolvers__(field_permissions)
|
||||||
|
|
||||||
|
cls.field_permissions = field_permissions
|
||||||
|
|
||||||
if not skip_registry:
|
if not skip_registry:
|
||||||
registry.register(cls)
|
registry.register(cls)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not hasattr(field_permissions, '__iter__'):
|
||||||
|
field_permissions = tuple(field_permissions)
|
||||||
|
|
||||||
|
setattr(cls, attr, get_auth_resolver(field_name, field_permissions, resolver))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def __set_as_nullable__(cls, field_permissions, model, registry):
|
||||||
|
"""Set restricted fields as nullable"""
|
||||||
|
django_fields = yank_fields_from_attrs(
|
||||||
|
construct_fields(model, registry, 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)
|
||||||
|
|
||||||
def resolve_id(self, info):
|
def resolve_id(self, info):
|
||||||
return self.pk
|
return self.pk
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.db.models.manager import Manager
|
||||||
|
|
||||||
|
|
||||||
# from graphene.utils import LazyList
|
# from graphene.utils import LazyList
|
||||||
|
from graphene.types.resolver import get_default_resolver
|
||||||
from graphene.utils.get_unbound_function import get_unbound_function
|
from graphene.utils.get_unbound_function import get_unbound_function
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,11 +111,13 @@ def resolve_bound_resolver(resolver, root, info, **args):
|
||||||
return resolver(root, info, **args)
|
return resolver(root, info, **args)
|
||||||
|
|
||||||
|
|
||||||
def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **args):
|
def auth_resolver(parent_resolver, permissions, attname, default_value, raise_exception, root, info, **args):
|
||||||
"""
|
"""
|
||||||
Middleware resolver to check viewer's permissions
|
Middleware resolver to check viewer's permissions
|
||||||
:param parent_resolver: Field resolver
|
:param parent_resolver: Field resolver
|
||||||
:param permissions: Field permissions
|
: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 raise_exception: If True a PermissionDenied is raised
|
||||||
:param root: Schema root
|
:param root: Schema root
|
||||||
:param info: Schema info
|
:param info: Schema info
|
||||||
|
@ -130,6 +133,8 @@ def auth_resolver(parent_resolver, permissions, raise_exception, root, info, **a
|
||||||
if parent_resolver:
|
if parent_resolver:
|
||||||
# A resolver is provided in the class
|
# A resolver is provided in the class
|
||||||
return resolve_bound_resolver(parent_resolver, root, info, **args)
|
return resolve_bound_resolver(parent_resolver, root, info, **args)
|
||||||
|
# Get default resolver
|
||||||
|
return get_default_resolver()(attname, default_value, root, info, **args)
|
||||||
elif raise_exception:
|
elif raise_exception:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
return None
|
return None
|
||||||
|
|
Loading…
Reference in New Issue
Block a user