From 39198c827a1ef48ce5a024d2948a013bd17a559b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Wed, 26 Aug 2020 14:48:31 +0300 Subject: [PATCH] Read-only users cannot edit or remove channels. --- hc/front/tests/test_channels.py | 13 ++++++++ hc/front/tests/test_edit_webhook.py | 8 +++++ hc/front/tests/test_remove_channel.py | 25 +++++++------- hc/front/tests/test_update_channel.py | 10 ++++++ hc/front/tests/test_update_channel_name.py | 10 ++++++ hc/front/views.py | 38 ++++++++++++++-------- static/css/channels.css | 6 ++-- static/js/channels.js | 2 +- templates/front/channels.html | 10 ++++-- 9 files changed, 92 insertions(+), 30 deletions(-) diff --git a/hc/front/tests/test_channels.py b/hc/front/tests/test_channels.py index 6953f90b..c0613df3 100644 --- a/hc/front/tests/test_channels.py +++ b/hc/front/tests/test_channels.py @@ -108,3 +108,16 @@ class ChannelsTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.get(self.channels_url) self.assertContains(r, "broken-channels", status_code=200) + + def test_it_hides_actions_from_readonly_users(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + Channel.objects.create(project=self.project, kind="webhook", value="{}") + + self.client.login(username="bob@example.org", password="password") + r = self.client.get(self.channels_url) + + self.assertNotContains(r, "Add Integration", status_code=200) + self.assertNotContains(r, "icon-delete") + self.assertNotContains(r, "edit_webhook") diff --git a/hc/front/tests/test_edit_webhook.py b/hc/front/tests/test_edit_webhook.py index f04720b2..9e0886c8 100644 --- a/hc/front/tests/test_edit_webhook.py +++ b/hc/front/tests/test_edit_webhook.py @@ -82,3 +82,11 @@ class EditWebhookTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.get(self.url) self.assertEqual(r.status_code, 400) + + def test_it_requires_rw_access(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + self.client.login(username="bob@example.org", password="password") + r = self.client.post(self.url, {}) + self.assertEqual(r.status_code, 403) diff --git a/hc/front/tests/test_remove_channel.py b/hc/front/tests/test_remove_channel.py index 1f02b7ab..9db776f6 100644 --- a/hc/front/tests/test_remove_channel.py +++ b/hc/front/tests/test_remove_channel.py @@ -9,20 +9,18 @@ class RemoveChannelTestCase(BaseTestCase): self.channel.value = "alice@example.org" self.channel.save() - def test_it_works(self): - url = "/integrations/%s/remove/" % self.channel.code + self.url = "/integrations/%s/remove/" % self.channel.code + def test_it_works(self): self.client.login(username="alice@example.org", password="password") - r = self.client.post(url) + r = self.client.post(self.url) self.assertRedirects(r, self.channels_url) assert Channel.objects.count() == 0 def test_team_access_works(self): - url = "/integrations/%s/remove/" % self.channel.code - self.client.login(username="bob@example.org", password="password") - self.client.post(url) + self.client.post(self.url) assert Channel.objects.count() == 0 def test_it_handles_bad_uuid(self): @@ -33,10 +31,8 @@ class RemoveChannelTestCase(BaseTestCase): self.assertEqual(r.status_code, 404) def test_it_checks_owner(self): - url = "/integrations/%s/remove/" % self.channel.code - self.client.login(username="charlie@example.org", password="password") - r = self.client.post(url) + r = self.client.post(self.url) self.assertEqual(r.status_code, 404) def test_it_handles_missing_uuid(self): @@ -48,7 +44,14 @@ class RemoveChannelTestCase(BaseTestCase): self.assertEqual(r.status_code, 404) def test_it_rejects_get(self): - url = "/integrations/%s/remove/" % self.channel.code self.client.login(username="alice@example.org", password="password") - r = self.client.get(url) + r = self.client.get(self.url) self.assertEqual(r.status_code, 405) + + def test_it_requires_rw_access(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + self.client.login(username="bob@example.org", password="password") + r = self.client.post(self.url) + self.assertEqual(r.status_code, 403) diff --git a/hc/front/tests/test_update_channel.py b/hc/front/tests/test_update_channel.py index a45ea66d..95f8162b 100644 --- a/hc/front/tests/test_update_channel.py +++ b/hc/front/tests/test_update_channel.py @@ -70,3 +70,13 @@ class UpdateChannelTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.post(self.channels_url, data=payload) self.assertEqual(r.status_code, 400) + + def test_it_requires_rw_access(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + payload = {"channel": self.channel.code} + + self.client.login(username="bob@example.org", password="password") + r = self.client.post(self.channels_url, data=payload) + self.assertEqual(r.status_code, 403) diff --git a/hc/front/tests/test_update_channel_name.py b/hc/front/tests/test_update_channel_name.py index 4d6c92ff..2c1a20fc 100644 --- a/hc/front/tests/test_update_channel_name.py +++ b/hc/front/tests/test_update_channel_name.py @@ -51,3 +51,13 @@ class UpdateChannelNameTestCase(BaseTestCase): self.client.login(username="alice@example.org", password="password") r = self.client.get(self.url) self.assertEqual(r.status_code, 405) + + def test_it_requires_rw_access(self): + self.bobs_membership.rw = False + self.bobs_membership.save() + + payload = {"name": "My work email"} + + self.client.login(username="bob@example.org", password="password") + r = self.client.post(self.url, data=payload) + self.assertEqual(r.status_code, 403) diff --git a/hc/front/views.py b/hc/front/views.py index f527358a..a5dc81fd 100644 --- a/hc/front/views.py +++ b/hc/front/views.py @@ -110,15 +110,23 @@ def _get_channel_for_user(request, code): assert request.user.is_authenticated - q = Channel.objects - if not request.user.is_superuser: - project_ids = request.profile.projects().values("id") - q = q.filter(project_id__in=project_ids) + channel = get_object_or_404(Channel.objects.select_related("project"), code=code) + if request.user.is_superuser: + return channel, True - try: - return q.get(code=code) - except Channel.DoesNotExist: - raise Http404("not found") + if request.user.id == channel.project.owner_id: + return channel, True + + membership = get_object_or_404(Member, project=channel.project, user=request.user) + return channel, membership.rw + + +def _get_rw_channel_for_user(request, code): + channel, rw = _get_channel_for_user(request, code) + if not rw: + raise PermissionDenied + + return channel def _get_project_for_user(request, project_code): @@ -693,6 +701,9 @@ def channels(request, code): project, rw = _get_project_for_user(request, code) if request.method == "POST": + if not rw: + return HttpResponseForbidden() + code = request.POST["channel"] try: channel = Channel.objects.get(code=code) @@ -722,6 +733,7 @@ def channels(request, code): ctx = { "page": "channels", + "rw": rw, "project": project, "profile": project.owner_profile, "channels": channels, @@ -746,7 +758,7 @@ def channels(request, code): @login_required def channel_checks(request, code): - channel = _get_channel_for_user(request, code) + channel = _get_rw_channel_for_user(request, code) assigned = set(channel.checks.values_list("code", flat=True).distinct()) checks = Check.objects.filter(project=channel.project).order_by("created") @@ -759,7 +771,7 @@ def channel_checks(request, code): @require_POST @login_required def update_channel_name(request, code): - channel = _get_channel_for_user(request, code) + channel = _get_rw_channel_for_user(request, code) form = forms.ChannelNameForm(request.POST) if form.is_valid(): @@ -817,7 +829,7 @@ def unsubscribe_email(request, code, signed_token): @require_POST @login_required def send_test_notification(request, code): - channel = _get_channel_for_user(request, code) + channel, rw = _get_channel_for_user(request, code) dummy = Check(name="TEST", status="down") dummy.last_ping = timezone.now() - td(days=1) @@ -846,7 +858,7 @@ def send_test_notification(request, code): @require_POST @login_required def remove_channel(request, code): - channel = _get_channel_for_user(request, code) + channel = _get_rw_channel_for_user(request, code) project = channel.project channel.delete() @@ -926,7 +938,7 @@ def add_webhook(request, code): @login_required def edit_webhook(request, code): - channel = _get_channel_for_user(request, code) + channel = _get_rw_channel_for_user(request, code) if channel.kind != "webhook": return HttpResponseBadRequest() diff --git a/static/css/channels.css b/static/css/channels.css index f6c7dd06..94d40524 100644 --- a/static/css/channels.css +++ b/static/css/channels.css @@ -54,7 +54,6 @@ table.channels-table > tbody > tr > th { .edit-name, .edit-checks { padding: 12px 6px; border: 1px solid transparent; - cursor: pointer; } .edit-name .channel-details-mini { @@ -70,9 +69,10 @@ table.channels-table > tbody > tr > th { color: #111; } -.channel-row:hover .edit-name, -.channel-row:hover .edit-checks { +.rw .channel-row:hover .edit-name, +.rw .channel-row:hover .edit-checks { border: 1px dotted #AAA; + cursor: pointer; } .edit-checks { diff --git a/static/js/channels.js b/static/js/channels.js index 90110bca..f2bb8542 100644 --- a/static/js/channels.js +++ b/static/js/channels.js @@ -1,6 +1,6 @@ $(function() { - $(".edit-checks").click(function() { + $(".rw .edit-checks").click(function() { $("#checks-modal").modal("show"); $.ajax(this.dataset.url).done(function(data) { $("#checks-modal .modal-content").html(data); diff --git a/templates/front/channels.html b/templates/front/channels.html index 62a3bff1..17f56dd0 100644 --- a/templates/front/channels.html +++ b/templates/front/channels.html @@ -16,7 +16,7 @@
{% if channels %} - +
@@ -132,7 +132,7 @@ {% endif %}
Name, Details - {% if ch.kind == "webhook" %} + {% if ch.kind == "webhook" and rw %} Edit {% endif %}
@@ -145,6 +145,7 @@ Test!
+ {% if rw %} + {% endif %}
@@ -173,6 +175,7 @@ {% endif %} + {% if rw %}

Add More

  • @@ -431,6 +434,7 @@
+ {% endif %} @@ -472,6 +476,7 @@ +{% if rw %} {% for ch in channels %} {% endfor %} +{% endif %} {% endblock %}