Repository: nautobot/nautobot

Issue:
Sync Git Repository requires different permissions for UI vs API
### Environment
* Nautobot version (Docker tag too if applicable): v1.6.2b1
* Python version: 3.11
* Database platform, version: pg15
* Middleware(s): n/a

From the API it requires “add” and “change” permissions for “extras > Git repository” to sync git repositories. However from the UI a user with only the “change” permission can perform a sync git repository operation
### Steps to Reproduce
For UI
1. Create object permission for `extras > git repository`
2. Create one with "can change" permission item checked ("can view" as well, but not related to reproducing issue)
4. Assign to Group and assign that to User
5. You can perform sync from UI

For API
1. Create object permission for `extras > git repository`
2. Create one with "can change" permission item checked ("can view" as well, but not related to reproducing issue)
4. Assign to Group and assign that to User
5. You cannot perform sync from API
6. Edit permission for the group, adding "can add"
7. You can now perform the sync from the API

<!-- What did you expect to happen? -->
### Expected Behavior
Required permissions to be the same for action

<!-- What happened instead? -->
### Observed Behavior
Required permissions were not the same for action


Discussion:
API endpoint -> /api/extras/git-repositories/{id}/sync/
Per the code both views are only explicitly checking for `extras.change_gitrepository` permission. Likely the API is automatically checking for `add` permission because it's a POST endpoint which by default in DRF would be considered a `create` action. Thanks for the report!
We will set it up such that a user only needs the change permission to do the sync. They do not need the add permission.

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
@@ -375,7 +375,10 @@ class GitRepositoryViewSet(NautobotModelViewSet):
     filterset_class = filters.GitRepositoryFilterSet
 
     @extend_schema(methods=["post"], request=serializers.GitRepositorySerializer)
-    @action(detail=True, methods=["post"])
+    # Since we are explicitly checking for `extras:change_gitrepository` in the API sync() method
+    # We explicitly set the permission_classes to IsAuthenticated in the @action decorator
+    # bypassing the default DRF permission check for `extras:add_gitrepository` and the permission check fall through to the function itself.
+    @action(detail=True, methods=["post"], permission_classes=[IsAuthenticated])
     def sync(self, request, pk):
         """
         Enqueue pull git repository and refresh data.


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
@@ -984,7 +984,6 @@ def setUpTestData(cls):
     def test_run_git_sync_no_celery_worker(self, mock_get_worker_count):
         """Git sync cannot be triggered if Celery is not running."""
         mock_get_worker_count.return_value = 0
-        self.add_permissions("extras.add_gitrepository")
         self.add_permissions("extras.change_gitrepository")
         url = reverse("extras-api:gitrepository-sync", kwargs={"pk": self.repos[0].id})
         response = self.client.post(url, format="json", **self.header)
@@ -998,7 +997,6 @@ def test_run_git_sync_no_celery_worker(self, mock_get_worker_count):
     def test_run_git_sync_nonexistent_repo(self, mock_get_worker_count):
         """Git sync request handles case of a nonexistent repository."""
         mock_get_worker_count.return_value = 1
-        self.add_permissions("extras.add_gitrepository")
         self.add_permissions("extras.change_gitrepository")
         url = reverse("extras-api:gitrepository-sync", kwargs={"pk": "11111111-1111-1111-1111-111111111111"})
         response = self.client.post(url, format="json", **self.header)
@@ -1017,7 +1015,6 @@ def test_run_git_sync_without_permissions(self, mock_get_worker_count):
     @mock.patch("nautobot.extras.api.views.get_worker_count", return_value=1)
     def test_run_git_sync_with_permissions(self, _):
         """Git sync request can be submitted successfully."""
-        self.add_permissions("extras.add_gitrepository")
         self.add_permissions("extras.change_gitrepository")
         url = reverse("extras-api:gitrepository-sync", kwargs={"pk": self.repos[0].id})
         response = self.client.post(url, format="json", **self.header)