Skip to content

Commit

Permalink
graphql: Add HTTP401-wrapper @auth_required for gql actions
Browse files Browse the repository at this point in the history
- Create @auth_required wrapper that throws custom
  GraphqlAuthenticationError if user is not authenticated
- Create DRFAuthenticatedGraphQLView which wraps GraphQLView
  and re-raises DRF.AuthenticationFailed if
  GraphqlAuthenticationError is raised. DRF will then return 401
- Create AuthenticatedGraphQLView which wraps GraphQLView
  and returns

Note: DRFAuthenticatedGraphQL view This is a custom workaround.
It raises an HTTP 401 error, which is not a part of the GraphQL
spec. It may fit some frontend apps for a transition period to
GraphQL, but should not be seen as a permanent solution.
AuthenticatedGraphQLView is more in line with GraphQL spec, and
shoul be preferred if applicable for you.
  • Loading branch information
tomfa committed Nov 20, 2018
1 parent c097b85 commit 3a72f29
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 3 deletions.
47 changes: 46 additions & 1 deletion graphy/conftest.py
@@ -1,4 +1,5 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser

from graphene.test import Client as GraphQLClient

Expand All @@ -7,6 +8,7 @@
from rest_framework import test

from graphy.schema import schema
from graphy.utils.graphql import AuthenticatedGraphQLView


@pytest.fixture
Expand Down Expand Up @@ -54,7 +56,50 @@ def __init__(self):

class Context:
def __init__(self):
self.user = AnonymousUser()
self._request = Request()
self.META = {}

return GraphQLClient(schema, context=Context())
return GraphQLClient(
schema,
format_error=AuthenticatedGraphQLView.format_error,
context=Context(),
)


@pytest.fixture
def gql_client_staff(staff_user):
class Request:
def __init__(self):
self.META = {}

class Context:
def __init__(self, user):
self.user = user
self._request = Request()
self.META = {}

return GraphQLClient(
schema,
format_error=AuthenticatedGraphQLView.format_error,
context=Context(user=staff_user),
)


@pytest.fixture
def gql_client_user(user):
class Request:
def __init__(self):
self.META = {}

class Context:
def __init__(self, user):
self.user = user
self._request = Request()
self.META = {}

return GraphQLClient(
schema,
format_error=AuthenticatedGraphQLView.format_error,
context=Context(user=user),
)
3 changes: 3 additions & 0 deletions graphy/customers/gql_actions.py
Expand Up @@ -3,6 +3,7 @@

from graphy.customers.gql_types import CustomerType
from graphy.customers.models import Customer
from graphy.utils.graphql import auth_required


class CustomerQuery:
Expand All @@ -11,9 +12,11 @@ class CustomerQuery:
CustomerType, id=graphene.UUID(), email=graphene.String()
)

@auth_required
def resolve_customer(self, info, **kwargs):
return Customer.objects.filter(id=kwargs.get('id')).first()

@auth_required
def resolve_customers(self, info, **kwargs):
qs = Customer.objects.all()
if 'email' in kwargs:
Expand Down
6 changes: 4 additions & 2 deletions graphy/urls.py
Expand Up @@ -2,13 +2,15 @@
from django.contrib import admin
from django.urls import path, include

from graphene_django.views import GraphQLView
from graphy.utils.graphql import AuthenticatedGraphQLView


urlpatterns = [
path('admin/', admin.site.urls),
path('location/', include('graphy.location.urls')),
path('leads/', include('graphy.leads.urls')),
path('customers/', include('graphy.customers.urls')),
path('graphql/', GraphQLView.as_view(graphiql=settings.DEBUG)),
path(
'graphql/', AuthenticatedGraphQLView.as_view(graphiql=settings.DEBUG)
),
]
68 changes: 68 additions & 0 deletions graphy/utils/graphql.py
@@ -0,0 +1,68 @@
from graphene_django.views import GraphQLView
from graphql import GraphQLError

from rest_framework import (
permissions,
request as drf_request,
)
from rest_framework.decorators import (
api_view,
permission_classes,
)
from rest_framework.exceptions import AuthenticationFailed


class GraphqlAuthenticationError(GraphQLError):
pass


def auth_required(fn):
def wrapper(*args, **kwargs):
*_, info = args

if not info.context.user.is_authenticated:
raise GraphqlAuthenticationError('Authentication required.')
return fn(*args, **kwargs)
return wrapper


class DRFAuthenticatedGraphQLView(GraphQLView):
"""
Returns a HTTP 401. Outside GraphQL spec.
"""
def parse_body(self, request):
if isinstance(request, drf_request.Request):
return request.data
return super().parse_body(request)

@staticmethod
def format_error(error):
if (
hasattr(error, 'original_error') and
isinstance(error.original_error, GraphqlAuthenticationError)):
raise AuthenticationFailed()
return GraphQLView.format_error(error)

@classmethod
def as_view(cls, *args, **kwargs):
view = super(GraphQLView, cls).as_view(*args, **kwargs)
view = permission_classes((permissions.AllowAny,))(view)
view = api_view(['GET', 'POST'])(view)
return view


class AuthenticatedGraphQLView(GraphQLView):
"""
Returns a regular GraphQL error
"""
def parse_body(self, request):
if isinstance(request, drf_request.Request):
return request.data
return super().parse_body(request)

@classmethod
def as_view(cls, *args, **kwargs):
view = super(GraphQLView, cls).as_view(*args, **kwargs)
view = permission_classes((permissions.AllowAny,))(view)
view = api_view(['GET', 'POST'])(view)
return view

0 comments on commit 3a72f29

Please sign in to comment.