Repository: strawberry-graphql/strawberry (python)

Issue:
Support for field permissions & visibility
Would be nice to have a simple way to require permission for specific fields and also hide them from the schema (visibility).

Permission example (borrowed from DRF)

```py
import strawberry

from strawberry.permission import BasePermission


class IsAuthenticated(BasePermission):
    def has_permission(self, info) -> bool:
        return False


@strawberry.type
class User:
    name: str
    age: int


@strawberry.type
class Query:
    @strawberry.field(permission_classes=[IsAuthenticated])
    def me(self, info) -> Optional[User]:
        return User(name="Patrick", age=100)
```

Visibility example, what this should do is to completely hide the field from the schema if the client is not able to see it (so not even shown when doing introspection).

```py
import strawberry

from strawberry.permission import BasePermission


class IsAdminClient(BasePermission):
    def has_permission(self, info) -> bool:
        return False


@strawberry.type
class User:
    name: str
    age: int


@strawberry.type
class Query:
    @strawberry.field(visiblity_classes=[IsAdminClient])
    def me(self, info) -> Optional[User]:
        return User(name="Patrick", age=100)
```

Pull request description:
Add Support for Field Permissions
This PR adds support for creating permission classes in strawberry. It takes inspiration from DRF's `BasePermission` and `permission_classes`

Fixes #37 

Patch:
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 00000000..b29809b0
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,21 @@
+Release Type: minor
+
+This release adds field support for permissions 
+
+```python
+import strawberry
+
+from strawberry.permission import BasePermission
+
+class IsAdmin(BasePermission):
+    message = "You are not authorized"
+
+    def has_permission(self, info):
+      return False
+
+@strawberry.type
+class Query:
+    @strawberry.field(permisson_classes=[IsAdmin])
+    def hello(self, info) -> str:
+      return "Hello"
+```
diff --git a/strawberry/__init__.py b/strawberry/__init__.py
index 8cbd4ce5..7220b009 100644
--- a/strawberry/__init__.py
+++ b/strawberry/__init__.py
@@ -7,3 +7,4 @@ from .mutation import mutation, subscription  # noqa
 from .scalars import ID  # noqa
 from .schema import Schema  # noqa
 from .type import input, type, interface  # noqa
+from .permission import BasePermission  # noqa
diff --git a/strawberry/field.py b/strawberry/field.py
index 5b5a01ef..708d0d2d 100644
--- a/strawberry/field.py
+++ b/strawberry/field.py
@@ -40,6 +40,7 @@ class LazyFieldWrapper:
         resolver=None,
         name=None,
         description=None,
+        permission_classes=None
     ):
         self._wrapped_obj = obj
         self.is_subscription = is_subscription
@@ -47,6 +48,7 @@ class LazyFieldWrapper:
         self.field_name = name
         self.field_resolver = resolver
         self.field_description = description
+        self.permission_classes = permission_classes or []
 
         if callable(self._wrapped_obj):
             self._check_has_annotations(self._wrapped_obj)
@@ -85,6 +87,23 @@ class LazyFieldWrapper:
     def __call__(self, *args, **kwargs):
         return self._wrapped_obj(self, *args, **kwargs)
 
+    def _get_permissions(self):
+        """
+        Gets all permissions defined in the permission classes
+        >>> strawberry.field(permission_classes=[IsAuthenticated])
+        """
+        return (permission() for permission in self.permission_classes)
+
+    def _check_permissions(self, info):
+        """
+        Checks if the permission should be accepted and
+        raises an exception if not
+        """
+        for permission in self._get_permissions():
+            if not permission.has_permission(info):
+                message = getattr(permission, "message", None)
+                raise PermissionError(message)
+
     @lazy_property
     def field(self):
         return _get_field(
@@ -93,6 +112,7 @@ class LazyFieldWrapper:
             is_subscription=self.is_subscription,
             name=self.field_name,
             description=self.field_description,
+            check_permission=self._check_permissions,
         )
 
 
@@ -128,12 +148,14 @@ class strawberry_field(dataclasses.Field):
         name=None,
         description=None,
         metadata=None,
+        permission_classes=None
     ):
         self.field_name = name
         self.field_description = description
         self.field_resolver = resolver
         self.is_subscription = is_subscription
         self.is_input = is_input
+        self.field_permission_classes = permission_classes
 
         super().__init__(
             # TODO:
@@ -161,6 +183,7 @@ class strawberry_field(dataclasses.Field):
             resolver=self.field_resolver,
             name=self.field_name,
             description=self.field_description,
+            permission_classes=self.field_permission_classes,
         )
 
 
@@ -198,7 +221,13 @@ def convert_args(args, annotations):
 
 
 def _get_field(
-    wrap, *, is_input=False, is_subscription=False, name=None, description=None
+    wrap,
+    *,
+    is_input=False,
+    is_subscription=False,
+    name=None,
+    description=None,
+    check_permission=None
 ):
     name = wrap.__name__
 
@@ -220,8 +249,9 @@ def _get_field(
     }
 
     def resolver(source, info, **args):
+        if check_permission:
+            check_permission(info)
         args = convert_args(args, arguments_annotations)
-
         return wrap(source, info, **args)
 
     field_params = {}
@@ -232,6 +262,8 @@ def _get_field(
         if is_subscription:
 
             def _resolve(event, info):
+                if check_permission:
+                    check_permission(info)
                 return event
 
             field_params.update({"subscribe": resolver, "resolve": _resolve})
@@ -253,6 +285,7 @@ def field(
     resolver=None,
     is_input=False,
     is_subscription=False,
+    permission_classes=None
 ):
     """Annotates a method or property as a GraphQL field.
 
@@ -275,6 +308,7 @@ def field(
         resolver=resolver,
         is_input=is_input,
         is_subscription=is_subscription,
+        permission_classes=permission_classes,
     )
 
     # when calling this with parens we are going to return a strawberry_field
diff --git a/strawberry/permission.py b/strawberry/permission.py
new file mode 100644
index 00000000..f2143014
--- /dev/null
+++ b/strawberry/permission.py
@@ -0,0 +1,9 @@
+class BasePermission:
+    """
+    Base class for creating permissions
+    """
+
+    def has_permission(self, info):
+        raise NotImplementedError(
+            "Permission classes should override has_permission method"
+        )


Test patch:
diff --git a/tests/test_permission.py b/tests/test_permission.py
new file mode 100644
index 00000000..99682825
--- /dev/null
+++ b/tests/test_permission.py
@@ -0,0 +1,78 @@
+import typing
+
+import pytest
+
+import strawberry
+from graphql import graphql_sync
+from graphql.language import parse
+from graphql.subscription import subscribe
+from strawberry.permission import BasePermission
+
+
+def test_raises_graphql_error_when_permission_method_is_missing():
+    class IsAuthenticated(BasePermission):
+        pass
+
+    @strawberry.type
+    class Query:
+        @strawberry.field(permission_classes=[IsAuthenticated])
+        def user(self, info) -> str:
+            return "patrick"
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user }"
+
+    result = graphql_sync(schema, query)
+    assert (
+        result.errors[0].message
+        == "Permission classes should override has_permission method"
+    )
+
+
+def test_raises_graphql_error_when_permission_is_denied():
+    class IsAuthenticated(BasePermission):
+        message = "User is not authenticated"
+
+        def has_permission(self, info):
+            return False
+
+    @strawberry.type
+    class Query:
+        @strawberry.field(permission_classes=[IsAuthenticated])
+        def user(self, info) -> str:
+            return "patrick"
+
+    schema = strawberry.Schema(query=Query)
+
+    query = "{ user }"
+
+    result = graphql_sync(schema, query)
+    assert result.errors[0].message == "User is not authenticated"
+
+
+@pytest.mark.asyncio
+async def test_raises_permission_error_for_subscription():
+    class IsAdmin(BasePermission):
+        message = "You are not authorized"
+
+        def has_permission(self, info):
+            return False
+
+    @strawberry.type
+    class Query:
+        name: str = "Andrew"
+
+    @strawberry.type
+    class Subscription:
+        @strawberry.subscription(permission_classes=[IsAdmin])
+        async def user(self, info) -> typing.AsyncGenerator[str, None]:
+            yield "Hello"
+
+    schema = strawberry.Schema(query=Query, subscription=Subscription)
+
+    query = "subscription { user }"
+
+    result = await subscribe(schema, parse(query))
+
+    assert result.errors[0].message == "You are not authorized"