Repository: saleor/saleor
Pull request: Hide all secret fields in plugin configuration
- Adds new config fields - `Secret`, `Password`
   - `Secret`  dedicated for fields like `Public Key`, `Private Key`
   - `Password` dedicated to fields that should be fully hidden.
- Remove `hide_secret_fields`logic  from `BasePlugin`
- Hide secret logic on graphene type level - closes #4937
For API:
    - `Secret returns last 4 or 1 digits for token. Null if secret is empty
    - `Password` returns empty string if password exists. Null if password is empty




Problem statement:
Plugins query doesn't have all fields
```
class ExtensionsManager(PaymentInterface):
    def get_plugin_configurations(self) -> List["PluginConfiguration"]:
        plugin_configuration_ids = []
        plugin_configurations_qs = PluginConfiguration.objects.all()
        for plugin in self.plugins:
            plugin_configuration = plugin.get_plugin_configuration(
                plugin_configurations_qs
            )
            plugin_configuration_ids.append(plugin_configuration.pk)
        return PluginConfiguration.objects.filter(pk__in=plugin_configuration_ids)
```
`get_plugin_configurations` returns configuration directly from database and skips all static fields that we attached in real time in `plugin.get_plugin_configuration`. 
`filter` input field in query requires QuerySet as a returned value from resolvers (previosuly it was `list`)

Hints from issue discussion:

Commit message: Hide all secret fields in plugin configuration

Patch to review:
diff --git a/saleor/dashboard/forms.py b/saleor/dashboard/forms.py
index 6fc3c2675fe..ffb39d7562c 100644
--- a/saleor/dashboard/forms.py
+++ b/saleor/dashboard/forms.py
@@ -219,3 +219,9 @@ class ConfigCharField(forms.CharField, ConfigField):
     def clean(self, value):
         parsed_value = super().clean(value)
         return {"name": self.label, "value": parsed_value}
+
+
+class ConfigPasswordField(ConfigCharField):
+    def __init__(self, *args, **kwargs):
+        self.widget = forms.PasswordInput(render_value=True)
+        super().__init__(*args, **kwargs)
diff --git a/saleor/dashboard/payment/forms.py b/saleor/dashboard/payment/forms.py
index 5a706eecd3d..1821492c805 100644
--- a/saleor/dashboard/payment/forms.py
+++ b/saleor/dashboard/payment/forms.py
@@ -5,11 +5,13 @@
 
 from ...extensions import ConfigurationTypeField
 from ...extensions.models import PluginConfiguration
-from ..forms import ConfigBooleanField, ConfigCharField
+from ..forms import ConfigBooleanField, ConfigCharField, ConfigPasswordField
 
 TYPE_TO_FIELD = {
     ConfigurationTypeField.STRING: ConfigCharField,
     ConfigurationTypeField.BOOLEAN: ConfigBooleanField,
+    ConfigurationTypeField.SECRET: ConfigPasswordField,
+    ConfigurationTypeField.PASSWORD: ConfigPasswordField,
 }
 
 
diff --git a/saleor/extensions/__init__.py b/saleor/extensions/__init__.py
index 2ff4b876ae1..c29788c213b 100644
--- a/saleor/extensions/__init__.py
+++ b/saleor/extensions/__init__.py
@@ -24,10 +24,17 @@ def discover_plugins_modules(plugins: List[str]):
 class ConfigurationTypeField:
     STRING = "String"
     BOOLEAN = "Boolean"
+    SECRET = "Secret"
+    PASSWORD = "Password"
     CHOICES = [
         (STRING, pgettext_lazy("Type of the configuration field", "Field is a String")),
         (
             BOOLEAN,
             pgettext_lazy("Type of the configuration field", "Field is a Boolean"),
         ),
+        (SECRET, pgettext_lazy("Type of the configuration field", "Field is a Secret")),
+        (
+            PASSWORD,
+            pgettext_lazy("Type of the configuration field", "Field is a Password"),
+        ),
     ]
diff --git a/saleor/extensions/base_plugin.py b/saleor/extensions/base_plugin.py
index 7f8f54b9d1c..29433465797 100644
--- a/saleor/extensions/base_plugin.py
+++ b/saleor/extensions/base_plugin.py
@@ -28,7 +28,6 @@ class BasePlugin:
 
     PLUGIN_NAME = ""
     CONFIG_STRUCTURE = None
-    REDACTED_FORM = "*" * 10
 
     def __init__(self, *args, **kwargs):
         self._cached_config = None
@@ -330,11 +329,6 @@ def _update_config_items(
                 config_item_name = config_item_to_update.get("name")
                 if config_item["name"] == config_item_name:
                     new_value = config_item_to_update.get("value")
-                    # The frontend can send all configuration fields with
-                    # all secrets as **...**, we have to omit all secrets which
-                    # weren't modified
-                    if new_value == cls.REDACTED_FORM:
-                        continue
                     item_type = config_structure.get(config_item_name, {}).get("type")
                     if item_type == ConfigurationTypeField.BOOLEAN and not isinstance(
                         new_value, bool
@@ -365,7 +359,6 @@ def save_plugin_configuration(
         if plugin_configuration.configuration:
             # Let's add a translated descriptions and labels
             cls._append_config_structure(plugin_configuration.configuration)
-            cls._hide_secret_configuration_fields(plugin_configuration.configuration)
         return plugin_configuration
 
     @classmethod
@@ -379,11 +372,6 @@ def _get_default_configuration(cls):
         defaults = None
         return defaults
 
-    @classmethod
-    def _hide_secret_configuration_fields(cls, configuration: List[dict]) -> None:
-        """Hide secret values of the configuration fields."""
-        return
-
     @classmethod
     def _append_config_structure(cls, configuration):
         """Append configuration structure to config from the database.
@@ -427,7 +415,11 @@ def _update_configuration_structure(cls, configuration):
         configuration.save(update_fields=["configuration"])
 
     @classmethod
-    def get_plugin_configuration(cls, queryset: QuerySet) -> "PluginConfiguration":
+    def get_plugin_configuration(
+        cls, queryset: QuerySet = None
+    ) -> "PluginConfiguration":
+        if not queryset:
+            queryset = PluginConfiguration.objects.all()
         defaults = cls._get_default_configuration()
         configuration = queryset.get_or_create(name=cls.PLUGIN_NAME, defaults=defaults)[
             0
@@ -436,5 +428,4 @@ def get_plugin_configuration(cls, queryset: QuerySet) -> "PluginConfiguration":
         if configuration.configuration:
             # Let's add a translated descriptions and labels
             cls._append_config_structure(configuration.configuration)
-            cls._hide_secret_configuration_fields(configuration.configuration)
         return configuration
diff --git a/saleor/extensions/plugins/avatax/plugin.py b/saleor/extensions/plugins/avatax/plugin.py
index fb205938715..09d4b5535c1 100644
--- a/saleor/extensions/plugins/avatax/plugin.py
+++ b/saleor/extensions/plugins/avatax/plugin.py
@@ -48,7 +48,7 @@ class AvataxPlugin(BasePlugin):
             "label": pgettext_lazy("Plugin label", "Username or account"),
         },
         "Password or license": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.PASSWORD,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide password or license details"
             ),
@@ -442,13 +442,6 @@ def validate_plugin_configuration(cls, plugin_configuration: "PluginConfiguratio
                 code=ExtensionsErrorCode.PLUGIN_MISCONFIGURED,
             )
 
-    @classmethod
-    def _hide_secret_configuration_fields(cls, configuration):
-        for field in configuration:
-            if field.get("name") == "Password or license" and field.get("value"):
-                # We don't want to share our secret data
-                field["value"] = cls.REDACTED_FORM
-
     @classmethod
     def _get_default_configuration(cls):
         defaults = {
@@ -456,8 +449,8 @@ def _get_default_configuration(cls):
             "description": "",
             "active": False,
             "configuration": [
-                {"name": "Username or account", "value": ""},
-                {"name": "Password or license", "value": ""},
+                {"name": "Username or account", "value": None},
+                {"name": "Password or license", "value": None},
                 {"name": "Use sandbox", "value": True},
                 {"name": "Company name", "value": "DEFAULT"},
                 {"name": "Autocommit", "value": False},
diff --git a/saleor/graphql/extensions/types.py b/saleor/graphql/extensions/types.py
index 864f58826f4..662d730e310 100644
--- a/saleor/graphql/extensions/types.py
+++ b/saleor/graphql/extensions/types.py
@@ -1,13 +1,13 @@
 import graphene
 
-from ...extensions import models
+from ...extensions import ConfigurationTypeField, manager, models
 from ..core.connection import CountableDjangoObjectType
 from .enums import ConfigurationTypeFieldEnum
 
 
 class ConfigurationItem(graphene.ObjectType):
     name = graphene.String(required=True, description="Name of the field.")
-    value = graphene.String(required=True, description="Current value of the field.")
+    value = graphene.String(required=False, description="Current value of the field.")
     type = graphene.Field(ConfigurationTypeFieldEnum, description="Type of the field.")
     help_text = graphene.String(required=False, description="Help text for the field.")
     label = graphene.String(required=False, description="Label for the field.")
@@ -24,3 +24,25 @@ class Meta:
         model = models.PluginConfiguration
         interfaces = [graphene.relay.Node]
         only_fields = ["name", "description", "active", "configuration"]
+
+    def resolve_configuration(self, _info):
+        plugin = manager.get_extensions_manager().get_plugin(self.name)
+        configuration = plugin.get_plugin_configuration().configuration
+        if plugin.CONFIG_STRUCTURE and configuration:
+            for field in configuration:
+                name = field["name"]
+                value = field["value"]
+                if value is None:
+                    continue
+                field_type = plugin.CONFIG_STRUCTURE.get(name, {}).get("type")
+                if field_type == ConfigurationTypeField.PASSWORD:
+                    field["value"] = "" if value else None
+
+                if field_type == ConfigurationTypeField.SECRET:
+                    if not value:
+                        field["value"] = None
+                    elif len(value) > 4:
+                        field["value"] = value[-4:]
+                    else:
+                        field["value"] = value[-1:]
+        return configuration
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index 4d12f342c11..0cf0146afde 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -960,7 +960,7 @@ type CollectionUpdatePrivateMeta {
 
 type ConfigurationItem {
   name: String!
-  value: String!
+  value: String
   type: ConfigurationTypeFieldEnum
   helpText: String
   label: String
@@ -974,6 +974,8 @@ input ConfigurationItemInput {
 enum ConfigurationTypeFieldEnum {
   STRING
   BOOLEAN
+  SECRET
+  PASSWORD
 }
 
 enum CountryCode {
diff --git a/saleor/payment/gateways/braintree/plugin.py b/saleor/payment/gateways/braintree/plugin.py
index 9f2055840b3..870c5af5bce 100644
--- a/saleor/payment/gateways/braintree/plugin.py
+++ b/saleor/payment/gateways/braintree/plugin.py
@@ -46,21 +46,21 @@ class BraintreeGatewayPlugin(BasePlugin):
             "label": pgettext_lazy("Plugin label", "Template path"),
         },
         "Public API key": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide Braintree public API key"
             ),
             "label": pgettext_lazy("Plugin label", "Public API key"),
         },
         "Secret API key": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide Braintree secret API key"
             ),
             "label": pgettext_lazy("Plugin label", "Secret API key"),
         },
         "Merchant ID": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide Braintree merchant ID"
             ),
@@ -131,14 +131,6 @@ def _initialize_plugin_configuration(self):
                 require_3d_secure=configuration["Require 3D secure"],
             )
 
-    @classmethod
-    def _hide_secret_configuration_fields(cls, configuration):
-        secret_fields = ["Public API key", "Secret API key", "Merchant ID"]
-        for field in configuration:
-            # We don't want to share our secret data
-            if field.get("name") in secret_fields and field.get("value"):
-                field["value"] = cls.REDACTED_FORM
-
     @classmethod
     def _get_default_configuration(cls):
         defaults = {
@@ -147,10 +139,10 @@ def _get_default_configuration(cls):
             "active": False,
             "configuration": [
                 {"name": "Template path", "value": "order/payment/braintree.html"},
-                {"name": "Public API key", "value": ""},
-                {"name": "Secret API key", "value": ""},
+                {"name": "Public API key", "value": None},
+                {"name": "Secret API key", "value": None},
                 {"name": "Use sandbox", "value": True},
-                {"name": "Merchant ID", "value": ""},
+                {"name": "Merchant ID", "value": None},
                 {"name": "Store customers card", "value": False},
                 {"name": "Automatic payment capture", "value": True},
                 {"name": "Require 3D secure", "value": False},
diff --git a/saleor/payment/gateways/razorpay/plugin.py b/saleor/payment/gateways/razorpay/plugin.py
index bdb328fb238..d12f32a6d3e 100644
--- a/saleor/payment/gateways/razorpay/plugin.py
+++ b/saleor/payment/gateways/razorpay/plugin.py
@@ -36,12 +36,12 @@ class RazorpayGatewayPlugin(BasePlugin):
             "label": pgettext_lazy("Plugin label", "Template path"),
         },
         "Public API key": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy("Plugin help text", "Provide  public API key"),
             "label": pgettext_lazy("Plugin label", "Public API key"),
         },
         "Secret API key": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide Stripe secret API key"
             ),
@@ -88,14 +88,6 @@ def _initialize_plugin_configuration(self):
                 store_customer=configuration["Store customers card"],
             )
 
-    @classmethod
-    def _hide_secret_configuration_fields(cls, configuration):
-        secret_fields = ["Public API key", "Secret API key"]
-        for field in configuration:
-            # We don't want to share our secret data
-            if field.get("name") in secret_fields and field.get("value"):
-                field["value"] = cls.REDACTED_FORM
-
     @classmethod
     def _get_default_configuration(cls):
         defaults = {
@@ -104,8 +96,8 @@ def _get_default_configuration(cls):
             "active": False,
             "configuration": [
                 {"name": "Template path", "value": "order/payment/razorpay.html"},
-                {"name": "Public API key", "value": ""},
-                {"name": "Secret API key", "value": ""},
+                {"name": "Public API key", "value": None},
+                {"name": "Secret API key", "value": None},
                 {"name": "Store customers card", "value": False},
                 {"name": "Automatic payment capture", "value": True},
             ],
diff --git a/saleor/payment/gateways/stripe/plugin.py b/saleor/payment/gateways/stripe/plugin.py
index 7cc19b4dfd0..13fc73a705c 100644
--- a/saleor/payment/gateways/stripe/plugin.py
+++ b/saleor/payment/gateways/stripe/plugin.py
@@ -36,14 +36,14 @@ class StripeGatewayPlugin(BasePlugin):
     PLUGIN_NAME = GATEWAY_NAME
     CONFIG_STRUCTURE = {
         "Public API key": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide Stripe public API key"
             ),
             "label": pgettext_lazy("Plugin label", "Public API key"),
         },
         "Secret API key": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.SECRET,
             "help_text": pgettext_lazy(
                 "Plugin help text", "Provide Stripe secret API key"
             ),
@@ -90,14 +90,6 @@ def _initialize_plugin_configuration(self):
                 store_customer=configuration["Store customers card"],
             )
 
-    @classmethod
-    def _hide_secret_configuration_fields(cls, configuration):
-        secret_fields = ["Public API key", "Secret API key"]
-        for field in configuration:
-            # We don't want to share our secret data
-            if field.get("name") in secret_fields and field.get("value"):
-                field["value"] = cls.REDACTED_FORM
-
     @classmethod
     def _get_default_configuration(cls):
         defaults = {
@@ -105,8 +97,8 @@ def _get_default_configuration(cls):
             "description": "",
             "active": False,
             "configuration": [
-                {"name": "Public API key", "value": ""},
-                {"name": "Secret API key", "value": ""},
+                {"name": "Public API key", "value": None},
+                {"name": "Secret API key", "value": None},
                 {"name": "Store customers card", "value": False},
                 {"name": "Automatic payment capture", "value": True},
             ],
diff --git a/tests/api/test_extensions.py b/tests/api/test_extensions.py
index ea86ddf8eab..30ee4dc32d1 100644
--- a/tests/api/test_extensions.py
+++ b/tests/api/test_extensions.py
@@ -17,10 +17,15 @@ class PluginSample(BasePlugin):
             "label": "Username",
         },
         "Password": {
-            "type": ConfigurationTypeField.STRING,
+            "type": ConfigurationTypeField.PASSWORD,
             "help_text": "Password input field",
             "label": "Password",
         },
+        "API private key": {
+            "type": ConfigurationTypeField.SECRET,
+            "help_text": "API key",
+            "label": "Private key",
+        },
         "Use sandbox": {
             "type": ConfigurationTypeField.BOOLEAN,
             "help_text": "Use sandbox",
@@ -29,48 +34,22 @@ class PluginSample(BasePlugin):
     }
 
     @classmethod
-    def get_plugin_configuration(cls, queryset) -> "PluginConfiguration":
-        qs = queryset.filter(name="PluginSample")
-        if qs.exists():
-            return qs[0]
+    def _get_default_configuration(cls):
         defaults = {
             "name": "PluginSample",
             "description": "Test plugin description",
             "active": True,
             "configuration": [
-                {
-                    "name": "Username",
-                    "value": "admin",
-                    "type": ConfigurationTypeField.STRING,
-                    "help_text": "Username input field",
-                    "label": "Username",
-                },
-                {
-                    "name": "Password",
-                    "value": "123",
-                    "type": ConfigurationTypeField.STRING,
-                    "help_text": "Password input field",
-                    "label": "Password",
-                },
-                {
-                    "name": "Use sandbox",
-                    "value": False,
-                    "type": ConfigurationTypeField.BOOLEAN,
-                    "help_text": "Use sandbox",
-                    "label": "Use sandbox",
-                },
+                {"name": "Username", "value": "admin"},
+                {"name": "Password", "value": None},
+                {"name": "API private key", "value": None},
+                {"name": "Use sandbox", "value": False},
             ],
         }
-        return PluginConfiguration.objects.create(**defaults)
+        return defaults
 
 
-def test_query_plugin_configurations(
-    staff_api_client, permission_manage_plugins, settings
-):
-
-    # Enable test plugin
-    settings.PLUGINS = ["tests.api.test_extensions.PluginSample"]
-    query = """
+PLUGINS_QUERY = """
         {
           plugins(first:1){
             edges{
@@ -91,20 +70,72 @@ def test_query_plugin_configurations(
           }
         }
     """
+
+
+@pytest.mark.parametrize(
+    "password, expected_password, api_key, expected_api_key",
+    [
+        (None, None, None, None),
+        ("ABCDEFGHIJ", "", "123456789", "6789"),
+        ("", None, "", None),
+        (None, None, "1234", "4"),
+    ],
+)
+def test_query_plugins_hides_secret_fields(
+    password,
+    expected_password,
+    api_key,
+    expected_api_key,
+    staff_api_client,
+    permission_manage_plugins,
+    settings,
+):
+
+    settings.PLUGINS = ["tests.api.test_extensions.PluginSample"]
+    manager = get_extensions_manager()
+    plugin_configuration = manager.get_plugin_configuration(PluginSample.PLUGIN_NAME)
+    for conf_field in plugin_configuration.configuration:
+        if conf_field["name"] == "Password":
+            conf_field["value"] = password
+        if conf_field["name"] == "API private key":
+            conf_field["value"] = api_key
+    plugin_configuration.save()
+
+    staff_api_client.user.user_permissions.add(permission_manage_plugins)
+    response = staff_api_client.post_graphql(PLUGINS_QUERY)
+    content = get_graphql_content(response)
+
+    plugins = content["data"]["plugins"]["edges"]
+    assert len(plugins) == 1
+    plugin = plugins[0]["node"]
+
+    for conf_field in plugin["configuration"]:
+        if conf_field["name"] == "Password":
+            assert conf_field["value"] == expected_password
+        if conf_field["name"] == "API private key":
+            assert conf_field["value"] == expected_api_key
+
+
+def test_query_plugin_configurations(
+    staff_api_client, permission_manage_plugins, settings
+):
+
+    # Enable test plugin
+    settings.PLUGINS = ["tests.api.test_extensions.PluginSample"]
     staff_api_client.user.user_permissions.add(permission_manage_plugins)
-    response = staff_api_client.post_graphql(query)
+    response = staff_api_client.post_graphql(PLUGINS_QUERY)
     content = get_graphql_content(response)
 
     plugins = content["data"]["plugins"]["edges"]
 
     assert len(plugins) == 1
     plugin = plugins[0]["node"]
-    plugin_configuration = PluginConfiguration.objects.get()
+    manager = get_extensions_manager()
+    plugin_configuration = manager.get_plugin_configuration(PluginSample.PLUGIN_NAME)
 
     assert plugin["name"] == plugin_configuration.name
     assert plugin["active"] == plugin_configuration.active
     assert plugin["description"] == plugin_configuration.description
-
     for index, configuration_item in enumerate(plugin["configuration"]):
         assert (
             configuration_item["name"]
@@ -119,6 +150,8 @@ def test_query_plugin_configurations(
                 configuration_item["value"]
                 == plugin_configuration.configuration[index]["value"]
             )
+        elif configuration_item["value"] is None:
+            assert not plugin_configuration.configuration[index]["value"]
         else:
             assert (
                 configuration_item["value"]
@@ -134,15 +167,7 @@ def test_query_plugin_configurations(
         )
 
 
-def test_query_plugin_configuration(
-    staff_api_client, permission_manage_plugins, settings
-):
-    settings.PLUGINS = ["tests.api.test_extensions.PluginSample"]
-    manager = get_extensions_manager()
-    plugin_configuration = manager.get_plugin_configuration("PluginSample")
-    configuration_id = graphene.Node.to_global_id("Plugin", plugin_configuration.pk)
-
-    query = """
+PLUGIN_QUERY = """
     query plugin($id: ID!){
       plugin(id:$id){
         name
@@ -157,10 +182,64 @@ def test_query_plugin_configuration(
         }
       }
     }
-    """
+"""
+
+
+@pytest.mark.parametrize(
+    "password, expected_password, api_key, expected_api_key",
+    [
+        (None, None, None, None),
+        ("ABCDEFGHIJ", "", "123456789", "6789"),
+        ("", None, "", None),
+        (None, None, "1234", "4"),
+    ],
+)
+def test_query_plugin_hides_secret_fields(
+    password,
+    expected_password,
+    api_key,
+    expected_api_key,
+    staff_api_client,
+    permission_manage_plugins,
+    settings,
+):
+
+    settings.PLUGINS = ["tests.api.test_extensions.PluginSample"]
+    manager = get_extensions_manager()
+    plugin_configuration = manager.get_plugin_configuration(PluginSample.PLUGIN_NAME)
+    for conf_field in plugin_configuration.configuration:
+        if conf_field["name"] == "Password":
+            conf_field["value"] = password
+        if conf_field["name"] == "API private key":
+            conf_field["value"] = api_key
+    plugin_configuration.save()
+    configuration_id = graphene.Node.to_global_id("Plugin", plugin_configuration.pk)
+
     variables = {"id": configuration_id}
     staff_api_client.user.user_permissions.add(permission_manage_plugins)
-    response = staff_api_client.post_graphql(query, variables)
+    response = staff_api_client.post_graphql(PLUGIN_QUERY, variables)
+    content = get_graphql_content(response)
+
+    plugin = content["data"]["plugin"]
+
+    for conf_field in plugin["configuration"]:
+        if conf_field["name"] == "Password":
+            assert conf_field["value"] == expected_password
+        if conf_field["name"] == "API private key":
+            assert conf_field["value"] == expected_api_key
+
+
+def test_query_plugin_configuration(
+    staff_api_client, permission_manage_plugins, settings
+):
+    settings.PLUGINS = ["tests.api.test_extensions.PluginSample"]
+    manager = get_extensions_manager()
+    plugin_configuration = manager.get_plugin_configuration("PluginSample")
+    configuration_id = graphene.Node.to_global_id("Plugin", plugin_configuration.pk)
+
+    variables = {"id": configuration_id}
+    staff_api_client.user.user_permissions.add(permission_manage_plugins)
+    response = staff_api_client.post_graphql(PLUGIN_QUERY, variables)
     content = get_graphql_content(response)
     plugin = content["data"]["plugin"]
     assert plugin["name"] == plugin_configuration.name
@@ -237,7 +316,7 @@ def test_plugin_configuration_update(
     response = staff_api_client.post_graphql(PLUGIN_UPDATE_MUTATION, variables)
     get_graphql_content(response)
 
-    plugin.refresh_from_db()
+    plugin = manager.get_plugin_configuration(plugin_name="PluginSample")
     assert plugin.active == active
 
     first_configuration_item = plugin.configuration[0]
diff --git a/tests/gateway/stripe/test_stripe.py b/tests/gateway/stripe/test_stripe.py
index 32c8a373ce7..add0e7689fd 100644
--- a/tests/gateway/stripe/test_stripe.py
+++ b/tests/gateway/stripe/test_stripe.py
@@ -4,7 +4,6 @@
 
 import pytest
 
-from saleor.extensions.manager import get_extensions_manager
 from saleor.payment import ChargeStatus
 from saleor.payment.gateways.stripe import (
     TransactionKind,
@@ -304,8 +303,8 @@ def test_void_error_response(stripe_payment, sandbox_gateway_config):
 @pytest.mark.vcr(filter_headers=["authorization"])
 def test_confirm__intent(stripe_payment, sandbox_gateway_config):
     PAYMENT_INTENT = (
-        "pi_1F6bslIUmJaD6Oqv1MNDaBSv"
-    )  # PI with status "requires_confirmation"
+        "pi_1F6bslIUmJaD6Oqv1MNDaBSv"  # PI with status "requires_confirmation"
+    )
     payment_info = create_payment_information(stripe_payment, PAYMENT_INTENT)
     response = confirm(payment_info, sandbox_gateway_config)
     assert not response.error
@@ -353,28 +352,3 @@ def test_get_client(gateway_config):
 
 def test_get_client_token():
     assert get_client_token() is None
-
-
-def test_get_plugin_configuration_hides_all_stripe_secrets(settings):
-    settings.PLUGINS = ["saleor.payment.gateways.stripe.plugin.StripeGatewayPlugin"]
-    manager = get_extensions_manager()
-    manager.get_plugin_configuration("Stripe")
-    manager.save_plugin_configuration(
-        "Stripe",
-        {
-            "configuration": [
-                {"name": "Public API key", "value": "123"},
-                {"name": "Secret API key", "value": "456"},
-            ]
-        },
-    )
-    plugin_configuration = manager.get_plugin_configuration("Stripe")
-    configuration = plugin_configuration.configuration
-    public_api_key = [
-        field for field in configuration if field["name"] == "Public API key"
-    ][0]
-    secret_api_key = [
-        field for field in configuration if field["name"] == "Secret API key"
-    ][0]
-    assert public_api_key["value"] == "*" * 10
-    assert secret_api_key["value"] == "*" * 10
diff --git a/tests/gateway/test_braintree.py b/tests/gateway/test_braintree.py
index 206da7bc91d..ff39e129a9f 100644
--- a/tests/gateway/test_braintree.py
+++ b/tests/gateway/test_braintree.py
@@ -7,7 +7,6 @@
 from braintree.validation_error import ValidationError
 from django.core.exceptions import ImproperlyConfigured
 
-from saleor.extensions.manager import get_extensions_manager
 from saleor.payment.gateways.braintree import (
     TransactionKind,
     authorize,
@@ -440,35 +439,3 @@ def test_list_customer_sources(sandbox_braintree_gateway_config):
     )
     sources = list_client_sources(sandbox_braintree_gateway_config, CUSTOMER_ID)
     assert sources == [expected_customer_source]
-
-
-def test_get_plugin_configuration_hides_all_braintree_secrets(settings):
-    settings.PLUGINS = [
-        "saleor.payment.gateways.braintree.plugin.BraintreeGatewayPlugin"
-    ]
-    manager = get_extensions_manager()
-    manager.get_plugin_configuration("Braintree")
-    manager.save_plugin_configuration(
-        "Braintree",
-        {
-            "configuration": [
-                {"name": "Public API key", "value": "123"},
-                {"name": "Merchant ID", "value": "456"},
-                {"name": "Secret API key", "value": "456"},
-            ]
-        },
-    )
-    plugin_configuration = manager.get_plugin_configuration("Braintree")
-    configuration = plugin_configuration.configuration
-    public_api_key = [
-        field for field in configuration if field["name"] == "Public API key"
-    ][0]
-    secret_api_key = [
-        field for field in configuration if field["name"] == "Secret API key"
-    ][0]
-    merchant_id = [field for field in configuration if field["name"] == "Merchant ID"][
-        0
-    ]
-    assert public_api_key["value"] == "*" * 10
-    assert secret_api_key["value"] == "*" * 10
-    assert merchant_id["value"] == "*" * 10
diff --git a/tests/gateway/test_razorpay.py b/tests/gateway/test_razorpay.py
index db7f440bca7..e16ca7c06f2 100644
--- a/tests/gateway/test_razorpay.py
+++ b/tests/gateway/test_razorpay.py
@@ -4,7 +4,6 @@
 import pytest
 from razorpay.errors import BadRequestError, ServerError
 
-from saleor.extensions.manager import get_extensions_manager
 from saleor.payment import ChargeStatus, TransactionKind
 from saleor.payment.gateways.razorpay import (
     capture,
@@ -314,28 +313,3 @@ def test_refund_invalid_data(
 
     # Ensure the HTTP error was logged
     assert mocked_logger.call_count == 1
-
-
-def test_get_plugin_configuration_hides_all_razor_secrets(settings):
-    settings.PLUGINS = ["saleor.payment.gateways.razorpay.plugin.RazorpayGatewayPlugin"]
-    manager = get_extensions_manager()
-    manager.get_plugin_configuration("Razorpay")
-    manager.save_plugin_configuration(
-        "Razorpay",
-        {
-            "configuration": [
-                {"name": "Public API key", "value": "123"},
-                {"name": "Secret API key", "value": "456"},
-            ]
-        },
-    )
-    plugin_configuration = manager.get_plugin_configuration("Razorpay")
-    configuration = plugin_configuration.configuration
-    public_api_key = [
-        field for field in configuration if field["name"] == "Public API key"
-    ][0]
-    secret_api_key = [
-        field for field in configuration if field["name"] == "Secret API key"
-    ][0]
-    assert public_api_key["value"] == "*" * 10
-    assert secret_api_key["value"] == "*" * 10


Code review comments:
diff_hunk: @@ -24,3 +24,25 @@ class Meta:
         model = models.PluginConfiguration
         interfaces = [graphene.relay.Node]
         only_fields = ["name", "description", "active", "configuration"]
+
+    def resolve_configuration(self, _info):
+        plugin = manager.get_extensions_manager().get_plugin(self.name)
+        configuration = plugin.get_plugin_configuration().configuration
+        if plugin.CONFIG_STRUCTURE and configuration:
+            for field in configuration:
+                name = field["name"]
+                value = field["value"]
+                if value is None:
+                    continue
+                field_type = plugin.CONFIG_STRUCTURE.get(name, {}).get("type")
+                if field_type == ConfigurationTypeField.PASSWORD:
+                    field["value"] = "" if value else None
+
+                if field_type == ConfigurationTypeField.SECRET:
path: saleor/graphql/extensions/types.py
text: @user1:
Maybe moving this to some separate function would be a good idea? I'm pretty sure that this will be needed somewhere else :)