Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

info.context.user is not a real user model #23

Closed
ghost opened this issue Aug 22, 2019 · 11 comments
Closed

info.context.user is not a real user model #23

ghost opened this issue Aug 22, 2019 · 11 comments

Comments

@ghost
Copy link

ghost commented Aug 22, 2019

There has been no issue when I query or mutate through grahiql or ordinary HTTP. info.context.user is not a real user model and provokes an issue only when I connect through a websocket using DjangoChannelsGraphqlWs. An error occurs when I return some thing like:

def resolve_user(self, info, **kwargs):
    return info.conext.user

So I had to write like this:

def resolve_user(self, info, **kwargs):
    return get_object_or_404(models.User, username=user.username)

There was also an issue on a mutation when I returned a model connected to info.context.user. For example, this does not work.

class CreateMessage(graphene.Mutation):
	class Arguments:
		text = graphene.String(required=True)
		room_slug = graphene.String(required=True)

	ok = graphene.Boolean()
	message = graphene.Field(MessageType)

	def mutate(root, info, text, room_slug):
		if info.context.user.is_authenticated is False:
			raise PermissionDenied('Login required')

		ok = False
		room = get_object_or_404(models.Room, slug=room_slug)
		message = models.Message(room=room, creator=info.context.user, text=text)
		message.save()
		ok = True
		return CreateMessage(ok=ok, message=message)

mutation sendMessage($text: String!) {
                        createMessage(text: $text roomSlug: "${this.state.slug}") {
                          ok
                          message {
                            slug
                            createdDate
                            text
                            creator {
                              ...

It is because I used info.context.user when I made a Message model instance. To make it work, I have to write like this:

def mutate(root, info, text, room_slug):
    creator = users_models.User.objects.get(username=info.context.user.username)
    message = models.Message(room=room, creator=creator, text=text)

Why does info.context.user work differently only when I use websocket? Is there anything I am missing?

What is the right way to return user model right after using info.context.user?

@prokher
Copy link
Member

prokher commented Aug 22, 2019

@caesar4321 In order to understand the root of the problem please answer a couple of questions:

  • How do you authorize user? Do you use channels.auth.login like in the example, or somehow differently?
  • What is your routing configuration? Haven't you forgot to use channels.auth.AuthMiddlewareStack?

@ghost
Copy link
Author

ghost commented Aug 23, 2019

@prokher I use ordinary from django.contrib.auth import authenticate, login, logout because channels.auth.login did not work for me. I login using ordinary HTTP method by using axios from client and then I open a new websocket connection.

from django.contrib.auth import authenticate, login, logout

...
def mutate(root, info, email, password, **kwargs):
		ok = False
		user = authenticate(info.context, username=email, password=password)
		if user is not None:
			login(info.context, user)

My routing configuration is like this:

from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from django.urls import path
from .consumers import MyGraphqlWsConsumer

application = ProtocolTypeRouter({
	'websocket': AuthMiddlewareStack(
		URLRouter([
			path('graphql/', MyGraphqlWsConsumer),
		])
	),
})

@ghost
Copy link
Author

ghost commented Aug 23, 2019

@prokher When I use the recommended way of login, an error occurs.

user = django.contrib.auth.authenticate(username=username, password=password)
if user is None:
     return Login(ok=False)
asgiref.sync.async_to_sync(channels.auth.login)(info.context._asdict(), user)
...channels_graphql_ws\scope_as_conetext.py", line 42, in __getattr__ return self._scope[name] KeyError: 'META'

The above exception was the direct cause of the following exceptions:
...
...channels_graphql_ws\scope_as_conetext.py", line 44, in __getattr__ raise AttributeError() from ex

The error occurs because I use info.context.META['HTTP_USER_AGENT'] and
ip, is_routable = get_client_ip(info.context, ['HTTP_X_FORWARDED_FOR', 'X_FORWARDED_FOR', 'REMOTE_ADDR']) from ipware. I then failed to find a workaround and I decided to stick to the ordinary HTTP login method using django.contrib.auth.login

@ghost
Copy link
Author

ghost commented Aug 23, 2019

I guess it became an issue because I did not use channels.auth.login, but django.contrib.auth.login Is there any way I can resolve this issue while maintaining the use of django.contrib.auth.login?

Or if I call a new User model instance every time I need to return it straightly to graphql query like this:

class CreateMessage(graphene.Mutation):
	class Arguments:
		text = graphene.String(required=True)
		room_slug = graphene.String(required=True)

	ok = graphene.Boolean()
	message = graphene.Field(MessageType)

	def mutate(root, info, text, room_slug):
		ok = False
		creator = get_object_or_404(users_models.User, username=info.context.user.username)
		message = models.Message(room=room, creator=creator, text=text)
		message.save()
		ok = True
		return CreateMessage(ok=ok, message=message)

will there be not a performance issue because I have to additionally access to db to get a creator (a User model instance) instead of just directly creating a new Message model like this:
models.Message(room=room, creator=info.context.user, text=text)

@prokher
Copy link
Member

prokher commented Aug 23, 2019

@caesar4321 If you have already authenticated thought HTTP by regular Django mechanisms, then when you establish WebSocket connection you do not need to authenticate again. The info.context.user has already contain a user object. The trick is that that object is not a Django model, but a wrapper put there by the channels.auth.AuthMiddlewareStack I suppose. The wrapper itself is an instance of channels.auth.UserLazyObject which carefully wraps all inner object methods and fields. So it is supposed to work according to the duck typing principle.

What is interesting here is where the object returned by your resolve_user goes to and why it does not work with channels.auth.UserLazyObject instance.

Anyway, I would avoid requesting DB when I do not need to. As a workaround you can extract real Django model from channels.auth.UserLazyObject._wrapped, e.g. like

user = getattr(info.context.user, "_wrapped", None) or info.context.user

while it shall work for both HTTP and WebSocket, it is definitely a hack which I do not like. Let's understand what is wrong with the channels.auth.UserLazyObject wrapper and why receiver of what resolve_user returns does not work with it correctly.

Another issue you have touched is that you try to extract something from info.context which it does not contain. The reason is that when you work thought the HTTP connection you probably use graphene_django.views.GraphQLView which puts the Django request object into the info.context. When you work thought WebSockets info.context contains a Channels consumer self.scope. These two object (Django HTTP request and Channels scope) have something in common, by they are anyway different. If your resolvers work with both HTTP & WebSockets then they probably need to support two kind of contexts. I personally do not like it, but do not see any feasible solution of how to unify what graphene_django.views.GraphQLView puts in the info.context and what Channels put into self.scope. If you have any ideas - let's discuss.

BTW this is an example of Channels self.scope (info.context is just a proxy for this):

{'client': ['127.0.0.1', 58726],
 'cookies': {'csrftoken': 'mpy8zqEviiot1ugLXqJqLY0HT30GM20tMZ6IhLJikMDHkAofn7sNhuFuKLyodbtu',
             'sessionid': 't0auyp41awaqo8y90mvkgscwzusdlizf'},
 'headers': [(b'upgrade', b'websocket'),
             (b'connection', b'Upgrade'),
             (b'host', b'127.0.0.1:4242'),
             (b'origin', b'http://127.0.0.1:4242'),
             (b'sec-websocket-protocol', b'graphql-ws'),
             (b'pragma', b'no-cache'),
             (b'cache-control', b'no-cache'),
             (b'sec-websocket-key', b'3Wvfsqb4vvQYQZtyb1T0jQ=='),
             (b'sec-websocket-version', b'13'),
             (b'sec-websocket-extensions', b'x-webkit-deflate-frame'),
             (b'user-agent',
              b'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/'
              b'605.1.15 (KHTML, like Gecko) Version/12.1.2 Safari/605.1.15'),
             (b'cookie',
              b'csrftoken=mpy8zqEviiot1ugLXqJqLY0HT30GM20tMZ6IhLJikMDHkAofn7'
              b'sNhuFuKLyodbtu; sessionid=t0auyp41awaqo8y90mvkgscwzusdlizf')],
 'path': '/graphql/',
 'path_remaining': '',
 'query_string': b'',
 'server': ['127.0.0.1', 4242],
 'session': <django.utils.functional.LazyObject object at 0x109196f50>,
 'subprotocols': ['graphql-ws'],
 'type': 'websocket',
 'url_route': {'args': (), 'kwargs': {}},

@prokher
Copy link
Member

prokher commented Aug 23, 2019

I think I eventually realized how to handle existent HTTP-based authentication properly. You can simply set self.scope["user"] in the on_connect handler of you MyGraphqlWsConsumer:

class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):
    async def on_connect(self, payload):
        self.scope["user"] = await channels.auth.get_user(self.scope)

This will properly initialize info.context.user for both cases, when user is already authenticated and when he is not.

Added this to the example, please check. Closing this, cause it seems to be addressed. Feel free to reopen if there is still an issue.

@fmoga
Copy link

fmoga commented Feb 1, 2021

@prokher We are using the recommended way to set the user on scope described above but could notice on production that subscribe functions sometimes execute prior to on_connect, at which point the scope is not populated.

We've attributed this to subscriptions-transport-ws not waiting for connection ack to initiate subscriptions (and the lack of an option to do so), thus we've enabled strict_ordering on the consumer in order to make sure that the scope is populated when subscriptions are run.

Is there anything else we could do instead? I should mention that we are initializing multiple objects on the scope so there is a benefit to doing it once on connection. Thanks!

@SebasWilde
Copy link

is that the right way to solve this? does this solution have some implications?

@IIMunchII
Copy link

IIMunchII commented Nov 21, 2021

UPDATE: (I found this to be easy. Didn't realise this option could be set as boolean on the class attributes) I am using channels 2.4 and wondering if this is possible with newer versions of channels.

@SebasWilde, I am facing the same issue when setting the self.scope["user"] in the async on_connect() handler. I am curious as to how you enforce the ordering of execution on the consumer end of things.

@tony
Copy link

tony commented Jan 7, 2022

UPDATE: (I found this to be easy. Didn't realise this option could be set as boolean on the class attributes) I am using channels 2.4 and wondering if this is possible with newer versions of channels.

Hi! If you had progress that's great! Can you explain what you you fixed and how you did it?

@tony
Copy link

tony commented Jan 7, 2022

This has been continued in #75

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants