Repository: saleor/saleor

Issue:
Protect meta field for User model
Currently, we don't require any permissions for `updateMetaUser` mutation. This should be protected and accessible only for logged-in users.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/account/mutations/account.py b/saleor/graphql/account/mutations/account.py
--- a/saleor/graphql/account/mutations/account.py
+++ b/saleor/graphql/account/mutations/account.py
@@ -8,7 +8,13 @@
 from ....core.utils.url import validate_storefront_url
 from ...account.enums import AddressTypeEnum
 from ...account.types import Address, AddressInput, User
-from ...core.mutations import BaseMutation, ModelDeleteMutation, ModelMutation
+from ...core.mutations import (
+    BaseMutation,
+    ModelDeleteMutation,
+    ModelMutation,
+    UpdateMetaBaseMutation,
+)
+from ...core.types import MetaInput
 from ...core.types.common import AccountError
 from .base import (
     INVALID_TOKEN,
@@ -262,3 +268,26 @@ def perform_mutation(cls, _root, info, **data):
 
         utils.change_user_default_address(user, address, address_type)
         return cls(user=user)
+
+
+class AccountUpdateMeta(UpdateMetaBaseMutation):
+    class Meta:
+        description = "Updates metadata of the logged-in user."
+        model = models.User
+        public = True
+        error_type_class = AccountError
+        error_type_field = "account_errors"
+
+    class Arguments:
+        input = MetaInput(
+            description="Fields required to update new or stored metadata item.",
+            required=True,
+        )
+
+    @classmethod
+    def check_permissions(cls, context):
+        return context.user.is_authenticated
+
+    @classmethod
+    def get_instance(cls, info, **data):
+        return info.context.user
diff --git a/saleor/graphql/account/mutations/base.py b/saleor/graphql/account/mutations/base.py
--- a/saleor/graphql/account/mutations/base.py
+++ b/saleor/graphql/account/mutations/base.py
@@ -385,6 +385,7 @@ class Meta:
         public = True
         error_type_class = AccountError
         error_type_field = "account_errors"
+        permissions = ("account.manage_users",)
 
 
 class UserClearMeta(ClearMetaBaseMutation):
@@ -394,3 +395,4 @@ class Meta:
         public = True
         error_type_class = AccountError
         error_type_field = "account_errors"
+        permissions = ("account.manage_users",)
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
@@ -17,6 +17,7 @@
     AccountRequestDeletion,
     AccountSetDefaultAddress,
     AccountUpdate,
+    AccountUpdateMeta,
 )
 from .mutations.base import (
     PasswordChange,
@@ -176,9 +177,6 @@ class AccountMutations(graphene.ObjectType):
     set_password = SetPassword.Field()
     password_change = PasswordChange.Field()
 
-    user_update_metadata = UserUpdateMeta.Field()
-    user_clear_metadata = UserClearMeta.Field()
-
     # Account mutations
     account_address_create = AccountAddressCreate.Field()
     account_address_update = AccountAddressUpdate.Field()
@@ -190,6 +188,8 @@ class AccountMutations(graphene.ObjectType):
     account_request_deletion = AccountRequestDeletion.Field()
     account_delete = AccountDelete.Field()
 
+    account_update_meta = AccountUpdateMeta.Field()
+
     # Account deprecated mutations
     customer_password_reset = CustomerPasswordReset.Field()
 
@@ -219,6 +219,9 @@ class AccountMutations(graphene.ObjectType):
     user_avatar_delete = UserAvatarDelete.Field()
     user_bulk_set_active = UserBulkSetActive.Field()
 
+    user_update_metadata = UserUpdateMeta.Field()
+    user_clear_metadata = UserClearMeta.Field()
+
     user_update_private_metadata = UserUpdatePrivateMeta.Field()
     user_clear_private_metadata = UserClearPrivateMeta.Field()
 


Test patch:
diff --git a/tests/api/test_account_meta.py b/tests/api/test_account_meta.py
--- a/tests/api/test_account_meta.py
+++ b/tests/api/test_account_meta.py
@@ -3,7 +3,7 @@
 import graphene
 import pytest
 
-from tests.api.utils import get_graphql_content
+from tests.api.utils import assert_no_permission, get_graphql_content
 
 PRIVATE_META_NAMESPACE = "TEST_NAMESPACE"
 PUBLIC_META_NAMESPACE = "PUBLIC_NAMESPACE"
@@ -99,7 +99,7 @@ def test_user_has_no_access_to_private_meta(user_api_client, customer_with_meta)
 
 
 UPDATE_PRIVATE_METADATA_MUTATION = """
-    mutation TokenCreate($id: ID!, $input: MetaInput!) {
+    mutation UserUpdatePrivateMetadata($id: ID!, $input: MetaInput!) {
       userUpdatePrivateMetadata(
         id: $id
         input: $input
@@ -328,7 +328,7 @@ def test_staff_access_to_public_metadata(
 
 
 UPDATE_METADATA_MUTATION = """
-    mutation TokenCreate($id: ID!, $input: MetaInput!) {
+    mutation UserUpdateMetadata($id: ID!, $input: MetaInput!) {
       userUpdateMetadata(
         id: $id
         input: $input
@@ -350,7 +350,9 @@ def test_staff_access_to_public_metadata(
 """
 
 
-def test_update_metadata_through_mutation(user_api_client, customer_with_meta):
+def test_staff_update_metadata_through_mutation(
+    staff_api_client, permission_manage_users, customer_with_meta
+):
     NEW_VALUE = "NEW_VALUE"
     user_id = graphene.Node.to_global_id("User", customer_with_meta.id)
     variables = {
@@ -362,7 +364,10 @@ def test_update_metadata_through_mutation(user_api_client, customer_with_meta):
             "value": NEW_VALUE,
         },
     }
-    resp = user_api_client.post_graphql(UPDATE_METADATA_MUTATION, variables)
+
+    resp = staff_api_client.post_graphql(
+        UPDATE_METADATA_MUTATION, variables, permissions=[permission_manage_users]
+    )
     meta = get_graphql_content(resp)["data"]["userUpdateMetadata"]["user"]["meta"][0]
 
     assert meta["namespace"] == PUBLIC_META_NAMESPACE
@@ -371,8 +376,8 @@ def test_update_metadata_through_mutation(user_api_client, customer_with_meta):
     ]
 
 
-def test_add_new_key_value_pair_to_metadata_using_mutation(
-    user_api_client, customer_with_meta
+def test_staff_add_new_key_value_pair_to_metadata_using_mutation(
+    staff_api_client, customer_with_meta, permission_manage_users
 ):
     NEW_KEY = "NEW_KEY"
     NEW_VALUE = "NEW_VALUE"
@@ -386,7 +391,9 @@ def test_add_new_key_value_pair_to_metadata_using_mutation(
             "value": NEW_VALUE,
         },
     }
-    response = user_api_client.post_graphql(UPDATE_METADATA_MUTATION, variables)
+    response = staff_api_client.post_graphql(
+        UPDATE_METADATA_MUTATION, variables, permissions=[permission_manage_users]
+    )
     meta = get_graphql_content(response)["data"]["userUpdateMetadata"]["user"]["meta"][
         0
     ]
@@ -399,7 +406,9 @@ def test_add_new_key_value_pair_to_metadata_using_mutation(
     assert meta["clients"] == [{"metadata": expected_metadata, "name": META_CLIENT}]
 
 
-def test_add_new_namespace_metadata_using_mutation(user_api_client, customer_with_meta):
+def test_add_new_namespace_metadata_using_mutation(
+    staff_api_client, customer_with_meta, permission_manage_users
+):
     NEW_KEY = "NEW_KEY"
     NEW_VALUE = "NEW_VALUE"
     NEW_NAMESPACE = "NEW_NAMESPACE"
@@ -415,7 +424,9 @@ def test_add_new_namespace_metadata_using_mutation(user_api_client, customer_wit
             "value": NEW_VALUE,
         },
     }
-    response = user_api_client.post_graphql(UPDATE_METADATA_MUTATION, variables)
+    response = staff_api_client.post_graphql(
+        UPDATE_METADATA_MUTATION, variables, permissions=[permission_manage_users]
+    )
     meta = get_graphql_content(response)["data"]["userUpdateMetadata"]["user"]["meta"]
 
     assert meta[0]["namespace"] == NEW_NAMESPACE
@@ -452,7 +463,9 @@ def test_add_new_namespace_metadata_using_mutation(user_api_client, customer_wit
 """
 
 
-def test_clear_metadata_through_mutation(user_api_client, customer_with_meta):
+def test_staff_clear_metadata_through_mutation(
+    staff_api_client, customer_with_meta, permission_manage_users
+):
     user_id = graphene.Node.to_global_id("User", customer_with_meta.id)
     variables = {
         "id": user_id,
@@ -462,7 +475,9 @@ def test_clear_metadata_through_mutation(user_api_client, customer_with_meta):
             "key": PUBLIC_KEY,
         },
     }
-    response = user_api_client.post_graphql(CLEAR_METADATA_MUTATION, variables)
+    response = staff_api_client.post_graphql(
+        CLEAR_METADATA_MUTATION, variables, permissions=[permission_manage_users]
+    )
     meta = get_graphql_content(response)["data"]["userClearMetadata"]["user"]["meta"][0]
 
     assert meta["namespace"] == PUBLIC_META_NAMESPACE
@@ -482,10 +497,91 @@ def test_clear_silently_metadata_from_nonexistent_client(
             "key": PUBLIC_KEY,
         },
     }
-    response = staff_api_client.post_graphql(CLEAR_METADATA_MUTATION, variables)
+    response = staff_api_client.post_graphql(
+        CLEAR_METADATA_MUTATION, variables, permissions=[permission_manage_users]
+    )
     meta = get_graphql_content(response)["data"]["userClearMetadata"]["user"]["meta"][0]
 
     assert meta["namespace"] == PUBLIC_META_NAMESPACE
     assert meta["clients"] == [
         {"metadata": [{"key": PUBLIC_KEY, "value": PUBLIC_VALUE}], "name": META_CLIENT}
     ]
+
+
+@pytest.mark.parametrize(
+    "mutation", [UPDATE_METADATA_MUTATION, UPDATE_PRIVATE_METADATA_MUTATION]
+)
+def test_staff_update_meta_without_permissions(
+    staff_api_client, customer_with_meta, mutation
+):
+    user_id = graphene.Node.to_global_id("User", customer_with_meta.id)
+    variables = {
+        "id": user_id,
+        "input": {
+            "namespace": "new_namespace",
+            "clientName": "client_name",
+            "key": "meta_key",
+            "value": "value",
+        },
+    }
+    response = staff_api_client.post_graphql(mutation, variables)
+    assert_no_permission(response)
+
+
+@pytest.mark.parametrize(
+    "mutation", [CLEAR_METADATA_MUTATION, CLEAR_PRIVATE_METADATA_MUTATION]
+)
+def test_staff_clear_meta_without_permissions(
+    staff_api_client, customer_with_meta, mutation
+):
+    user_id = graphene.Node.to_global_id("User", customer_with_meta.id)
+    variables = {
+        "id": user_id,
+        "input": {
+            "namespace": PUBLIC_META_NAMESPACE,
+            "clientName": META_CLIENT,
+            "key": PUBLIC_KEY,
+        },
+    }
+    response = staff_api_client.post_graphql(mutation, variables)
+    assert_no_permission(response)
+
+
+UPDATE_ACCOUNT_META_MUTATION = """
+    mutation AccountUpdateMeta($input: MetaInput!) {
+      accountUpdateMeta(input: $input){
+        user{
+          meta{
+            namespace
+            clients{
+              name
+              metadata{
+                key
+                value
+              }
+            }
+          }
+        }
+      }
+    }
+"""
+
+
+def test_user_updates_own_meta(user_api_client, customer_with_meta):
+    NEW_VALUE = "NEW_VALUE"
+    variables = {
+        "input": {
+            "namespace": PUBLIC_META_NAMESPACE,
+            "clientName": META_CLIENT,
+            "key": PUBLIC_KEY,
+            "value": NEW_VALUE,
+        }
+    }
+
+    resp = user_api_client.post_graphql(UPDATE_ACCOUNT_META_MUTATION, variables)
+    meta = get_graphql_content(resp)["data"]["accountUpdateMeta"]["user"]["meta"][0]
+
+    assert meta["namespace"] == PUBLIC_META_NAMESPACE
+    assert meta["clients"] == [
+        {"metadata": [{"key": PUBLIC_KEY, "value": NEW_VALUE}], "name": META_CLIENT}
+    ]