Repository: nautobot/nautobot

Issue:
Swagger documentation lists incorrect authorization header
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Python version: 3.10
* Nautobot version: 1.39

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Open the swagger API interface 
2. Attempt to use the included curl commands to execute API calls

<!-- What did you expect to happen? -->
### Expected Behavior
The listed authorzation header value should be 
Authorization: Token <VALUE>

<!-- What happened instead? -->
### Observed Behavior
THe header value is instead
Authorzation <VALUE>

![image](https://user-images.githubusercontent.com/776287/183323157-1359a238-3fde-4d08-89f7-8389b5ad9b00.png)



Discussion:
@achard and I looked into this a little. This issue occurs when you use Authorize on the tokenAuth with just the token and no `Token` prefix. Perhaps this should be renamed to Swagger Docs Token Authorization without `Token` prefix.
This is not easily fixable because it's not a part of the Swagger 3.0 API schema spec. See:

https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securitySchemeObject

Specifically:

> bearerFormat | string | http("bearer") | A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes.

As indicated here the bearer format style clearly indicates that this value is "primarily for documentation purposes".

So even if we were to manually override token authentication to return an "http.bearer" vs. "apiKey" authentication type, it still wouldn't work:

```json
 {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "Token",
    "description": "Token-based authentication with required prefix \"Token\""
}
```

This results in a slightly different UI experience:

<img width="406" alt="image" src="https://user-images.githubusercontent.com/138052/186213098-d0493f00-2d3c-4fc6-a69d-93bf86ca4c18.png">

However this does not address what gets emitted because of the OpenAPI spec adherence:

<img width="459" alt="image" src="https://user-images.githubusercontent.com/138052/186213218-67602a6a-06bf-4fda-b9a4-6764e5e6d8a6.png">

The name of the header is derived from the authentication method, however the argument that gets passed to the header is a function of the schema itself and it is **not possible** to prepend to this argument because it is literally coming through from the HTTP client.

So what we're left with is deferring to "apiKey" authentication type, which is what is currently used, and again, does not support prepending a header value.

We might be able to augment the front-end code to pre-pend this "Token" keyword to the header value, but that seems mighty invasive for something that is working as intended. I personally do not feel the juice is worth the squeeze strictly because "Token {key}" is intended to be the header value.



Resolving patch:
diff --git a/nautobot/core/api/views.py b/nautobot/core/api/views.py
--- a/nautobot/core/api/views.py
+++ b/nautobot/core/api/views.py
@@ -512,6 +512,9 @@ def get(self, request, *args, **kwargs):
         response = super().get(request, *args, **kwargs)
         response.data["swagger_settings"] = response.data["settings"]
         del response.data["settings"]
+
+        # Add additional data so drf-spectacular will use the Token keyword in authorization header.
+        response.data["schema_auth_names"] = ["tokenAuth"]
         return response
 
 
diff --git a/nautobot/core/views/mixins.py b/nautobot/core/views/mixins.py
--- a/nautobot/core/views/mixins.py
+++ b/nautobot/core/views/mixins.py
@@ -20,7 +20,7 @@
 from django.views.generic.edit import FormView
 
 from rest_framework import mixins, exceptions
-from rest_framework.parsers import MultiPartParser
+from rest_framework.parsers import FormParser, MultiPartParser
 from rest_framework.response import Response
 from rest_framework.viewsets import GenericViewSet
 
@@ -72,7 +72,7 @@ class NautobotViewSetMixin(GenericViewSet, AccessMixin, GetReturnURLMixin, FormV
     filterset_class = None
     filterset_form_class = None
     form_class = None
-    parser_classes = [MultiPartParser]
+    parser_classes = [FormParser, MultiPartParser]
     queryset = None
     # serializer_class has to be specified to eliminate the need to override retrieve() in the RetrieveModelMixin for now.
     serializer_class = None


Test patch:
diff --git a/nautobot/utilities/testing/views.py b/nautobot/utilities/testing/views.py
--- a/nautobot/utilities/testing/views.py
+++ b/nautobot/utilities/testing/views.py
@@ -9,6 +9,7 @@
 from django.test import TestCase as _TestCase, override_settings, tag
 from django.urls import reverse, NoReverseMatch
 from django.utils.text import slugify
+from django.utils.http import urlencode
 
 from nautobot.extras.choices import CustomFieldTypeChoices, RelationshipSideChoices, ObjectChangeActionChoices
 from nautobot.extras.models import ChangeLoggedModel, CustomField, Relationship
@@ -545,6 +546,35 @@ def test_delete_object_with_permission(self):
                 self.assertEqual(len(objectchanges), 1)
                 self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE)
 
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+        def test_delete_object_with_permission_and_xwwwformurlencoded(self):
+            instance = self.get_deletable_object()
+
+            # Assign model-level permission
+            obj_perm = ObjectPermission(name="Test permission", actions=["delete"])
+            obj_perm.save()
+            obj_perm.users.add(self.user)
+            obj_perm.object_types.add(ContentType.objects.get_for_model(self.model))
+
+            # Try GET with model-level permission
+            self.assertHttpStatus(self.client.get(self._get_url("delete", instance)), 200)
+
+            # Try POST with model-level permission
+            request = {
+                "path": self._get_url("delete", instance),
+                "data": urlencode({"confirm": True}),
+                "content_type": "application/x-www-form-urlencoded",
+            }
+            self.assertHttpStatus(self.client.post(**request), 302)
+            with self.assertRaises(ObjectDoesNotExist):
+                self._get_queryset().get(pk=instance.pk)
+
+            if hasattr(self.model, "to_objectchange"):
+                # Verify ObjectChange creation
+                objectchanges = get_changes_for_model(instance)
+                self.assertEqual(len(objectchanges), 1)
+                self.assertEqual(objectchanges[0].action, ObjectChangeActionChoices.ACTION_DELETE)
+
         @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
         def test_delete_object_with_constrained_permission(self):
             instance1 = self.get_deletable_object()