Repository: nautobot/nautobot

Issue:
Add new secret - 'Validate' Button
### As ...

Nelly - Network Engineer

### I want ...

To confirm that the values entered in "Add new Secret" dialog are correct.

### So that ...

Data quality can be ensured without having to trigger some functionality that uses the newly created secret for validation.

### I know this is done when...

There should be a "Validate" button, that tries to retrieve the newly defined secret and displays "Secret was successfully retrieved!" or "Failed to get the secret!" message.

Maybe this validation could be automatically triggered when pressing the "Create" button. The secret should be stored without any further interaction when it is successfully validated else a confirmation dialog should be presented to "Nelly" ("Failed to retrieve secret! Store anyway?") to allow correcting the values or storing it anyhow. (e.g. preparing data for a new site where secret-server isn't available yet.)

### Optional - Feature groups this request pertains to.

- [ ] Automation
- [ ] Circuits
- [ ] DCIM
- [ ] IPAM
- [ ] Misc (including Data Sources)
- [ ] Organization
- [ ] Plugins (and other Extensibility)
- [ ] Security (Secrets, etc)
- [ ] Image Management
- [X] UI/UX
- [ ] Documentation
- [ ] Other (not directly a platform feature)

### Database Changes

None

### External Dependencies

None


Discussion:
@bryanculver Is it possible to add the output of the following script on Advanced tab of a secret? 

So no call to the secret.provider is executed as long as someone clicks on the Advanced tab

```python
try: 
  secret.get_value()
  is_secret_valid = True
except SecError:
  is_secret_valid = False
```
I've added a pull request to nautobot-plugin-secrets-providers that solves this by adding a new "Check" tab to secret detail view.

https://github.com/nautobot/nautobot-plugin-secrets-providers/pull/62
Let’s upstream https://github.com/nautobot/nautobot-plugin-secrets-providers/pull/62

Resolving patch:
diff --git a/nautobot/extras/api/views.py b/nautobot/extras/api/views.py
--- a/nautobot/extras/api/views.py
+++ b/nautobot/extras/api/views.py
@@ -62,6 +62,7 @@
 )
 from nautobot.extras.models import CustomField, CustomFieldChoice
 from nautobot.extras.jobs import run_job
+from nautobot.extras.secrets.exceptions import SecretError
 from nautobot.extras.utils import get_job_content_type, get_worker_count
 from nautobot.utilities.exceptions import CeleryWorkerNotRunningException
 from nautobot.utilities.api import get_serializer_for_model
@@ -1140,6 +1141,31 @@ class SecretsViewSet(NautobotModelViewSet):
     serializer_class = serializers.SecretSerializer
     filterset_class = filters.SecretFilterSet
 
+    @extend_schema(
+        responses={
+            200: {
+                "type": "object",
+                "properties": {
+                    "result": {"type": "boolean"},
+                    "message": {"type": "string"},
+                },
+            }
+        },
+    )
+    @action(methods=["GET"], detail=True)
+    def check(self, request, pk):
+        """Check that a secret's value is accessible."""
+        result = False
+        message = "Unknown error"
+        try:
+            self.get_object().get_value()
+            result = True
+            message = "Passed"
+        except SecretError as e:
+            message = str(e)
+        response = {"result": result, "message": message}
+        return Response(response)
+
 
 class SecretsGroupViewSet(NautobotModelViewSet):
     """


Test patch:
diff --git a/nautobot/extras/tests/test_api.py b/nautobot/extras/tests/test_api.py
--- a/nautobot/extras/tests/test_api.py
+++ b/nautobot/extras/tests/test_api.py
@@ -1,5 +1,6 @@
 from datetime import datetime, timedelta
 import uuid
+import tempfile
 from unittest import mock
 
 from django.conf import settings
@@ -3318,6 +3319,47 @@ def setUpTestData(cls):
         for secret in secrets:
             secret.validated_save()
 
+    def test_secret_check(self):
+        """
+        Ensure that we can check the validity of a secret.
+        """
+
+        with self.subTest("Secret is not accessible"):
+            test_secret = Secret.objects.create(
+                name="secret-check-test-not-accessible",
+                provider="text-file",
+                parameters={"path": "/tmp/does-not-matter"},
+            )
+            response = self.client.get(reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header)
+            self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
+
+        self.add_permissions("extras.view_secret")
+
+        with self.subTest("Secret check successful"):
+            with tempfile.NamedTemporaryFile() as secret_file:
+                secret_file.write(b"HELLO WORLD")
+                test_secret = Secret.objects.create(
+                    name="secret-check-test-accessible",
+                    provider="text-file",
+                    parameters={"path": secret_file.name},
+                )
+                response = self.client.get(
+                    reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header
+                )
+                self.assertHttpStatus(response, status.HTTP_200_OK)
+                self.assertEqual(response.data["result"], True)
+
+        with self.subTest("Secret check failed"):
+            test_secret = Secret.objects.create(
+                name="secret-check-test-failed",
+                provider="text-file",
+                parameters={"path": "/tmp/does-not-exist"},
+            )
+            response = self.client.get(reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header)
+            self.assertHttpStatus(response, status.HTTP_200_OK)
+            self.assertEqual(response.data["result"], False)
+            self.assertIn("SecretValueNotFoundError", response.data["message"])
+
 
 class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
     model = SecretsGroup