From a5e668fa8f4901655f7e504a6abf6cf99c83b094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Tue, 26 May 2026 19:42:45 +0200 Subject: [PATCH 1/3] fix(crm): require change permission for customer user invites --- .../templates/payments/customer_detail.html | 14 ++++---- weblate_web/crm/tests.py | 35 +++++++++++++++++++ weblate_web/crm/views.py | 9 +++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/weblate_web/crm/templates/payments/customer_detail.html b/weblate_web/crm/templates/payments/customer_detail.html index 86b25cdca..fefc27e33 100644 --- a/weblate_web/crm/templates/payments/customer_detail.html +++ b/weblate_web/crm/templates/payments/customer_detail.html @@ -52,12 +52,14 @@

Actions

-
-

{% translate "Add user to customer" %}

- {% csrf_token %} - {{ customer_user_form }} - -
+ {% if can_add_customer_user %} +
+

{% translate "Add user to customer" %}

+ {% csrf_token %} + {{ customer_user_form }} + +
+ {% endif %}

Services

{% include "weblate_web/service_list_content.html" with object_list=services %} diff --git a/weblate_web/crm/tests.py b/weblate_web/crm/tests.py index 5b375e5e9..05a0245a9 100644 --- a/weblate_web/crm/tests.py +++ b/weblate_web/crm/tests.py @@ -297,6 +297,41 @@ def test_customer_detail_renders_manual_note_form(self): self.assertContains(response, 'name="note"') self.assertContains(response, "Add note") + def test_customer_detail_hides_add_customer_user_for_readonly_staff(self): + customer = self.create_customer() + readonly_user = User.objects.create_user( + username="readonly", email="readonly@example.com", is_staff=True + ) + readonly_user.user_permissions.add( + Permission.objects.get(codename="view_customer") + ) + self.client.force_login(readonly_user) + + response = self.client.get(customer.get_absolute_url()) + + self.assertNotContains(response, 'name="add_customer_user"') + + def test_customer_detail_rejects_add_customer_user_for_readonly_staff(self): + customer = self.create_customer() + readonly_user = User.objects.create_user( + username="readonly", email="readonly@example.com", is_staff=True + ) + readonly_user.user_permissions.add( + Permission.objects.get(codename="view_customer") + ) + self.client.force_login(readonly_user) + + response = self.client.post( + customer.get_absolute_url(), + { + "add_customer_user": "1", + "email": "new@example.com", + "full_name": "New User", + }, + ) + + self.assertEqual(response.status_code, 403) + def test_customer_detail_adds_manual_note(self): customer = self.create_customer() note = ( diff --git a/weblate_web/crm/views.py b/weblate_web/crm/views.py index c699bec52..2e838558a 100644 --- a/weblate_web/crm/views.py +++ b/weblate_web/crm/views.py @@ -545,10 +545,18 @@ def get_context_data(self, **kwargs): context["customer_user_form"] = CustomerUserForm( self.request.POST if add_customer_user else None ) + context["can_add_customer_user"] = self.request.user.has_perm( + "payments.change_customer" + ) context["services"] = self.object.service_set.customer_services() context["donations"] = self.object.service_set.donations() return context + @staticmethod + def check_add_customer_user_permission(request) -> None: + if not request.user.has_perm("payments.change_customer"): + raise PermissionDenied + def get_title(self) -> str: return self.object.verbose_name @@ -642,6 +650,7 @@ def link_customer_hosted_user( return True def add_customer_user(self, request, customer: Customer, *args, **kwargs): + self.check_add_customer_user_permission(request) form = CustomerUserForm(request.POST) if not form.is_valid(): show_form_errors(request, form) From dca31b8a1ea0ac550fac338283293af467f52fe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Tue, 26 May 2026 19:52:15 +0200 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- weblate_web/crm/tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/weblate_web/crm/tests.py b/weblate_web/crm/tests.py index 05a0245a9..dfb5422d1 100644 --- a/weblate_web/crm/tests.py +++ b/weblate_web/crm/tests.py @@ -303,7 +303,11 @@ def test_customer_detail_hides_add_customer_user_for_readonly_staff(self): username="readonly", email="readonly@example.com", is_staff=True ) readonly_user.user_permissions.add( - Permission.objects.get(codename="view_customer") + Permission.objects.get( + codename="view_customer", + content_type__app_label="payments", + content_type__model="customer", + ) ) self.client.force_login(readonly_user) From cf6a67373dbd222bb454098f6cac4126b264e2bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Tue, 26 May 2026 19:52:15 +0200 Subject: [PATCH 3/3] Address review feedback --- weblate_web/crm/tests.py | 7 ++++++- weblate_web/crm/views.py | 11 +++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/weblate_web/crm/tests.py b/weblate_web/crm/tests.py index dfb5422d1..a8ed14044 100644 --- a/weblate_web/crm/tests.py +++ b/weblate_web/crm/tests.py @@ -313,6 +313,7 @@ def test_customer_detail_hides_add_customer_user_for_readonly_staff(self): response = self.client.get(customer.get_absolute_url()) + self.assertEqual(response.status_code, 200) self.assertNotContains(response, 'name="add_customer_user"') def test_customer_detail_rejects_add_customer_user_for_readonly_staff(self): @@ -321,7 +322,11 @@ def test_customer_detail_rejects_add_customer_user_for_readonly_staff(self): username="readonly", email="readonly@example.com", is_staff=True ) readonly_user.user_permissions.add( - Permission.objects.get(codename="view_customer") + Permission.objects.get( + codename="view_customer", + content_type__app_label="payments", + content_type__model="customer", + ) ) self.client.force_login(readonly_user) diff --git a/weblate_web/crm/views.py b/weblate_web/crm/views.py index 2e838558a..56cb01727 100644 --- a/weblate_web/crm/views.py +++ b/weblate_web/crm/views.py @@ -522,6 +522,7 @@ def get_queryset(self) -> CustomerQuerySet: class CustomerDetailView(CRMMixin, DetailView[Customer]): # type: ignore[misc] model = Customer permission = "payments.view_customer" + add_customer_user_permission = "payments.change_customer" title = "Customer detail" def get_context_data(self, **kwargs): @@ -546,15 +547,17 @@ def get_context_data(self, **kwargs): self.request.POST if add_customer_user else None ) context["can_add_customer_user"] = self.request.user.has_perm( - "payments.change_customer" + self.add_customer_user_permission ) context["services"] = self.object.service_set.customer_services() context["donations"] = self.object.service_set.donations() return context - @staticmethod - def check_add_customer_user_permission(request) -> None: - if not request.user.has_perm("payments.change_customer"): + @classmethod + def check_add_customer_user_permission( + cls, request: AuthenticatedHttpRequest + ) -> None: + if not request.user.has_perm(cls.add_customer_user_permission): raise PermissionDenied def get_title(self) -> str: