diff mbox series

[3/3] REST: Allow a project to restrict submitter state changes

Message ID 20210802152729.2110734-4-dja@axtens.net
State Changes Requested
Headers show
Series Allow a project to restrict submitter state changes | expand

Commit Message

Daniel Axtens Aug. 2, 2021, 3:27 p.m. UTC
As with xmlrpc and the UI.

Signed-off-by: Daniel Axtens <dja@axtens.net>
---
 patchwork/api/patch.py            | 10 +++++
 patchwork/tests/api/test_patch.py | 70 +++++++++++++++++++++++++++++++
 2 files changed, 80 insertions(+)

Comments

Stephen Finucane Aug. 3, 2021, 3:32 p.m. UTC | #1
On Tue, 2021-08-03 at 01:27 +1000, Daniel Axtens wrote:
> As with xmlrpc and the UI.
> 
> Signed-off-by: Daniel Axtens <dja@axtens.net>
> ---
>  patchwork/api/patch.py            | 10 +++++
>  patchwork/tests/api/test_patch.py | 70 +++++++++++++++++++++++++++++++
>  2 files changed, 80 insertions(+)
> 
> diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py
> index 9d222754412e..b8d0d5e17749 100644
> --- a/patchwork/api/patch.py
> +++ b/patchwork/api/patch.py
> @@ -122,6 +122,16 @@ class PatchListSerializer(BaseHyperlinkedModelSerializer):
>                                    "'%s'" % (value, self.instance.project))
>          return value
>  
> +    def validate_state(self, value):
> +        """Check that the users is authorised to set this state."""
> +        user = self.context.get('request').user
> +        if not self.instance.can_set_state(user):
> +            raise ValidationError(
> +                "User '%s' is not permitted to set state '%s' on this patch." %
> +                (user, value.name))
> +
> +        return value
> +

Oh, I think both this and the existing 'validate_delegate' are in the wrong
class. 'PatchListSerializer' is only used by the 'PatchList' view, which is
read-only. These should probably both be 'PatchDetailSerializer'? Assuming I've
groked things correctly, could you move 'validate_delegate' in a precursor
patch?

>      def to_representation(self, instance):
>          # NOTE(stephenfin): This is here to ensure our API looks the same even
>          # after we changed the series-patch relationship from M:N to 1:N. It
> diff --git a/patchwork/tests/api/test_patch.py b/patchwork/tests/api/test_patch.py
> index da2dd6e9084b..9de7b0d105f4 100644
> --- a/patchwork/tests/api/test_patch.py
> +++ b/patchwork/tests/api/test_patch.py
> @@ -11,6 +11,9 @@ from django.conf import settings
>  from django.urls import reverse
>  
>  from patchwork.models import Patch
> +from patchwork.models import Person
> +from patchwork.models import Project
> +from patchwork.models import State
>  from patchwork.tests.api import utils
>  from patchwork.tests.utils import create_maintainer
>  from patchwork.tests.utils import create_patch
> @@ -409,3 +412,70 @@ class TestPatchAPI(utils.APITestCase):
>          self.client.force_authenticate(user=user)
>          resp = self.client.delete(self.api_url(patch.id))
>          self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
> +
> +
> +@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
> +class TestPatchStateChecks(utils.APITestCase):
> +    fixtures = ['default_tags']
> +
> +    @staticmethod
> +    def api_url(item=None):
> +        kwargs = {'pk': item}
> +        return reverse('api-patch-detail', kwargs=kwargs)
> +
> +    def setUp(self):
> +        self.projects = {}
> +        self.maintainers = {}
> +        self.delegates = {}
> +        self.submitters = {}
> +        self.patches = {}
> +
> +        for project_type in (Project.SUBMITTER_NO_STATE_CHANGES,
> +                             Project.SUBMITTER_ALL_STATE_CHANGES):
> +            project = create_project(
> +                submitter_state_change_rules=project_type)
> +            self.projects[project_type] = project
> +            self.maintainers[project_type] = create_maintainer(project)
> +            submitter = create_user(project)
> +            self.submitters[project_type] = submitter
> +            delegate = create_user(project)
> +            self.delegates[project_type] = delegate
> +
> +            patch = create_patch(project=project,
> +                                 submitter=Person.objects.get(
> +                                     user=submitter),
> +                                 delegate=delegate)
> +            self.patches[project_type] = patch
> +
> +        create_state(name="New")
> +        create_state(name="RFC")
> +
> +    def can_set_state(self, patch, user):
> +        new_state = State.objects.get(name="New")
> +        rfc_state = State.objects.get(name="RFC")
> +        patch.state = new_state
> +        patch.save()
> +
> +        self.client.force_authenticate(user=user)
> +        resp = self.client.patch(self.api_url(patch.id),
> +                                 {'state': rfc_state.slug})
> +
> +        if resp.status_code != status.HTTP_200_OK:
> +            return False
> +
> +        self.assertEqual(Patch.objects.get(id=patch.id).state, rfc_state)
> +        return True
> +
> +    def test_allset(self):
> +        project = Project.SUBMITTER_ALL_STATE_CHANGES
> +        patch = self.patches[project]
> +        self.assertTrue(self.can_set_state(patch, self.maintainers[project]))
> +        self.assertTrue(self.can_set_state(patch, self.delegates[project]))
> +        self.assertTrue(self.can_set_state(patch, self.submitters[project]))
> +
> +    def test_noset(self):
> +        project = Project.SUBMITTER_NO_STATE_CHANGES
> +        patch = self.patches[project]
> +        self.assertTrue(self.can_set_state(patch, self.maintainers[project]))
> +        self.assertTrue(self.can_set_state(patch, self.delegates[project]))
> +        self.assertFalse(self.can_set_state(patch, self.submitters[project]))

Same question as the XML-RPC changes. I think a simple positive-negative test
would be more than sufficient here. Let's leave the full matrix to a simple unit
test of the model method.

Stephen
diff mbox series

Patch

diff --git a/patchwork/api/patch.py b/patchwork/api/patch.py
index 9d222754412e..b8d0d5e17749 100644
--- a/patchwork/api/patch.py
+++ b/patchwork/api/patch.py
@@ -122,6 +122,16 @@  class PatchListSerializer(BaseHyperlinkedModelSerializer):
                                   "'%s'" % (value, self.instance.project))
         return value
 
+    def validate_state(self, value):
+        """Check that the users is authorised to set this state."""
+        user = self.context.get('request').user
+        if not self.instance.can_set_state(user):
+            raise ValidationError(
+                "User '%s' is not permitted to set state '%s' on this patch." %
+                (user, value.name))
+
+        return value
+
     def to_representation(self, instance):
         # NOTE(stephenfin): This is here to ensure our API looks the same even
         # after we changed the series-patch relationship from M:N to 1:N. It
diff --git a/patchwork/tests/api/test_patch.py b/patchwork/tests/api/test_patch.py
index da2dd6e9084b..9de7b0d105f4 100644
--- a/patchwork/tests/api/test_patch.py
+++ b/patchwork/tests/api/test_patch.py
@@ -11,6 +11,9 @@  from django.conf import settings
 from django.urls import reverse
 
 from patchwork.models import Patch
+from patchwork.models import Person
+from patchwork.models import Project
+from patchwork.models import State
 from patchwork.tests.api import utils
 from patchwork.tests.utils import create_maintainer
 from patchwork.tests.utils import create_patch
@@ -409,3 +412,70 @@  class TestPatchAPI(utils.APITestCase):
         self.client.force_authenticate(user=user)
         resp = self.client.delete(self.api_url(patch.id))
         self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, resp.status_code)
+
+
+@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
+class TestPatchStateChecks(utils.APITestCase):
+    fixtures = ['default_tags']
+
+    @staticmethod
+    def api_url(item=None):
+        kwargs = {'pk': item}
+        return reverse('api-patch-detail', kwargs=kwargs)
+
+    def setUp(self):
+        self.projects = {}
+        self.maintainers = {}
+        self.delegates = {}
+        self.submitters = {}
+        self.patches = {}
+
+        for project_type in (Project.SUBMITTER_NO_STATE_CHANGES,
+                             Project.SUBMITTER_ALL_STATE_CHANGES):
+            project = create_project(
+                submitter_state_change_rules=project_type)
+            self.projects[project_type] = project
+            self.maintainers[project_type] = create_maintainer(project)
+            submitter = create_user(project)
+            self.submitters[project_type] = submitter
+            delegate = create_user(project)
+            self.delegates[project_type] = delegate
+
+            patch = create_patch(project=project,
+                                 submitter=Person.objects.get(
+                                     user=submitter),
+                                 delegate=delegate)
+            self.patches[project_type] = patch
+
+        create_state(name="New")
+        create_state(name="RFC")
+
+    def can_set_state(self, patch, user):
+        new_state = State.objects.get(name="New")
+        rfc_state = State.objects.get(name="RFC")
+        patch.state = new_state
+        patch.save()
+
+        self.client.force_authenticate(user=user)
+        resp = self.client.patch(self.api_url(patch.id),
+                                 {'state': rfc_state.slug})
+
+        if resp.status_code != status.HTTP_200_OK:
+            return False
+
+        self.assertEqual(Patch.objects.get(id=patch.id).state, rfc_state)
+        return True
+
+    def test_allset(self):
+        project = Project.SUBMITTER_ALL_STATE_CHANGES
+        patch = self.patches[project]
+        self.assertTrue(self.can_set_state(patch, self.maintainers[project]))
+        self.assertTrue(self.can_set_state(patch, self.delegates[project]))
+        self.assertTrue(self.can_set_state(patch, self.submitters[project]))
+
+    def test_noset(self):
+        project = Project.SUBMITTER_NO_STATE_CHANGES
+        patch = self.patches[project]
+        self.assertTrue(self.can_set_state(patch, self.maintainers[project]))
+        self.assertTrue(self.can_set_state(patch, self.delegates[project]))
+        self.assertFalse(self.can_set_state(patch, self.submitters[project]))