mirror of
https://github.com/healthchecks/healthchecks.git
synced 2025-04-17 18:22:33 +00:00
Read-only users cannot edit or remove channels.
This commit is contained in:
parent
24c34430ac
commit
39198c827a
9 changed files with 92 additions and 30 deletions
hc/front
tests
test_channels.pytest_edit_webhook.pytest_remove_channel.pytest_update_channel.pytest_update_channel_name.py
views.pystatic
templates/front
|
@ -108,3 +108,16 @@ class ChannelsTestCase(BaseTestCase):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.channels_url)
|
r = self.client.get(self.channels_url)
|
||||||
self.assertContains(r, "broken-channels", status_code=200)
|
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")
|
||||||
|
|
|
@ -82,3 +82,11 @@ class EditWebhookTestCase(BaseTestCase):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.url)
|
r = self.client.get(self.url)
|
||||||
self.assertEqual(r.status_code, 400)
|
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)
|
||||||
|
|
|
@ -9,20 +9,18 @@ class RemoveChannelTestCase(BaseTestCase):
|
||||||
self.channel.value = "alice@example.org"
|
self.channel.value = "alice@example.org"
|
||||||
self.channel.save()
|
self.channel.save()
|
||||||
|
|
||||||
def test_it_works(self):
|
self.url = "/integrations/%s/remove/" % self.channel.code
|
||||||
url = "/integrations/%s/remove/" % self.channel.code
|
|
||||||
|
|
||||||
|
def test_it_works(self):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
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)
|
self.assertRedirects(r, self.channels_url)
|
||||||
|
|
||||||
assert Channel.objects.count() == 0
|
assert Channel.objects.count() == 0
|
||||||
|
|
||||||
def test_team_access_works(self):
|
def test_team_access_works(self):
|
||||||
url = "/integrations/%s/remove/" % self.channel.code
|
|
||||||
|
|
||||||
self.client.login(username="bob@example.org", password="password")
|
self.client.login(username="bob@example.org", password="password")
|
||||||
self.client.post(url)
|
self.client.post(self.url)
|
||||||
assert Channel.objects.count() == 0
|
assert Channel.objects.count() == 0
|
||||||
|
|
||||||
def test_it_handles_bad_uuid(self):
|
def test_it_handles_bad_uuid(self):
|
||||||
|
@ -33,10 +31,8 @@ class RemoveChannelTestCase(BaseTestCase):
|
||||||
self.assertEqual(r.status_code, 404)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
def test_it_checks_owner(self):
|
def test_it_checks_owner(self):
|
||||||
url = "/integrations/%s/remove/" % self.channel.code
|
|
||||||
|
|
||||||
self.client.login(username="charlie@example.org", password="password")
|
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)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
def test_it_handles_missing_uuid(self):
|
def test_it_handles_missing_uuid(self):
|
||||||
|
@ -48,7 +44,14 @@ class RemoveChannelTestCase(BaseTestCase):
|
||||||
self.assertEqual(r.status_code, 404)
|
self.assertEqual(r.status_code, 404)
|
||||||
|
|
||||||
def test_it_rejects_get(self):
|
def test_it_rejects_get(self):
|
||||||
url = "/integrations/%s/remove/" % self.channel.code
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
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)
|
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)
|
||||||
|
|
|
@ -70,3 +70,13 @@ class UpdateChannelTestCase(BaseTestCase):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.channels_url, data=payload)
|
r = self.client.post(self.channels_url, data=payload)
|
||||||
self.assertEqual(r.status_code, 400)
|
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)
|
||||||
|
|
|
@ -51,3 +51,13 @@ class UpdateChannelNameTestCase(BaseTestCase):
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.get(self.url)
|
r = self.client.get(self.url)
|
||||||
self.assertEqual(r.status_code, 405)
|
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)
|
||||||
|
|
|
@ -110,15 +110,23 @@ def _get_channel_for_user(request, code):
|
||||||
|
|
||||||
assert request.user.is_authenticated
|
assert request.user.is_authenticated
|
||||||
|
|
||||||
q = Channel.objects
|
channel = get_object_or_404(Channel.objects.select_related("project"), code=code)
|
||||||
if not request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
project_ids = request.profile.projects().values("id")
|
return channel, True
|
||||||
q = q.filter(project_id__in=project_ids)
|
|
||||||
|
|
||||||
try:
|
if request.user.id == channel.project.owner_id:
|
||||||
return q.get(code=code)
|
return channel, True
|
||||||
except Channel.DoesNotExist:
|
|
||||||
raise Http404("not found")
|
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):
|
def _get_project_for_user(request, project_code):
|
||||||
|
@ -693,6 +701,9 @@ def channels(request, code):
|
||||||
project, rw = _get_project_for_user(request, code)
|
project, rw = _get_project_for_user(request, code)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
if not rw:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
code = request.POST["channel"]
|
code = request.POST["channel"]
|
||||||
try:
|
try:
|
||||||
channel = Channel.objects.get(code=code)
|
channel = Channel.objects.get(code=code)
|
||||||
|
@ -722,6 +733,7 @@ def channels(request, code):
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"page": "channels",
|
"page": "channels",
|
||||||
|
"rw": rw,
|
||||||
"project": project,
|
"project": project,
|
||||||
"profile": project.owner_profile,
|
"profile": project.owner_profile,
|
||||||
"channels": channels,
|
"channels": channels,
|
||||||
|
@ -746,7 +758,7 @@ def channels(request, code):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def channel_checks(request, code):
|
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())
|
assigned = set(channel.checks.values_list("code", flat=True).distinct())
|
||||||
checks = Check.objects.filter(project=channel.project).order_by("created")
|
checks = Check.objects.filter(project=channel.project).order_by("created")
|
||||||
|
@ -759,7 +771,7 @@ def channel_checks(request, code):
|
||||||
@require_POST
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def update_channel_name(request, code):
|
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)
|
form = forms.ChannelNameForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -817,7 +829,7 @@ def unsubscribe_email(request, code, signed_token):
|
||||||
@require_POST
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def send_test_notification(request, code):
|
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 = Check(name="TEST", status="down")
|
||||||
dummy.last_ping = timezone.now() - td(days=1)
|
dummy.last_ping = timezone.now() - td(days=1)
|
||||||
|
@ -846,7 +858,7 @@ def send_test_notification(request, code):
|
||||||
@require_POST
|
@require_POST
|
||||||
@login_required
|
@login_required
|
||||||
def remove_channel(request, code):
|
def remove_channel(request, code):
|
||||||
channel = _get_channel_for_user(request, code)
|
channel = _get_rw_channel_for_user(request, code)
|
||||||
project = channel.project
|
project = channel.project
|
||||||
channel.delete()
|
channel.delete()
|
||||||
|
|
||||||
|
@ -926,7 +938,7 @@ def add_webhook(request, code):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def edit_webhook(request, code):
|
def edit_webhook(request, code):
|
||||||
channel = _get_channel_for_user(request, code)
|
channel = _get_rw_channel_for_user(request, code)
|
||||||
if channel.kind != "webhook":
|
if channel.kind != "webhook":
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,6 @@ table.channels-table > tbody > tr > th {
|
||||||
.edit-name, .edit-checks {
|
.edit-name, .edit-checks {
|
||||||
padding: 12px 6px;
|
padding: 12px 6px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-name .channel-details-mini {
|
.edit-name .channel-details-mini {
|
||||||
|
@ -70,9 +69,10 @@ table.channels-table > tbody > tr > th {
|
||||||
color: #111;
|
color: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-row:hover .edit-name,
|
.rw .channel-row:hover .edit-name,
|
||||||
.channel-row:hover .edit-checks {
|
.rw .channel-row:hover .edit-checks {
|
||||||
border: 1px dotted #AAA;
|
border: 1px dotted #AAA;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-checks {
|
.edit-checks {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
||||||
$(".edit-checks").click(function() {
|
$(".rw .edit-checks").click(function() {
|
||||||
$("#checks-modal").modal("show");
|
$("#checks-modal").modal("show");
|
||||||
$.ajax(this.dataset.url).done(function(data) {
|
$.ajax(this.dataset.url).done(function(data) {
|
||||||
$("#checks-modal .modal-content").html(data);
|
$("#checks-modal .modal-content").html(data);
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{% if channels %}
|
{% if channels %}
|
||||||
<table class="table channels-table">
|
<table class="table channels-table {% if rw %}rw{% endif %}">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th class="th-name">Name, Details</th>
|
<th class="th-name">Name, Details</th>
|
||||||
|
@ -132,7 +132,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
{% if ch.kind == "webhook" %}
|
{% if ch.kind == "webhook" and rw %}
|
||||||
<a class="btn btn-sm btn-default" href="{% url 'hc-edit-webhook' ch.code %}">Edit</a>
|
<a class="btn btn-sm btn-default" href="{% url 'hc-edit-webhook' ch.code %}">Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form action="{% url 'hc-channel-test' ch.code %}" method="post">
|
<form action="{% url 'hc-channel-test' ch.code %}" method="post">
|
||||||
|
@ -145,6 +145,7 @@
|
||||||
Test!
|
Test!
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% if rw %}
|
||||||
<button
|
<button
|
||||||
data-kind="{{ ch.get_kind_display }}"
|
data-kind="{{ ch.get_kind_display }}"
|
||||||
data-url="{% url 'hc-remove-channel' ch.code %}"
|
data-url="{% url 'hc-remove-channel' ch.code %}"
|
||||||
|
@ -152,6 +153,7 @@
|
||||||
type="button">
|
type="button">
|
||||||
<span class="icon-delete"></span>
|
<span class="icon-delete"></span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
|
@ -173,6 +175,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if rw %}
|
||||||
<h1 class="ai-title">Add More</h1>
|
<h1 class="ai-title">Add More</h1>
|
||||||
<ul class="add-integration">
|
<ul class="add-integration">
|
||||||
<li>
|
<li>
|
||||||
|
@ -431,6 +434,7 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -472,6 +476,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if rw %}
|
||||||
{% for ch in channels %}
|
{% for ch in channels %}
|
||||||
<div id="name-{{ ch.code }}" class="modal channel-modal">
|
<div id="name-{{ ch.code }}" class="modal channel-modal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
@ -566,6 +571,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue