Repository: saleor/saleor

Issue:
Query logged in user (self) on GraphQL API
### What I'm trying to achieve
Query the API for information on the currently logged in customer. Currently, only staff are allowed to query information on users. Customers should at least be able to query their own information.

### Describe a proposed solution
Allow customers to query their own profile information.

### Other solutions I've tried and won't work
This is possible using the `tokenVerify` mutation, but using a mutation to query information isn't convenient. It cannot be used with relay's `QueryRenderer` for example.



Discussion:
I think it's possible to retrieve a user's information if a valid token is provided.
That only works for me if the token is for an admin user. With a customer token, I get an error saying I do not have permission to perform the action.

Edit:
I should also mention I'm using the `user` query...
Looking at the code in https://github.com/mirumee/saleor/blob/master/saleor/graphql/api.py
```
    @permission_required('account.manage_users')
    def resolve_user(self, info, id):
        return graphene.Node.get_node_from_global_id(info, id, User)

    @permission_required('account.manage_users')
    def resolve_customers(self, info, query=None, **kwargs):
        return resolve_customers(info, query=query)

    @permission_required('account.manage_staff')
    def resolve_staff_users(self, info, query=None, **kwargs):
        return resolve_staff_users(info, query=query)
```
Yeah, the queries require admin permissions
It would seem the `addressCreate` mutation and possibly other address management mutations only allow staff access as well.
```
    @classmethod
    def user_is_allowed(cls, user, input):
        return user.has_perm('account.manage_users')
```
Since the user model itself lacks some fields I need in my use case (name, phone), I've found it convenient to use the first address instance to store those information. It seems reasonable to me that the user should be able to update their address as well.
Hey @khalibloo, thanks for reporting those problems! You're right - user-related mutations and queries require staff permissions as we wrote them with Dashboard 2.0 in mind. Of course, this should be changed as you suggest - authenticated customers should be able to fetch their account data with `user(id:"")` query as well as to modify their addresses.

Resolving patch:
diff --git a/saleor/graphql/account/mutations.py b/saleor/graphql/account/mutations.py
--- a/saleor/graphql/account/mutations.py
+++ b/saleor/graphql/account/mutations.py
@@ -62,13 +62,17 @@ class UserInput(graphene.InputObjectType):
     note = graphene.String(description='A note about the user.')
 
 
-class CustomerInput(UserInput):
+class UserAddressInput(graphene.InputObjectType):
     default_billing_address = AddressInput(
         description='Billing address of the customer.')
     default_shipping_address = AddressInput(
         description='Shipping address of the customer.')
 
 
+class CustomerInput(UserInput, UserAddressInput):
+    pass
+
+
 class UserCreateInput(CustomerInput):
     send_password_email = graphene.Boolean(
         description='Send an email with a link to set a password')
@@ -148,6 +152,28 @@ class Meta:
         model = models.User
 
 
+class LoggedUserUpdate(CustomerCreate):
+    class Arguments:
+        input = UserAddressInput(
+            description='Fields required to update logged in user.',
+            required=True)
+
+    class Meta:
+        description = 'Updates data of the logged in user.'
+        exclude = ['password']
+        model = models.User
+
+    @classmethod
+    def user_is_allowed(cls, user, input):
+        return user.is_authenticated
+
+    @classmethod
+    def mutate(cls, root, info, **data):
+        user = info.context.user
+        data['id'] = graphene.Node.to_global_id('User', user.id)
+        return super().mutate(root, info, **data)
+
+
 class UserDelete(ModelDeleteMutation):
     class Meta:
         abstract = True
diff --git a/saleor/graphql/account/resolvers.py b/saleor/graphql/account/resolvers.py
--- a/saleor/graphql/account/resolvers.py
+++ b/saleor/graphql/account/resolvers.py
@@ -1,3 +1,4 @@
+import graphene
 import graphene_django_optimizer as gql_optimizer
 from django.db.models import Q
 from i18naddress import get_validation_rules
@@ -5,7 +6,7 @@
 from ...account import models
 from ...core.utils import get_client_ip, get_country_by_ip
 from ..utils import filter_by_query_param
-from .types import AddressValidationData, ChoiceValue
+from .types import AddressValidationData, ChoiceValue, User
 
 USER_SEARCH_FIELDS = (
     'email', 'default_shipping_address__first_name',
@@ -13,6 +14,16 @@
     'default_shipping_address__country')
 
 
+def resolve_user(info, id):
+    logged_user = info.context.user
+    if not id:
+        return logged_user
+    user = graphene.Node.get_node_from_global_id(info, id, User)
+    if logged_user.has_perm('account.manage_users') or user == logged_user:
+        return user
+    return None
+
+
 def resolve_customers(info, query):
     qs = models.User.objects.filter(
         Q(is_staff=False) | (Q(is_staff=True) & Q(orders__isnull=False)))
diff --git a/saleor/graphql/account/schema.py b/saleor/graphql/account/schema.py
--- a/saleor/graphql/account/schema.py
+++ b/saleor/graphql/account/schema.py
@@ -1,14 +1,16 @@
 import graphene
-from graphql_jwt.decorators import permission_required
+from graphql_jwt.decorators import login_required, permission_required
 
 from ..core.fields import PrefetchingConnectionField
 from ..descriptions import DESCRIPTIONS
 from .mutations import (
     AddressCreate, AddressDelete, AddressUpdate, CustomerCreate,
     CustomerDelete, CustomerPasswordReset, CustomerRegister, CustomerUpdate,
-    PasswordReset, SetPassword, StaffCreate, StaffDelete, StaffUpdate)
+    LoggedUserUpdate, PasswordReset, SetPassword, StaffCreate, StaffDelete,
+    StaffUpdate)
 from .resolvers import (
-    resolve_address_validator, resolve_customers, resolve_staff_users)
+    resolve_address_validator, resolve_customers, resolve_staff_users,
+    resolve_user)
 from .types import AddressValidationData, AddressValidationInput, User
 
 
@@ -37,9 +39,9 @@ def resolve_customers(self, info, query=None, **kwargs):
     def resolve_staff_users(self, info, query=None, **kwargs):
         return resolve_staff_users(info, query=query)
 
-    @permission_required('account.manage_users')
+    @login_required
     def resolve_user(self, info, id):
-        return graphene.Node.get_node_from_global_id(info, id, User)
+        return resolve_user(info, id)
 
 
 class AccountMutations(graphene.ObjectType):
@@ -52,6 +54,8 @@ class AccountMutations(graphene.ObjectType):
     customer_register = CustomerRegister.Field()
     customer_update = CustomerUpdate.Field()
 
+    logged_user_update = LoggedUserUpdate.Field()
+
     staff_create = StaffCreate.Field()
     staff_delete = StaffDelete.Field()
     staff_update = StaffUpdate.Field()


Test patch:
diff --git a/tests/api/conftest.py b/tests/api/conftest.py
--- a/tests/api/conftest.py
+++ b/tests/api/conftest.py
@@ -1,7 +1,7 @@
 import json
 
 import pytest
-
+from django.contrib.auth.models import AnonymousUser
 from django.shortcuts import reverse
 from django.test.client import MULTIPART_CONTENT, Client
 from graphql_jwt.shortcuts import get_token
@@ -17,12 +17,14 @@ class ApiClient(Client):
     def __init__(self, *args, **kwargs):
         user = kwargs.pop('user')
         self.user = user
-        self.token = get_token(user)
+        if not user.is_anonymous:
+            self.token = get_token(user)
         super().__init__(*args, **kwargs)
 
     def _base_environ(self, **request):
         environ = super()._base_environ(**request)
-        environ.update({'HTTP_AUTHORIZATION': 'JWT %s' % self.token})
+        if not self.user.is_anonymous:
+            environ.update({'HTTP_AUTHORIZATION': 'JWT %s' % self.token})
         return environ
 
     def post(self, data=None, **kwargs):
@@ -82,3 +84,8 @@ def staff_api_client(staff_user):
 @pytest.fixture
 def user_api_client(customer_user):
     return ApiClient(user=customer_user)
+
+
+@pytest.fixture
+def api_client():
+    return ApiClient(user=AnonymousUser())
diff --git a/tests/api/test_account.py b/tests/api/test_account.py
--- a/tests/api/test_account.py
+++ b/tests/api/test_account.py
@@ -2,16 +2,16 @@
 import re
 from unittest.mock import Mock, patch
 
-import pytest
-
 import graphene
+import pytest
 from django.contrib.auth import get_user_model
 from django.contrib.auth.tokens import default_token_generator
 from django.shortcuts import reverse
+from tests.api.utils import get_graphql_content
+
 from saleor.account.models import Address, User
 from saleor.graphql.account.mutations import (
     CustomerDelete, SetPassword, StaffDelete, StaffUpdate, UserDelete)
-from tests.api.utils import get_graphql_content
 
 from .utils import assert_no_permission, convert_dict_keys_to_camel_case
 
@@ -119,8 +119,8 @@ def test_query_user(staff_api_client, customer_user, permission_manage_users):
     """
     ID = graphene.Node.to_global_id('User', customer_user.id)
     variables = {'id': ID}
-    response = staff_api_client.post_graphql(
-        query, variables, permissions=[permission_manage_users])
+    staff_api_client.user.user_permissions.add(permission_manage_users)
+    response = staff_api_client.post_graphql(query, variables)
     content = get_graphql_content(response)
     data = content['data']['user']
     assert data['email'] == user.email
@@ -143,6 +143,50 @@ def test_query_user(staff_api_client, customer_user, permission_manage_users):
     assert address['phone'] == user_address.phone.as_e164
 
 
+USER_QUERY = """
+    query User($id: ID!) {
+        user(id: $id) {
+            email
+        }
+    }
+"""
+
+
+def test_customer_can_see_his_own_data(user_api_client):
+    user = user_api_client.user
+    id = graphene.Node.to_global_id('User', user.id)
+    variables = {'id': id}
+    response = user_api_client.post_graphql(USER_QUERY, variables)
+    content = get_graphql_content(response)
+    data = content['data']['user']
+    assert data['email'] == user.email
+
+
+def test_customer_can_not_see_other_users_data(user_api_client,
+                                               staff_user):
+    id = graphene.Node.to_global_id('User', staff_user.id)
+    variables = {'id': id}
+    response = user_api_client.post_graphql(USER_QUERY, variables)
+    content = get_graphql_content(response)
+    data = content['data']['user']
+    assert data is None
+
+
+def test_user_query_with_empty_id(user_api_client):
+    variables = {'id': ''}
+    response = user_api_client.post_graphql(USER_QUERY, variables)
+    content = get_graphql_content(response)
+    data = content['data']['user']
+    assert data['email'] == user_api_client.user.email
+
+
+def test_user_query_anonymous_user(api_client):
+    variables = {'id': ''}
+    response = api_client.post_graphql(
+        USER_QUERY, variables)
+    assert_no_permission(response)
+
+
 def test_query_customers(
         staff_api_client, user_api_client, permission_manage_users):
     query = """
@@ -204,14 +248,6 @@ def test_who_can_see_user(
         staff_user, customer_user, staff_api_client, user_api_client,
         permission_manage_users):
     query = """
-    query User($id: ID!) {
-        user(id: $id) {
-            email
-        }
-    }
-    """
-
-    query_2 = """
     query Users {
         customers {
             totalCount
@@ -222,19 +258,22 @@ def test_who_can_see_user(
     # Random person (even staff) can't see users data without permissions
     ID = graphene.Node.to_global_id('User', customer_user.id)
     variables = {'id': ID}
-    response = staff_api_client.post_graphql(query, variables)
-    assert_no_permission(response)
+    response = staff_api_client.post_graphql(USER_QUERY, variables)
+    data = get_graphql_content(response)
+    content = get_graphql_content(response)
+    data = content['data']['user']
+    assert data is None
 
-    response = staff_api_client.post_graphql(query_2)
+    response = staff_api_client.post_graphql(query)
     assert_no_permission(response)
 
     # Add permission and ensure staff can see user(s)
     staff_user.user_permissions.add(permission_manage_users)
-    response = staff_api_client.post_graphql(query, variables)
+    response = staff_api_client.post_graphql(USER_QUERY, variables)
     content = get_graphql_content(response)
     assert content['data']['user']['email'] == customer_user.email
 
-    response = staff_api_client.post_graphql(query_2)
+    response = staff_api_client.post_graphql(query)
     content = get_graphql_content(response)
     assert content['data']['customers']['totalCount'] == 1
 
@@ -408,6 +447,67 @@ def test_customer_update(
     assert not data['user']['isActive']
 
 
+UPDATE_LOGGED_CUSTOMER_QUERY = """
+    mutation UpdateLoggedCustomer($billing: AddressInput,
+                                  $shipping: AddressInput) {
+        loggedUserUpdate(
+          input: {
+            defaultBillingAddress: $billing,
+            defaultShippingAddress: $shipping,
+        }) {
+            errors {
+                field
+                message
+            }
+            user {
+                email
+                defaultBillingAddress {
+                    id
+                }
+                defaultShippingAddress {
+                    id
+                }
+            }
+        }
+    }
+"""
+
+
+def test_logged_customer_update(user_api_client, graphql_address_data):
+    # this test requires addresses to be set and checks whether new address
+    # instances weren't created, but the existing ones got updated
+    user = user_api_client.user
+    new_first_name = graphql_address_data['firstName']
+    assert user.default_billing_address
+    assert user.default_shipping_address
+    assert user.default_billing_address.first_name != new_first_name
+    assert user.default_shipping_address.first_name != new_first_name
+    variables = {
+        'billing': graphql_address_data,
+        'shipping': graphql_address_data}
+    response = user_api_client.post_graphql(
+        UPDATE_LOGGED_CUSTOMER_QUERY, variables)
+    content = get_graphql_content(response)
+    data = content['data']['loggedUserUpdate']
+    assert not data['errors']
+
+    # check that existing instances are updated
+    billing_address_pk = user.default_billing_address.pk
+    shipping_address_pk = user.default_shipping_address.pk
+    user = User.objects.get(email=user.email)
+    assert user.default_billing_address.pk == billing_address_pk
+    assert user.default_shipping_address.pk == shipping_address_pk
+
+    assert user.default_billing_address.first_name == new_first_name
+    assert user.default_shipping_address.first_name == new_first_name
+
+
+def test_logged_customer_update_anonymus_user(api_client):
+    response = api_client.post_graphql(
+        UPDATE_LOGGED_CUSTOMER_QUERY, {})
+    assert_no_permission(response)
+
+
 def test_customer_delete(staff_api_client, customer_user, permission_manage_users):
     query = """
     mutation CustomerDelete($id: ID!) {
@@ -847,4 +947,3 @@ def test_customer_reset_password(
     content = get_graphql_content(response)
     assert send_password_reset_mock.called
     assert send_password_reset_mock.mock_calls[0][1][1] == customer_user.email
-