From 1990ac476e34a933b1f21deeec9ca46439e90465 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Thu, 4 Jun 2026 08:08:21 +0000 Subject: [PATCH 1/5] patchwork/api/check.py: better handle add checks permission Signed-off-by: Mauro Carvalho Chehab --- patchwork/api/check.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/patchwork/api/check.py b/patchwork/api/check.py index 74bbc19e0..79326a96a 100644 --- a/patchwork/api/check.py +++ b/patchwork/api/check.py @@ -108,9 +108,23 @@ class CheckListCreate(CheckMixin, ListCreateAPIView): lookup_url_kwarg = 'patch_id' ordering = 'id' + def is_editable(self, user): + if not user.is_authenticated: + return False + + # Only users with add_check permission can do it. + # Notice that this is a global permission: it allows + # adding checks to any project inside Patchwork. + if user.has_perm('patchwork.add_check'): + patch._edited_by = user + return True + + # Being maintainer doesn't grant rights to create checks. + return False + def create(self, request, patch_id, *args, **kwargs): p = get_object_or_404(Patch, id=patch_id) - if not p.is_editable(request.user): + if not self.is_editable(request.user): raise PermissionDenied() request.patch = p return super(CheckListCreate, self).create(request, *args, **kwargs) From 5ab5ddce1ce529c82f40db57fe2145870760ed32 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sun, 7 Jun 2026 08:41:47 +0200 Subject: [PATCH 2/5] add guardian to allow creating per-project permissions Signed-off-by: Mauro Carvalho Chehab --- patchwork/admin.py | 4 +++- patchwork/settings/base.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/patchwork/admin.py b/patchwork/admin.py index d3bdae1bb..49e981c8d 100644 --- a/patchwork/admin.py +++ b/patchwork/admin.py @@ -8,6 +8,8 @@ from django.contrib.auth.models import User from django.db.models import Prefetch +from guardian.admin import GuardedModelAdmin + from patchwork.models import Bundle from patchwork.models import Check from patchwork.models import Cover @@ -45,7 +47,7 @@ class DelegationRuleInline(admin.TabularInline): @admin.register(Project) -class ProjectAdmin(admin.ModelAdmin): +class ProjectAdmin(GuardedModelAdmin): list_display = ('name', 'linkname', 'listid', 'listemail') inlines = [ DelegationRuleInline, diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py index 2b557fc1c..6877fb7d1 100644 --- a/patchwork/settings/base.py +++ b/patchwork/settings/base.py @@ -22,6 +22,7 @@ 'django.contrib.sites', 'django.contrib.admin', 'django.contrib.staticfiles', + 'guardian', 'patchwork', ] @@ -71,6 +72,11 @@ }, ] +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', + 'guardian.backends.ObjectPermissionBackend', +] + FORM_RENDERER = 'patchwork.forms.PatchworkTableRenderer' # TODO(stephenfin): Consider changing to BigAutoField when we drop support for From 0736fc3aaef1ee81948726f6103bd5c53e20d0be Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sun, 7 Jun 2026 08:51:10 +0200 Subject: [PATCH 3/5] add a per-project rule to change CI checks Signed-off-by: Mauro Carvalho Chehab --- patchwork/api/check.py | 8 ++++++-- patchwork/models.py | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/patchwork/api/check.py b/patchwork/api/check.py index 79326a96a..e793aef9e 100644 --- a/patchwork/api/check.py +++ b/patchwork/api/check.py @@ -108,7 +108,7 @@ class CheckListCreate(CheckMixin, ListCreateAPIView): lookup_url_kwarg = 'patch_id' ordering = 'id' - def is_editable(self, user): + def is_editable(self, user, patch): if not user.is_authenticated: return False @@ -119,12 +119,16 @@ def is_editable(self, user): patch._edited_by = user return True + if user.has_perm('patchwork.add_check', patch.project): + patch._edited_by = user + return True + # Being maintainer doesn't grant rights to create checks. return False def create(self, request, patch_id, *args, **kwargs): p = get_object_or_404(Patch, id=patch_id) - if not self.is_editable(request.user): + if not self.is_editable(request.user, p): raise PermissionDenied() request.patch = p return super(CheckListCreate, self).create(request, *args, **kwargs) diff --git a/patchwork/models.py b/patchwork/models.py index d5cb31de9..e8d2349a7 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -122,6 +122,10 @@ def __str__(self): class Meta: unique_together = (('listid', 'subject_match'),) ordering = ['linkname'] + permissions = [ + # Per-project permission to add checks + ("add_check", "Can add checks"), + ] class DelegationRule(models.Model): From 054de5045a94e446553d6977de583ad977884b84 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Sun, 7 Jun 2026 13:07:08 +0200 Subject: [PATCH 4/5] add guardian requirements Signed-off-by: Mauro Carvalho Chehab --- requirements-dev.txt | 1 + requirements-prod.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index a1d0e03e7..b07b9b73c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,4 +3,5 @@ djangorestframework~=3.17.1 django-filter~=25.2.0 django-debug-toolbar~=6.3.0 django-dbbackup~=5.3.0 +django-guardian~=3.3.1 -r requirements-test.txt diff --git a/requirements-prod.txt b/requirements-prod.txt index 979731135..e5ed30606 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -1,5 +1,6 @@ Django~=6.0.0 djangorestframework~=3.17.1 django-filter~=25.2.0 +django-guardian~=3.3.1 psycopg~=3.3.4 sqlparse~=0.5.5 From e331e0fac9561e697ca5f343fbbb716ec9bd0061 Mon Sep 17 00:00:00 2001 From: Mauro Carvalho Chehab Date: Thu, 4 Jun 2026 08:13:35 +0000 Subject: [PATCH 5/5] patchwork/models.py: drop unused permissions Signed-off-by: Mauro Carvalho Chehab --- patchwork/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/patchwork/models.py b/patchwork/models.py index e8d2349a7..700476ce6 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -52,6 +52,7 @@ def __str__(self): return self.email class Meta: + default_permissions = () verbose_name_plural = 'People' @@ -120,6 +121,7 @@ def __str__(self): return self.name class Meta: + default_permissions = () unique_together = (('listid', 'subject_match'),) ordering = ['linkname'] permissions = [ @@ -149,6 +151,7 @@ def __str__(self): return self.path class Meta: + default_permissions = () ordering = ['-priority', 'path'] unique_together = ('path', 'project') @@ -257,6 +260,7 @@ def __str__(self): return self.name class Meta: + default_permissions = () ordering = ['ordering'] @@ -289,6 +293,7 @@ def __str__(self): return self.name class Meta: + default_permissions = () ordering = ['abbrev'] @@ -298,6 +303,7 @@ class PatchTag(models.Model): count = models.IntegerField(default=1) class Meta: + default_permissions = () unique_together = [('patch', 'tag')] @@ -419,6 +425,7 @@ def save(self, *args, **kwargs): super(EmailMixin, self).save(*args, **kwargs) class Meta: + default_permissions = () abstract = True @@ -461,6 +468,7 @@ def __str__(self): return self.name class Meta: + default_permissions = () abstract = True @@ -484,6 +492,7 @@ def get_mbox_url(self): ) class Meta: + default_permissions = () ordering = ['date'] unique_together = [('msgid', 'project')] indexes = [ @@ -721,6 +730,7 @@ def __str__(self): return self.name class Meta: + default_permissions = () verbose_name_plural = 'Patches' ordering = ['date'] base_manager_name = 'objects' @@ -784,6 +794,7 @@ def is_editable(self, user): return False class Meta: + default_permissions = () ordering = ['date'] unique_together = [('msgid', 'cover')] indexes = [ @@ -829,6 +840,7 @@ def is_editable(self, user): return self.patch.is_editable(user) class Meta: + default_permissions = () ordering = ['date'] unique_together = [('msgid', 'patch')] indexes = [ @@ -989,6 +1001,7 @@ def __str__(self): return self.name if self.name else 'Untitled series #%d' % self.id class Meta: + default_permissions = () verbose_name_plural = 'Series' @@ -1014,6 +1027,7 @@ def __str__(self): return self.msgid class Meta: + default_permissions = () unique_together = [('project', 'msgid')] @@ -1076,6 +1090,7 @@ def get_mbox_url(self): ) class Meta: + default_permissions = () unique_together = [('owner', 'name')] @@ -1085,6 +1100,7 @@ class BundlePatch(models.Model): order = models.IntegerField() class Meta: + default_permissions = () unique_together = [('bundle', 'patch')] ordering = ['order'] @@ -1154,6 +1170,9 @@ def __repr__(self): def __str__(self): return '%s (%s)' % (self.context, self.get_state_display()) + class Meta: + default_permissions = ('add',) + class Event(models.Model): """An event raised against a patch. @@ -1336,6 +1355,7 @@ def __repr__(self): return "