Browse Source

Return 403 when API key is wrong. Return 404 when resource not found. Return 405 when request method is wrong. Return 400 when request syntax is wrong.

pull/117/head
Pēteris Caune 8 years ago
parent
commit
5dafc07c29
16 changed files with 58 additions and 63 deletions
  1. +3
    -2
      hc/api/decorators.py
  2. +1
    -1
      hc/api/tests/test_badge.py
  3. +7
    -1
      hc/api/tests/test_bounce.py
  4. +2
    -2
      hc/api/tests/test_create_check.py
  5. +2
    -2
      hc/api/tests/test_pause.py
  6. +4
    -0
      hc/api/tests/test_ping.py
  7. +8
    -1
      hc/api/tests/test_update_check.py
  8. +16
    -26
      hc/api/views.py
  9. +1
    -1
      hc/front/tests/test_add_check.py
  10. +1
    -1
      hc/front/tests/test_cron_preview.py
  11. +1
    -1
      hc/front/tests/test_pause.py
  12. +1
    -1
      hc/front/tests/test_remove_channel.py
  13. +1
    -1
      hc/front/tests/test_remove_check.py
  14. +1
    -1
      hc/front/tests/test_update_name.py
  15. +1
    -1
      hc/front/tests/test_update_timeout.py
  16. +8
    -21
      hc/front/views.py

+ 3
- 2
hc/api/decorators.py View File

@ -3,7 +3,8 @@ import uuid
from functools import wraps from functools import wraps
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponseBadRequest, JsonResponse
from django.http import (HttpResponseBadRequest, HttpResponseForbidden,
JsonResponse)
from hc.lib.jsonschema import ValidationError, validate from hc.lib.jsonschema import ValidationError, validate
@ -44,7 +45,7 @@ def check_api_key(f):
try: try:
request.user = User.objects.get(profile__api_key=api_key) request.user = User.objects.get(profile__api_key=api_key)
except User.DoesNotExist: except User.DoesNotExist:
return make_error("wrong api_key")
return HttpResponseForbidden()
return f(request, *args, **kwds) return f(request, *args, **kwds)


+ 1
- 1
hc/api/tests/test_badge.py View File

@ -13,7 +13,7 @@ class BadgeTestCase(BaseTestCase):
def test_it_rejects_bad_signature(self): def test_it_rejects_bad_signature(self):
r = self.client.get("/badge/%s/12345678/foo.svg" % self.alice.username) r = self.client.get("/badge/%s/12345678/foo.svg" % self.alice.username)
assert r.status_code == 400
assert r.status_code == 404
def test_it_returns_svg(self): def test_it_returns_svg(self):
sig = base64_hmac(str(self.alice.username), "foo", settings.SECRET_KEY) sig = base64_hmac(str(self.alice.username), "foo", settings.SECRET_KEY)


+ 7
- 1
hc/api/tests/test_bounce.py View File

@ -33,10 +33,16 @@ class BounceTestCase(BaseTestCase):
url = "/api/v1/notifications/%s/bounce" % self.n.code url = "/api/v1/notifications/%s/bounce" % self.n.code
r = self.client.post(url, "foo", content_type="text/plain") r = self.client.post(url, "foo", content_type="text/plain")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 403)
def test_it_handles_long_payload(self): def test_it_handles_long_payload(self):
url = "/api/v1/notifications/%s/bounce" % self.n.code url = "/api/v1/notifications/%s/bounce" % self.n.code
payload = "A" * 500 payload = "A" * 500
r = self.client.post(url, payload, content_type="text/plain") r = self.client.post(url, payload, content_type="text/plain")
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
def test_it_handles_missing_notification(self):
fake_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02"
url = "/api/v1/notifications/%s/bounce" % fake_code
r = self.client.post(url, "", content_type="text/plain")
self.assertEqual(r.status_code, 404)

+ 2
- 2
hc/api/tests/test_create_check.py View File

@ -95,8 +95,8 @@ class CreateCheckTestCase(BaseTestCase):
self.assertEqual(r.json()["error"], "could not parse request body") self.assertEqual(r.json()["error"], "could not parse request body")
def test_it_rejects_wrong_api_key(self): def test_it_rejects_wrong_api_key(self):
self.post({"api_key": "wrong"},
expected_error="wrong api_key")
r = self.post({"api_key": "wrong"})
self.assertEqual(r.status_code, 403)
def test_it_rejects_small_timeout(self): def test_it_rejects_small_timeout(self):
self.post({"api_key": "abc", "timeout": 0}, self.post({"api_key": "abc", "timeout": 0},


+ 2
- 2
hc/api/tests/test_pause.py View File

@ -31,7 +31,7 @@ class PauseTestCase(BaseTestCase):
r = self.client.post(url, "", content_type="application/json", r = self.client.post(url, "", content_type="application/json",
HTTP_X_API_KEY="abc") HTTP_X_API_KEY="abc")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 403)
def test_it_validates_uuid(self): def test_it_validates_uuid(self):
url = "/api/v1/checks/not-uuid/pause" url = "/api/v1/checks/not-uuid/pause"
@ -45,4 +45,4 @@ class PauseTestCase(BaseTestCase):
r = self.client.post(url, "", content_type="application/json", r = self.client.post(url, "", content_type="application/json",
HTTP_X_API_KEY="abc") HTTP_X_API_KEY="abc")
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 404)

+ 4
- 0
hc/api/tests/test_ping.py View File

@ -45,6 +45,10 @@ class PingTestCase(TestCase):
r = self.client.get("/ping/not-uuid/") r = self.client.get("/ping/not-uuid/")
assert r.status_code == 400 assert r.status_code == 400
def test_it_handles_missing_check(self):
r = self.client.get("/ping/07c2f548-9850-4b27-af5d-6c9dc157ec02/")
assert r.status_code == 404
def test_it_handles_120_char_ua(self): def test_it_handles_120_char_ua(self):
ua = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) " ua = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) "
"AppleWebKit/537.36 (KHTML, like Gecko) " "AppleWebKit/537.36 (KHTML, like Gecko) "


+ 8
- 1
hc/api/tests/test_update_check.py View File

@ -74,7 +74,14 @@ class UpdateCheckTestCase(BaseTestCase):
def test_it_handles_missing_check(self): def test_it_handles_missing_check(self):
made_up_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02" made_up_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02"
r = self.post(made_up_code, {"api_key": "abc"}) r = self.post(made_up_code, {"api_key": "abc"})
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 404)
def test_it_validates_ownership(self):
check = Check(user=self.bob, status="up")
check.save()
r = self.post(check.code, {"api_key": "abc"})
self.assertEqual(r.status_code, 403)
def test_it_updates_cron_to_simple(self): def test_it_updates_cron_to_simple(self):
self.check.kind = "cron" self.check.kind = "cron"


+ 16
- 26
hc/api/views.py View File

@ -1,10 +1,13 @@
from datetime import timedelta as td from datetime import timedelta as td
from django.db.models import F from django.db.models import F
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
from django.http import (HttpResponse, HttpResponseForbidden,
HttpResponseNotFound, JsonResponse)
from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from hc.api import schemas from hc.api import schemas
from hc.api.decorators import check_api_key, uuid_or_400, validate_json from hc.api.decorators import check_api_key, uuid_or_400, validate_json
@ -16,10 +19,7 @@ from hc.lib.badges import check_signature, get_badge_svg
@uuid_or_400 @uuid_or_400
@never_cache @never_cache
def ping(request, code): def ping(request, code):
try:
check = Check.objects.get(code=code)
except Check.DoesNotExist:
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code)
check.n_pings = F("n_pings") + 1 check.n_pings = F("n_pings") + 1
check.last_ping = timezone.now() check.last_ping = timezone.now()
@ -122,34 +122,27 @@ def checks(request):
@csrf_exempt @csrf_exempt
@require_POST
@uuid_or_400 @uuid_or_400
@check_api_key @check_api_key
@validate_json(schemas.check) @validate_json(schemas.check)
def update(request, code): def update(request, code):
if request.method != "POST":
return HttpResponse(status=405) # method not allowed
try:
check = Check.objects.get(code=code, user=request.user)
except Check.DoesNotExist:
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code)
if check.user != request.user:
return HttpResponseForbidden()
_update(check, request.json) _update(check, request.json)
return JsonResponse(check.to_dict(), status=200) return JsonResponse(check.to_dict(), status=200)
@csrf_exempt @csrf_exempt
@require_POST
@uuid_or_400 @uuid_or_400
@check_api_key @check_api_key
def pause(request, code): def pause(request, code):
if request.method != "POST":
# Method not allowed
return HttpResponse(status=405)
try:
check = Check.objects.get(code=code, user=request.user)
except Check.DoesNotExist:
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code)
if check.user != request.user:
return HttpResponseForbidden()
check.status = "paused" check.status = "paused"
check.save() check.save()
@ -159,7 +152,7 @@ def pause(request, code):
@never_cache @never_cache
def badge(request, username, signature, tag): def badge(request, username, signature, tag):
if not check_signature(username, tag, signature): if not check_signature(username, tag, signature):
return HttpResponseBadRequest()
return HttpResponseNotFound()
status = "up" status = "up"
q = Check.objects.filter(user__username=username, tags__contains=tag) q = Check.objects.filter(user__username=username, tags__contains=tag)
@ -181,15 +174,12 @@ def badge(request, username, signature, tag):
@csrf_exempt @csrf_exempt
@uuid_or_400 @uuid_or_400
def bounce(request, code): def bounce(request, code):
try:
notification = Notification.objects.get(code=code)
except Notification.DoesNotExist:
return HttpResponseBadRequest()
notification = get_object_or_404(Notification, code=code)
# If webhook is more than 10 minutes late, don't accept it: # If webhook is more than 10 minutes late, don't accept it:
td = timezone.now() - notification.created td = timezone.now() - notification.created
if td.total_seconds() > 600: if td.total_seconds() > 600:
return HttpResponseBadRequest()
return HttpResponseForbidden()
notification.error = request.body[:200] notification.error = request.body[:200]
notification.save() notification.save()


+ 1
- 1
hc/front/tests/test_add_check.py View File

@ -24,4 +24,4 @@ class AddCheckTestCase(BaseTestCase):
url = "/checks/add/" url = "/checks/add/"
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 1
- 1
hc/front/tests/test_cron_preview.py View File

@ -29,4 +29,4 @@ class CronPreviewTestCase(BaseTestCase):
def test_it_rejects_get(self): def test_it_rejects_get(self):
r = self.client.get("/checks/cron_preview/", {}) r = self.client.get("/checks/cron_preview/", {})
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 1
- 1
hc/front/tests/test_pause.py View File

@ -23,4 +23,4 @@ class PauseTestCase(BaseTestCase):
url = "/checks/%s/pause/" % self.check.code url = "/checks/%s/pause/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 1
- 1
hc/front/tests/test_remove_channel.py View File

@ -52,4 +52,4 @@ class RemoveChannelTestCase(BaseTestCase):
url = "/integrations/%s/remove/" % self.channel.code url = "/integrations/%s/remove/" % self.channel.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 1
- 1
hc/front/tests/test_remove_check.py View File

@ -53,4 +53,4 @@ class RemoveCheckTestCase(BaseTestCase):
url = "/checks/%s/remove/" % self.check.code url = "/checks/%s/remove/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 1
- 1
hc/front/tests/test_update_name.py View File

@ -71,4 +71,4 @@ class UpdateNameTestCase(BaseTestCase):
url = "/checks/%s/name/" % self.check.code url = "/checks/%s/name/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 1
- 1
hc/front/tests/test_update_timeout.py View File

@ -125,4 +125,4 @@ class UpdateTimeoutTestCase(BaseTestCase):
url = "/checks/%s/timeout/" % self.check.code url = "/checks/%s/timeout/" % self.check.code
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.get(url) r = self.client.get(url)
self.assertEqual(r.status_code, 400)
self.assertEqual(r.status_code, 405)

+ 8
- 21
hc/front/views.py View File

@ -14,6 +14,7 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.utils.six.moves.urllib.parse import urlencode from django.utils.six.moves.urllib.parse import urlencode
from hc.api.decorators import uuid_or_400 from hc.api.decorators import uuid_or_400
from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check, from hc.api.models import (DEFAULT_GRACE, DEFAULT_TIMEOUT, Channel, Check,
@ -131,11 +132,9 @@ def about(request):
return render(request, "front/about.html", {"page": "about"}) return render(request, "front/about.html", {"page": "about"})
@require_POST
@login_required @login_required
def add_check(request): def add_check(request):
if request.method != "POST":
return HttpResponseBadRequest()
check = Check(user=request.team.user) check = Check(user=request.team.user)
check.save() check.save()
@ -144,12 +143,10 @@ def add_check(request):
return redirect("hc-checks") return redirect("hc-checks")
@require_POST
@login_required @login_required
@uuid_or_400 @uuid_or_400
def update_name(request, code): def update_name(request, code):
if request.method != "POST":
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user_id != request.team.user.id: if check.user_id != request.team.user.id:
return HttpResponseForbidden() return HttpResponseForbidden()
@ -163,12 +160,10 @@ def update_name(request, code):
return redirect("hc-checks") return redirect("hc-checks")
@require_POST
@login_required @login_required
@uuid_or_400 @uuid_or_400
def update_timeout(request, code): def update_timeout(request, code):
if request.method != "POST":
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user != request.team.user: if check.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
@ -200,10 +195,8 @@ def update_timeout(request, code):
@csrf_exempt @csrf_exempt
@require_POST
def cron_preview(request): def cron_preview(request):
if request.method != "POST":
return HttpResponseBadRequest()
schedule = request.POST.get("schedule") schedule = request.POST.get("schedule")
tz = request.POST.get("tz") tz = request.POST.get("tz")
ctx = {"tz": tz, "dates": []} ctx = {"tz": tz, "dates": []}
@ -223,12 +216,10 @@ def cron_preview(request):
return render(request, "front/cron_preview.html", ctx) return render(request, "front/cron_preview.html", ctx)
@require_POST
@login_required @login_required
@uuid_or_400 @uuid_or_400
def pause(request, code): def pause(request, code):
if request.method != "POST":
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user_id != request.team.user.id: if check.user_id != request.team.user.id:
return HttpResponseForbidden() return HttpResponseForbidden()
@ -239,12 +230,10 @@ def pause(request, code):
return redirect("hc-checks") return redirect("hc-checks")
@require_POST
@login_required @login_required
@uuid_or_400 @uuid_or_400
def remove_check(request, code): def remove_check(request, code):
if request.method != "POST":
return HttpResponseBadRequest()
check = get_object_or_404(Check, code=code) check = get_object_or_404(Check, code=code)
if check.user != request.team.user: if check.user != request.team.user:
return HttpResponseForbidden() return HttpResponseForbidden()
@ -375,12 +364,10 @@ def unsubscribe_email(request, code, token):
return render(request, "front/unsubscribe_success.html") return render(request, "front/unsubscribe_success.html")
@require_POST
@login_required @login_required
@uuid_or_400 @uuid_or_400
def remove_channel(request, code): def remove_channel(request, code):
if request.method != "POST":
return HttpResponseBadRequest()
# user may refresh the page during POST and cause two deletion attempts # user may refresh the page during POST and cause two deletion attempts
channel = Channel.objects.filter(code=code).first() channel = Channel.objects.filter(code=code).first()
if channel: if channel:


Loading…
Cancel
Save