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 <radke1899@gmail.com>
This commit is contained in:
alejandronunez 2020-12-11 07:56:10 -07:00 committed by GitHub
parent ae663474e0
commit 133a597f6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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