forked from GithubBackups/healthchecks
Allow simultaneous access to checks from different teams
This commit is contained in:
parent
d36d4fb543
commit
c2f200fa02
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||
### Improvements
|
||||
- Set Pushover alert priorities for "down" and "up" events separately
|
||||
- Additional python usage examples
|
||||
- Allow simultaneous access to checks from different teams
|
||||
|
||||
### Bug Fixes
|
||||
- Fix after-login redirects (the "?next=" query parameter)
|
||||
@ -52,4 +53,4 @@ All notable changes to this project will be documented in this file.
|
||||
- A new "Check Details" page.
|
||||
- Updated django-compressor, psycopg2, pytz, requests package versions.
|
||||
- C# usage example.
|
||||
- Checks have a "Description" field.
|
||||
- Checks have a "Description" field.
|
||||
|
@ -29,7 +29,7 @@ class DetailsTestCase(BaseTestCase):
|
||||
def test_it_checks_ownership(self):
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
assert r.status_code == 403
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_shows_cron_expression(self):
|
||||
self.check.kind = "cron"
|
||||
@ -38,3 +38,11 @@ class DetailsTestCase(BaseTestCase):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Cron Expression", status_code=200)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -52,7 +52,7 @@ class LogTestCase(BaseTestCase):
|
||||
url = "/checks/%s/log/" % self.check.code
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_shows_pushover_notifications(self):
|
||||
ch = Channel(kind="po", user=self.alice)
|
||||
@ -77,3 +77,12 @@ class LogTestCase(BaseTestCase):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(url)
|
||||
self.assertContains(r, "Called webhook foo/$NAME", status_code=200)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
url = "/checks/%s/log/" % self.check.code
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -24,3 +24,13 @@ class PauseTestCase(BaseTestCase):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
url = "/checks/%s/pause/" % self.check.code
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.post(url)
|
||||
self.assertRedirects(r, "/checks/")
|
||||
|
@ -17,7 +17,7 @@ class LastPingTestCase(BaseTestCase):
|
||||
def test_it_requires_user(self):
|
||||
check = Check.objects.create()
|
||||
r = self.client.get("/checks/%s/last_ping/" % check.code)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_accepts_n(self):
|
||||
check = Check(user=self.alice)
|
||||
@ -34,3 +34,16 @@ class LastPingTestCase(BaseTestCase):
|
||||
|
||||
r = self.client.get("/checks/%s/pings/2/" % check.code)
|
||||
self.assertContains(r, "bar-456", status_code=200)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
check = Check(user=self.alice)
|
||||
check.save()
|
||||
|
||||
Ping.objects.create(owner=check, body="this is body")
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.get("/checks/%s/last_ping/" % check.code)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -9,37 +9,32 @@ class RemoveCheckTestCase(BaseTestCase):
|
||||
self.check = Check(user=self.alice)
|
||||
self.check.save()
|
||||
|
||||
def test_it_works(self):
|
||||
url = "/checks/%s/remove/" % self.check.code
|
||||
self.remove_url = "/checks/%s/remove/" % self.check.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.remove_url)
|
||||
self.assertRedirects(r, "/checks/")
|
||||
|
||||
assert Check.objects.count() == 0
|
||||
self.assertEqual(Check.objects.count(), 0)
|
||||
|
||||
def test_team_access_works(self):
|
||||
url = "/checks/%s/remove/" % self.check.code
|
||||
|
||||
# Logging in as bob, not alice. Bob has team access so this
|
||||
# should work.
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
self.client.post(url)
|
||||
assert Check.objects.count() == 0
|
||||
self.client.post(self.remove_url)
|
||||
|
||||
self.assertEqual(Check.objects.count(), 0)
|
||||
|
||||
def test_it_handles_bad_uuid(self):
|
||||
url = "/checks/not-uuid/remove/"
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(url)
|
||||
r = self.client.post("/checks/not-uuid/remove/")
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_checks_owner(self):
|
||||
url = "/checks/%s/remove/" % self.check.code
|
||||
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.post(url)
|
||||
assert r.status_code == 403
|
||||
r = self.client.post(self.remove_url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_handles_missing_uuid(self):
|
||||
# Valid UUID but there is no check for it:
|
||||
@ -47,10 +42,17 @@ class RemoveCheckTestCase(BaseTestCase):
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(url)
|
||||
assert r.status_code == 404
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_rejects_get(self):
|
||||
url = "/checks/%s/remove/" % self.check.code
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(url)
|
||||
r = self.client.get(self.remove_url)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.post(self.remove_url)
|
||||
self.assertRedirects(r, "/checks/")
|
||||
|
@ -46,3 +46,11 @@ class StatusSingleTestCase(BaseTestCase):
|
||||
doc = r.json()
|
||||
|
||||
self.assertFalse("events" in doc)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.get("/checks/%s/status/" % self.check.code)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -32,7 +32,7 @@ class SwitchChannelTestCase(BaseTestCase):
|
||||
def test_it_checks_ownership(self):
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.post(self.url, {"state": "on"})
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_checks_channels_ownership(self):
|
||||
cc = Check(user=self.charlie)
|
||||
@ -43,4 +43,12 @@ class SwitchChannelTestCase(BaseTestCase):
|
||||
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.post(self.url, {"state": "on"})
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.post(self.url, {"state": "on"})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -12,10 +12,8 @@ class UpdateNameTestCase(BaseTestCase):
|
||||
self.url = "/checks/%s/name/" % self.check.code
|
||||
|
||||
def test_it_works(self):
|
||||
payload = {"name": "Alice Was Here"}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, data=payload)
|
||||
r = self.client.post(self.url, data={"name": "Alice Was Here"})
|
||||
self.assertRedirects(r, "/checks/")
|
||||
|
||||
self.check.refresh_from_db()
|
||||
@ -32,12 +30,22 @@ class UpdateNameTestCase(BaseTestCase):
|
||||
self.check.refresh_from_db()
|
||||
self.assertEqual(self.check.name, "Bob Was Here")
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
# Bob's current team is not set
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
# But this should still work:
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.post(self.url, data={"name": "Bob Was Here"})
|
||||
self.assertRedirects(r, "/checks/")
|
||||
|
||||
def test_it_checks_ownership(self):
|
||||
payload = {"name": "Charlie Sent This"}
|
||||
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.post(self.url, data=payload)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_handles_bad_uuid(self):
|
||||
url = "/checks/not-uuid/name/"
|
||||
|
@ -145,10 +145,21 @@ class UpdateTimeoutTestCase(BaseTestCase):
|
||||
|
||||
self.client.login(username="charlie@example.org", password="password")
|
||||
r = self.client.post(url, data=payload)
|
||||
assert r.status_code == 403
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_it_rejects_get(self):
|
||||
url = "/checks/%s/timeout/" % self.check.code
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 405)
|
||||
|
||||
def test_it_allows_cross_team_access(self):
|
||||
self.bobs_profile.current_team = None
|
||||
self.bobs_profile.save()
|
||||
|
||||
url = "/checks/%s/timeout/" % self.check.code
|
||||
payload = {"kind": "simple", "timeout": 3600, "grace": 60}
|
||||
|
||||
self.client.login(username="bob@example.org", password="password")
|
||||
r = self.client.post(url, data=payload)
|
||||
self.assertRedirects(r, "/checks/")
|
||||
|
@ -60,6 +60,23 @@ def _tags_statuses(checks):
|
||||
return tags, num_down
|
||||
|
||||
|
||||
def _get_check_for_user(request, code):
|
||||
""" Return specified check if current user has access to it. """
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
raise Http404("not found")
|
||||
|
||||
if request.user.is_superuser:
|
||||
q = Check.objects
|
||||
else:
|
||||
q = request.profile.checks_from_all_teams()
|
||||
|
||||
try:
|
||||
return q.get(code=code)
|
||||
except Check.DoesNotExist:
|
||||
raise Http404("not found")
|
||||
|
||||
|
||||
@login_required
|
||||
def my_checks(request):
|
||||
if request.GET.get("sort") in VALID_SORT_VALUES:
|
||||
@ -136,13 +153,11 @@ def status(request):
|
||||
@login_required
|
||||
@require_POST
|
||||
def switch_channel(request, code, channel_code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user_id != request.team.user.id:
|
||||
return HttpResponseForbidden()
|
||||
check = _get_check_for_user(request, code)
|
||||
|
||||
channel = get_object_or_404(Channel, code=channel_code)
|
||||
if channel.user_id != request.team.user.id:
|
||||
return HttpResponseForbidden()
|
||||
if channel.user_id != check.user_id:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if request.POST.get("state") == "on":
|
||||
channel.checks.add(check)
|
||||
@ -228,10 +243,7 @@ def add_check(request):
|
||||
@require_POST
|
||||
@login_required
|
||||
def update_name(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user_id != request.team.user.id:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
check = _get_check_for_user(request, code)
|
||||
form = NameTagsForm(request.POST)
|
||||
if form.is_valid():
|
||||
check.name = form.cleaned_data["name"]
|
||||
@ -248,9 +260,7 @@ def update_name(request, code):
|
||||
@require_POST
|
||||
@login_required
|
||||
def update_timeout(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user != request.team.user:
|
||||
return HttpResponseForbidden()
|
||||
check = _get_check_for_user(request, code)
|
||||
|
||||
kind = request.POST.get("kind")
|
||||
if kind == "simple":
|
||||
@ -304,13 +314,7 @@ def cron_preview(request):
|
||||
|
||||
|
||||
def ping_details(request, code, n=None):
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user_id != request.team.user.id:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
check = _get_check_for_user(request, code)
|
||||
q = Ping.objects.filter(owner=check)
|
||||
if n:
|
||||
q = q.filter(n=n)
|
||||
@ -328,9 +332,7 @@ def ping_details(request, code, n=None):
|
||||
@require_POST
|
||||
@login_required
|
||||
def pause(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user_id != request.team.user.id:
|
||||
return HttpResponseForbidden()
|
||||
check = _get_check_for_user(request, code)
|
||||
|
||||
check.status = "paused"
|
||||
check.save()
|
||||
@ -344,12 +346,8 @@ def pause(request, code):
|
||||
@require_POST
|
||||
@login_required
|
||||
def remove_check(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user != request.team.user:
|
||||
return HttpResponseForbidden()
|
||||
|
||||
check = _get_check_for_user(request, code)
|
||||
check.delete()
|
||||
|
||||
return redirect("hc-checks")
|
||||
|
||||
|
||||
@ -371,11 +369,9 @@ def _get_events(check, limit):
|
||||
|
||||
@login_required
|
||||
def log(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user != request.team.user:
|
||||
return HttpResponseForbidden()
|
||||
check = _get_check_for_user(request, code)
|
||||
|
||||
limit = request.team.ping_log_limit
|
||||
limit = check.user.profile.ping_log_limit
|
||||
ctx = {
|
||||
"check": check,
|
||||
"events": _get_events(check, limit),
|
||||
@ -388,11 +384,9 @@ def log(request, code):
|
||||
|
||||
@login_required
|
||||
def details(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user != request.team.user:
|
||||
return HttpResponseForbidden()
|
||||
check = _get_check_for_user(request, code)
|
||||
|
||||
channels = Channel.objects.filter(user=request.team.user)
|
||||
channels = Channel.objects.filter(user=check.user)
|
||||
channels = list(channels.order_by("created"))
|
||||
|
||||
ctx = {
|
||||
@ -407,9 +401,7 @@ def details(request, code):
|
||||
|
||||
@login_required
|
||||
def status_single(request, code):
|
||||
check = get_object_or_404(Check, code=code)
|
||||
if check.user_id != request.team.user.id:
|
||||
return HttpResponseForbidden()
|
||||
check = _get_check_for_user(request, code)
|
||||
|
||||
status = check.get_status()
|
||||
events = _get_events(check, 20)
|
||||
|
@ -115,7 +115,12 @@
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a id="nav-email" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button">
|
||||
{{ request.team }} <span class="caret"></span>
|
||||
{% if check %}
|
||||
{{ check.user.profile }}
|
||||
{% else %}
|
||||
{{ request.team }}
|
||||
{% endif %}
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{% with teams=request.get_teams %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user