From patchwork Mon Aug 2 15:27:27 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Axtens X-Patchwork-Id: 1512496 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org (client-ip=112.213.38.117; helo=lists.ozlabs.org; envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org; receiver=) Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=Qp1GPZPw; dkim-atps=neutral Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4Gdhhh17Nsz9sRN for ; Tue, 3 Aug 2021 01:27:48 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gdhhh09Jtz30F9 for ; Tue, 3 Aug 2021 01:27:48 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=Qp1GPZPw; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=axtens.net (client-ip=2607:f8b0:4864:20::102d; helo=mail-pj1-x102d.google.com; envelope-from=dja@axtens.net; receiver=) Authentication-Results: lists.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=Qp1GPZPw; dkim-atps=neutral Received: from mail-pj1-x102d.google.com (mail-pj1-x102d.google.com [IPv6:2607:f8b0:4864:20::102d]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 4GdhhY5R8cz3029 for ; Tue, 3 Aug 2021 01:27:41 +1000 (AEST) Received: by mail-pj1-x102d.google.com with SMTP id dw2-20020a17090b0942b0290177cb475142so6359685pjb.2 for ; Mon, 02 Aug 2021 08:27:41 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axtens.net; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=yUylZbZBdkKwOZhHNFFoD5nZaKc7S6+nskr/t7sr0zs=; b=Qp1GPZPwB1wADElAJx6BNS7Z1Cso8oznZvj7qGzGDf1gVhd0hJ7rd/nUi4QU7HNUpH o+P7l0IoSGL/AVZ5+phZjZSR/lKG5B5yZ21BuHf+URQwBMCWHFj8lAXywigeIXEqg1Ce +bm8Aqdkd0O2Te2ZvSgQuQEL1Tw3q4dkA1Dh8= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=yUylZbZBdkKwOZhHNFFoD5nZaKc7S6+nskr/t7sr0zs=; b=cKWQTNlaeVhvzYi0zV9uv9teorN8OqzHzncN/6FkSeKqYm1bZ2zTFXWeYJQdpSxxt+ wKy1/LEsJzoVeqEC0/WIL4nvsZvRD9fVkOSvUAwpnh6SAX8FNVJONg+dYvmSb9h29PQf 5hVhaawrmzuIYwLvcpyrO1gqOHEedmgMOR3sMpqP+Z+7gOeRSURuRpT/t/jrNlpBVKtH fNotwSlJdOFTKtQkKzx/xAuHW+K7hALDre3uUmkPzqq8sLepItvB07gvaM531U5VO59t dhxbQwyT9SYacH2dazfVLbVkkLoL6zhTQZWXhuNy37WSSTkGR5tBTE38+Y+7AKLBqIsd PKfA== X-Gm-Message-State: AOAM530u9FAPAFtmc2jOgGn0UXXSCc7UAKp7LX4z8teidGJDjaI0I6ES LpfY1aTM2VUnICG4TSsJKNdX24aOH7MVwQ== X-Google-Smtp-Source: ABdhPJz3FpWdAmgMNGh7z5MjULBtvaEU4sqDKsQ8aNJ+S/X3nrw42/tzz5dwT2CpjCCCdhVS/mPrCw== X-Received: by 2002:a65:5186:: with SMTP id h6mr2642047pgq.62.1627918058921; Mon, 02 Aug 2021 08:27:38 -0700 (PDT) Received: from localhost (ip-145-57.yless4u.com.au. [103.22.145.57]) by smtp.gmail.com with ESMTPSA id k193sm12921016pfd.128.2021.08.02.08.27.37 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 02 Aug 2021 08:27:38 -0700 (PDT) From: Daniel Axtens To: patchwork@lists.ozlabs.org Subject: [PATCH 1/3] Allow a project to restrict submitter state changes Date: Tue, 3 Aug 2021 01:27:27 +1000 Message-Id: <20210802152729.2110734-2-dja@axtens.net> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20210802152729.2110734-1-dja@axtens.net> References: <20210802152729.2110734-1-dja@axtens.net> MIME-Version: 1.0 X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" In discussions about how to make patchwork more user-friendly and suitable for more projects, we realised that perhaps the current ability for submitters to change their patch state to any value isn't the most appropriate setting for all maintainers, especially in light of increasing automation. Allow a project to stop a submitter from changing the state of their patches. This is not the default but can be set by a patchwork administrator. Signed-off-by: Daniel Axtens --- ...45_project_submitter_state_change_rules.py | 24 +++++++++++++ patchwork/models.py | 36 +++++++++++++++++++ patchwork/views/__init__.py | 8 +++++ patchwork/views/patch.py | 14 ++++++-- 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 patchwork/migrations/0045_project_submitter_state_change_rules.py diff --git a/patchwork/migrations/0045_project_submitter_state_change_rules.py b/patchwork/migrations/0045_project_submitter_state_change_rules.py new file mode 100644 index 000000000000..9d0b2892bd5c --- /dev/null +++ b/patchwork/migrations/0045_project_submitter_state_change_rules.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.12 on 2021-08-03 00:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('patchwork', '0044_add_project_linkname_validation'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='submitter_state_change_rules', + field=models.SmallIntegerField( + choices=[ + (0, 'Submitters may not change patch states'), + (1, 'Submitters may set any patch state')], + default=1, + help_text='What state changes can patch submitters make?' + ' Does not affect maintainers.'), + ), + ] diff --git a/patchwork/models.py b/patchwork/models.py index 00273da9f5bd..706b912c349a 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -93,6 +93,19 @@ class Project(models.Model): send_notifications = models.BooleanField(default=False) use_tags = models.BooleanField(default=True) + # how much can a patch submitter change? + SUBMITTER_NO_STATE_CHANGES = 0 + SUBMITTER_ALL_STATE_CHANGES = 1 + SUBMITTER_STATE_CHANGE_CHOICES = ( + (SUBMITTER_NO_STATE_CHANGES, 'Submitters may not change patch states'), + (SUBMITTER_ALL_STATE_CHANGES, 'Submitters may set any patch state'), + ) + submitter_state_change_rules = models.SmallIntegerField( + choices=SUBMITTER_STATE_CHANGE_CHOICES, + default=SUBMITTER_ALL_STATE_CHANGES, + help_text='What state changes can patch submitters make?' + ' Does not affect maintainers.') + def is_editable(self, user): if not user.is_authenticated: return False @@ -518,6 +531,29 @@ class Patch(SubmissionMixin): return True return False + def can_set_state(self, user): + # an unauthenticated user can never change state + if not user.is_authenticated: + return False + + # a maintainer can always set state + if self.project.is_editable(user): + self._edited_by = user + return True + + # a delegate can always set state + if user == self.delegate: + self._edited_by = user + return True + + # if the state change rules prohibit it, no other user can set change + if (self.project.submitter_state_change_rules == + Project.SUBMITTER_NO_STATE_CHANGES): + return False + + # otherwise, a submitter can change state + return self.is_editable(user) + @staticmethod def filter_unique_checks(checks): """Filter the provided checks to generate the unique list.""" diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py index 3efe90cd6929..9f5d316d18b5 100644 --- a/patchwork/views/__init__.py +++ b/patchwork/views/__init__.py @@ -312,6 +312,14 @@ def process_multiplepatch_form(request, form, action, patches, context): % patch.name) continue + field = form.fields.get('state', None) + if field and not field.is_no_change(form.cleaned_data['state']) \ + and not patch.can_set_state(request.user): + errors.append( + "You don't have the permissions to set the state of patch '%s'" + % patch.name) + continue + changed_patches += 1 form.save(patch) diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py index 3e6874ae1e5d..72c199135cbb 100644 --- a/patchwork/views/patch.py +++ b/patchwork/views/patch.py @@ -101,8 +101,18 @@ def patch_detail(request, project_id, msgid): elif action is None: form = PatchForm(data=request.POST, instance=patch) if form.is_valid(): - form.save() - messages.success(request, 'Patch updated') + old_patch = Patch.objects.get(id=patch.id) + if old_patch.state != form.cleaned_data['state'] and \ + not old_patch.can_set_state(request.user): + messages.error( + request, + "You don't have the permissions to set the state of " + "patch '%s'" % patch.name) + patch = old_patch + form = PatchForm(instance=patch) + else: + form.save() + messages.success(request, 'Patch updated') if request.user.is_authenticated: context['bundles'] = request.user.bundles.all() From patchwork Mon Aug 2 15:27:28 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Axtens X-Patchwork-Id: 1512497 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org; envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org; receiver=) Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=rZ/KrUcJ; dkim-atps=neutral Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4Gdhhm2kjsz9sRN for ; Tue, 3 Aug 2021 01:27:52 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gdhhm1dZ0z3bXm for ; Tue, 3 Aug 2021 01:27:52 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=rZ/KrUcJ; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=axtens.net (client-ip=2607:f8b0:4864:20::631; helo=mail-pl1-x631.google.com; envelope-from=dja@axtens.net; receiver=) Authentication-Results: lists.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=rZ/KrUcJ; dkim-atps=neutral Received: from mail-pl1-x631.google.com (mail-pl1-x631.google.com [IPv6:2607:f8b0:4864:20::631]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 4Gdhhf3l0xz30NX for ; Tue, 3 Aug 2021 01:27:46 +1000 (AEST) Received: by mail-pl1-x631.google.com with SMTP id c16so20012332plh.7 for ; Mon, 02 Aug 2021 08:27:46 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axtens.net; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=FyMxBF8abrZxfPmc7o+Zo2QOuJRrYEQs4mEjim78oPA=; b=rZ/KrUcJTNphsgGJimDsFlA4drBQYuew3iW9X7wPH4pXsR7VkF4WU5gEEzOin635k6 zaTk+PUwxBg4kF2J4aJWJXFMc3ScS+Xlo14taBQOCmc6gef0Gr0aoLx8j5MizjysMK5V fCNeidRVqCO0U6vcudxq2sXVprTPUhXcbThdc= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=FyMxBF8abrZxfPmc7o+Zo2QOuJRrYEQs4mEjim78oPA=; b=riGFf7dFo8IcXtL2cxA6vHMn5LIxKSTkkdxBSVgutaAUy5pkN0v7+7oU/46023b2c/ VARyXLKYZV8V/7gzTxVnvBRVhV5Gq4iwIPObFFNs9TRYe79FPFrb9IX3nNskAqNo0+b4 rWMC04b3S/QIXON/3pdxrB1+5fydJS4LrRNG4Hv5kM5UEosoqtvEX+T68nBIVC0Tty2+ VKuCEVdBjMixTujUBvxX3konDrLa6r1xFuIKoy10t5zg9ksDerZ17hORTxzzvKZ9cE8s alpt+mH+l2IGjDWPxKV9am2Df0r/vCtfE8e1M3kGpSeEA/sh5AF0cWMdxlrwj/YrkETr QgmQ== X-Gm-Message-State: AOAM5304R6DlF2LP7OF6QRrMKShXW/M7WtLfIdNF4d6GjPW/svIi5hIp qqLuafD9WtsNCkGS19vWLtESO6LnKw/DXg== X-Google-Smtp-Source: ABdhPJxZv+yLYPMRTNYavtajQcl6TOAvb1W2swrwGNq+jLDDk0h+cCT9JnQ9x3oiObvO/aDQrQxcjw== X-Received: by 2002:a17:90a:ca93:: with SMTP id y19mr18450101pjt.142.1627918063880; Mon, 02 Aug 2021 08:27:43 -0700 (PDT) Received: from localhost (ip-145-57.yless4u.com.au. [103.22.145.57]) by smtp.gmail.com with ESMTPSA id j185sm12415058pfb.86.2021.08.02.08.27.43 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 02 Aug 2021 08:27:43 -0700 (PDT) From: Daniel Axtens To: patchwork@lists.ozlabs.org Subject: [PATCH 2/3] xmlrpc: Allow a project to restrict submitter state changes Date: Tue, 3 Aug 2021 01:27:28 +1000 Message-Id: <20210802152729.2110734-3-dja@axtens.net> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20210802152729.2110734-1-dja@axtens.net> References: <20210802152729.2110734-1-dja@axtens.net> MIME-Version: 1.0 X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" As with the UI. Signed-off-by: Daniel Axtens --- patchwork/tests/test_xmlrpc.py | 90 ++++++++++++++++++++++++++++++++++ patchwork/views/xmlrpc.py | 4 ++ 2 files changed, 94 insertions(+) diff --git a/patchwork/tests/test_xmlrpc.py b/patchwork/tests/test_xmlrpc.py index 4726fdffa5d5..eea0b4eaf560 100644 --- a/patchwork/tests/test_xmlrpc.py +++ b/patchwork/tests/test_xmlrpc.py @@ -10,6 +10,9 @@ from django.conf import settings from django.test import LiveServerTestCase from django.urls import reverse +from patchwork.models import Person +from patchwork.models import Project +from patchwork.models import State from patchwork.tests import utils @@ -81,6 +84,93 @@ class XMLRPCAuthenticatedTest(LiveServerTestCase): self.assertTrue(result['archived']) +@unittest.skipUnless(settings.ENABLE_XMLRPC, + 'requires xmlrpc interface (use the ENABLE_XMLRPC ' + 'setting)') +class XMLRPCStateSettingTest(LiveServerTestCase): + + def url_for_user(self, user): + return ('http://%s:%s@' + self.url[7:]) % \ + (user.username, user.username) + + def setUp(self): + self.url = self.live_server_url + reverse('xmlrpc') + # url is of the form http://localhost:PORT/PATH + # strip the http and replace it with the username/passwd of a user. + self.projects = {} + self.maintainers = {} + self.delegates = {} + self.submitters = {} + self.patches = {} + self.rpcs = {} + + for project_type in (Project.SUBMITTER_NO_STATE_CHANGES, + Project.SUBMITTER_ALL_STATE_CHANGES): + project = utils.create_project( + submitter_state_change_rules=project_type) + self.projects[project_type] = project + self.maintainers[project_type] = utils.create_maintainer(project) + submitter = utils.create_user(project) + self.submitters[project_type] = submitter + delegate = utils.create_user(project) + self.delegates[project_type] = delegate + + self.rpcs[project_type] = { + 'maintainer': ServerProxy(self.url_for_user( + self.maintainers[project_type])), + 'delegate': ServerProxy(self.url_for_user(delegate)), + 'submitter': ServerProxy(self.url_for_user(submitter)), + } + + patch = utils.create_patch(project=project, + submitter=Person.objects.get( + user=submitter), + delegate=delegate) + self.patches[project_type] = patch + + utils.create_state(name="New") + utils.create_state(name="RFC") + + def tearDown(self): + for project_type in self.rpcs: + rpc_dict = self.rpcs[project_type] + for user in rpc_dict: + rpc_dict[user].close() + + def can_set_state(self, patch, rpc): + new_state = State.objects.get(name="New") + rfc_state = State.objects.get(name="RFC") + patch.state = new_state + patch.save() + + result = rpc.patch_get(patch.id) + self.assertEqual(result['state_id'], new_state.id) + + try: + rpc.patch_set(patch.id, {'state': rfc_state.id}) + except xmlrpc_client.Fault: + return False + + # reload the patch + result = rpc.patch_get(patch.id) + self.assertEqual(result['state_id'], rfc_state.id) + return True + + def test_allset(self): + rpc_dict = self.rpcs[Project.SUBMITTER_ALL_STATE_CHANGES] + patch = self.patches[Project.SUBMITTER_ALL_STATE_CHANGES] + self.assertTrue(self.can_set_state(patch, rpc_dict['maintainer'])) + self.assertTrue(self.can_set_state(patch, rpc_dict['delegate'])) + self.assertTrue(self.can_set_state(patch, rpc_dict['submitter'])) + + def test_noset(self): + rpc_dict = self.rpcs[Project.SUBMITTER_NO_STATE_CHANGES] + patch = self.patches[Project.SUBMITTER_NO_STATE_CHANGES] + self.assertTrue(self.can_set_state(patch, rpc_dict['maintainer'])) + self.assertTrue(self.can_set_state(patch, rpc_dict['delegate'])) + self.assertFalse(self.can_set_state(patch, rpc_dict['submitter'])) + + class XMLRPCModelTestMixin(object): def create_multiple(self, count): diff --git a/patchwork/views/xmlrpc.py b/patchwork/views/xmlrpc.py index 6701bf20f386..d73cfa7a8441 100644 --- a/patchwork/views/xmlrpc.py +++ b/patchwork/views/xmlrpc.py @@ -713,6 +713,10 @@ def patch_set(user, patch_id, params): if not patch.is_editable(user): raise Exception('No permissions to edit this patch') + if 'state' in params: + if not patch.can_set_state(user): + raise Exception('No permissions to set patch state') + for (k, v) in params.items(): if k not in ok_params: continue From patchwork Mon Aug 2 15:27:29 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Daniel Axtens X-Patchwork-Id: 1512498 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org (client-ip=112.213.38.117; helo=lists.ozlabs.org; envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org; receiver=) Authentication-Results: ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=pxVncD7P; dkim-atps=neutral Received: from lists.ozlabs.org (lists.ozlabs.org [112.213.38.117]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 4Gdhhv5ZZSz9sRN for ; Tue, 3 Aug 2021 01:27:59 +1000 (AEST) Received: from boromir.ozlabs.org (localhost [IPv6:::1]) by lists.ozlabs.org (Postfix) with ESMTP id 4Gdhhv4ch2z30F1 for ; Tue, 3 Aug 2021 01:27:59 +1000 (AEST) Authentication-Results: lists.ozlabs.org; dkim=fail reason="signature verification failed" (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=pxVncD7P; dkim-atps=neutral X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Authentication-Results: lists.ozlabs.org; spf=pass (sender SPF authorized) smtp.mailfrom=axtens.net (client-ip=2607:f8b0:4864:20::1031; helo=mail-pj1-x1031.google.com; envelope-from=dja@axtens.net; receiver=) Authentication-Results: lists.ozlabs.org; dkim=pass (1024-bit key; unprotected) header.d=axtens.net header.i=@axtens.net header.a=rsa-sha256 header.s=google header.b=pxVncD7P; dkim-atps=neutral Received: from mail-pj1-x1031.google.com (mail-pj1-x1031.google.com [IPv6:2607:f8b0:4864:20::1031]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.ozlabs.org (Postfix) with ESMTPS id 4Gdhhq06K5z3bXH for ; Tue, 3 Aug 2021 01:27:54 +1000 (AEST) Received: by mail-pj1-x1031.google.com with SMTP id s22-20020a17090a1c16b0290177caeba067so6447796pjs.0 for ; Mon, 02 Aug 2021 08:27:54 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axtens.net; s=google; h=from:to:cc:subject:date:message-id:in-reply-to:references :mime-version:content-transfer-encoding; bh=qFyMrJ/B1uPnlCGnC9TDoyOAZgRd6ucHRECxLdhd1R8=; b=pxVncD7PypNM4K4z3p2cBwQa+MTlAMck1jJXtuz5s0vke0h9C1avxdr4kvs1i602TU OsIo7nomX05jCmmNxV9Q5ioLVPry3IMR2IyFTZaUtT6l7IIGFZMNFtdlGr2HRK5Db2UT jbqAOyRkg9a1kJZQp461/SdOYRbFWj0iJtNqI= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id:in-reply-to :references:mime-version:content-transfer-encoding; bh=qFyMrJ/B1uPnlCGnC9TDoyOAZgRd6ucHRECxLdhd1R8=; b=lVpsPUEZSWNo/GjoQxzeridlLbtVUEMDkLt7C7YwFWqPIbckNc7GFPWlzqM2YtJSTq +2Yh3+sleuc1VzCDLOydL2qQOnbieoUpirHVm7ODuF36be7pIT6oflmsaJjMQoBkW3zs AL8iFdmqGliR4VmKUq2KiQ+DquG3FQ8dTTWDDLfG/cR7icWp9fERFmTUignZlCORhiDQ xzC65n/6OfKIC88jSi8ZmOWz8rFSJ3xc0c8OwXeqlyrhvGkI1fsecmFTeDSsDrZZYXPj 1szAj9pkI1hHeEkMheScpCV4HNZ2V9cNJ9dIZf2mXRe7sI6uuzFKsCy0gl2bkDF8uo9N pUhA== X-Gm-Message-State: AOAM530sQ73+YK8vHiAZTSOnHA77ZymLjLccr7EhBaEMYypcX26IUw6T 2XBobvQbGpIK/V7xz/LFCePaXZvcayPi0g== X-Google-Smtp-Source: ABdhPJzgrBGYtARjM8Wg+q8m84dx47amm/0wp7lnE8Q2t5CpRtpKaAhYyZAK02Cb/xq85O/eywVnWw== X-Received: by 2002:a17:90a:4e4e:: with SMTP id t14mr17967921pjl.8.1627918072072; Mon, 02 Aug 2021 08:27:52 -0700 (PDT) Received: from localhost (ip-145-57.yless4u.com.au. [103.22.145.57]) by smtp.gmail.com with ESMTPSA id w2sm10990742pjt.14.2021.08.02.08.27.51 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 02 Aug 2021 08:27:51 -0700 (PDT) From: Daniel Axtens To: patchwork@lists.ozlabs.org Subject: [PATCH 3/3] REST: Allow a project to restrict submitter state changes Date: Tue, 3 Aug 2021 01:27:29 +1000 Message-Id: <20210802152729.2110734-4-dja@axtens.net> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20210802152729.2110734-1-dja@axtens.net> References: <20210802152729.2110734-1-dja@axtens.net> MIME-Version: 1.0 X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" As with xmlrpc and the UI. Signed-off-by: Daniel Axtens --- 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 + 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]))