diff --git a/hc/api/decorators.py b/hc/api/decorators.py index c025b51d..ae2bc8bb 100644 --- a/hc/api/decorators.py +++ b/hc/api/decorators.py @@ -26,22 +26,26 @@ def make_error(msg): def check_api_key(f): @wraps(f) def wrapper(request, *args, **kwds): - try: - data = json.loads(request.body.decode("utf-8")) - except ValueError: - return make_error("could not parse request body") + request.json = {} + if request.body: + try: + request.json = json.loads(request.body.decode("utf-8")) + except ValueError: + return make_error("could not parse request body") + + if "HTTP_X_API_KEY" in request.META: + api_key = request.META["HTTP_X_API_KEY"] + else: + api_key = request.json.get("api_key", "") - api_key = str(data.get("api_key", "")) if api_key == "": return make_error("wrong api_key") try: - user = User.objects.get(profile__api_key=api_key) + request.user = User.objects.get(profile__api_key=api_key) except User.DoesNotExist: return make_error("wrong api_key") - request.json = data - request.user = user return f(request, *args, **kwds) return wrapper diff --git a/hc/api/tests/test_create_check.py b/hc/api/tests/test_create_check.py index 5ba6f0dd..860db1d3 100644 --- a/hc/api/tests/test_create_check.py +++ b/hc/api/tests/test_create_check.py @@ -5,16 +5,23 @@ from hc.test import BaseTestCase class CreateCheckTestCase(BaseTestCase): + URL = "/api/v1/checks/" def setUp(self): super(CreateCheckTestCase, self).setUp() - def post(self, url, data): - return self.client.post(url, json.dumps(data), - content_type="application/json") + def post(self, data, expected_error=None): + r = self.client.post(self.URL, json.dumps(data), + content_type="application/json") + + if expected_error: + self.assertEqual(r.status_code, 400) + self.assertEqual(r.json()["error"], expected_error) + + return r def test_it_works(self): - r = self.post("/api/v1/checks/", { + r = self.post({ "api_key": "abc", "name": "Foo", "tags": "bar,baz", @@ -32,46 +39,51 @@ class CreateCheckTestCase(BaseTestCase): self.assertEqual(check.timeout.total_seconds(), 3600) self.assertEqual(check.grace.total_seconds(), 60) + def test_it_accepts_api_key_in_header(self): + payload = json.dumps({"name": "Foo"}) + r = self.client.post(self.URL, payload, + content_type="application/json", + HTTP_X_API_KEY="abc") + + self.assertEqual(r.status_code, 201) + def test_it_assigns_channels(self): channel = Channel(user=self.alice) channel.save() - r = self.post("/api/v1/checks/", { - "api_key": "abc", - "channels": "*" - }) + r = self.post({"api_key": "abc", "channels": "*"}) self.assertEqual(r.status_code, 201) check = Check.objects.get() self.assertEqual(check.channel_set.get(), channel) def test_it_handles_missing_request_body(self): - r = self.client.post("/api/v1/checks/", - content_type="application/json") + r = self.client.post(self.URL, content_type="application/json") self.assertEqual(r.status_code, 400) self.assertEqual(r.json()["error"], "wrong api_key") - def test_it_rejects_wrong_api_key(self): - r = self.post("/api/v1/checks/", {"api_key": "wrong"}) - self.assertEqual(r.json()["error"], "wrong api_key") - def test_it_handles_invalid_json(self): - r = self.client.post("/api/v1/checks/", "this is not json", + r = self.client.post(self.URL, "this is not json", content_type="application/json") + self.assertEqual(r.status_code, 400) self.assertEqual(r.json()["error"], "could not parse request body") + def test_it_rejects_wrong_api_key(self): + self.post({"api_key": "wrong"}, + expected_error="wrong api_key") + def test_it_rejects_small_timeout(self): - r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 0}) - self.assertEqual(r.json()["error"], "timeout is too small") + self.post({"api_key": "abc", "timeout": 0}, + expected_error="timeout is too small") def test_it_rejects_large_timeout(self): - r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 604801}) - self.assertEqual(r.json()["error"], "timeout is too large") + self.post({"api_key": "abc", "timeout": 604801}, + expected_error="timeout is too large") def test_it_rejects_non_number_timeout(self): - r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": "oops"}) - self.assertEqual(r.json()["error"], "timeout is not a number") + self.post({"api_key": "abc", "timeout": "oops"}, + expected_error="timeout is not a number") def test_it_rejects_non_string_name(self): - r = self.post("/api/v1/checks/", {"api_key": "abc", "name": False}) - self.assertEqual(r.json()["error"], "name is not a string") + self.post({"api_key": "abc", "name": False}, + expected_error="name is not a string") diff --git a/hc/api/tests/test_list_checks.py b/hc/api/tests/test_list_checks.py index 2be3f77b..b1e9d162 100644 --- a/hc/api/tests/test_list_checks.py +++ b/hc/api/tests/test_list_checks.py @@ -10,38 +10,51 @@ class ListChecksTestCase(BaseTestCase): def setUp(self): super(ListChecksTestCase, self).setUp() - self.checks = [ - Check(user=self.alice, name="Alice 1", timeout=td(seconds=3600), grace=td(seconds=900)), - Check(user=self.alice, name="Alice 2", timeout=td(seconds=86400), grace=td(seconds=3600)), - ] - for check in self.checks: - check.save() + self.a1 = Check(user=self.alice, name="Alice 1") + self.a1.timeout = td(seconds=3600) + self.a1.grace = td(seconds=900) + self.a1.save() - def get(self, url, data): - return self.client.generic('GET', url, json.dumps(data), 'application/json') + self.a2 = Check(user=self.alice, name="Alice 2") + self.a2.timeout = td(seconds=86400) + self.a2.grace = td(seconds=3600) + self.a2.save() - def test_it_works(self): - r = self.get("/api/v1/checks/", { "api_key": "abc" }) + def get(self): + return self.client.get("/api/v1/checks/", HTTP_X_API_KEY="abc") + def test_it_works(self): + r = self.get() self.assertEqual(r.status_code, 200) - self.assertTrue("checks" in r.json()) - self.assertEqual(len(r.json()["checks"]), 2) - checks = { check["name"]: check for check in r.json()["checks"] } + doc = r.json() + self.assertTrue("checks" in doc) + + checks = {check["name"]: check for check in doc["checks"]} + self.assertEqual(len(checks), 2) + self.assertEqual(checks["Alice 1"]["timeout"], 3600) - self.assertEqual(checks["Alice 1"]["grace"], 900) - self.assertEqual(checks["Alice 1"]["ping_url"], self.checks[0].url()) + self.assertEqual(checks["Alice 1"]["grace"], 900) + self.assertEqual(checks["Alice 1"]["ping_url"], self.a1.url()) + self.assertEqual(checks["Alice 2"]["timeout"], 86400) - self.assertEqual(checks["Alice 2"]["grace"], 3600) - self.assertEqual(checks["Alice 2"]["ping_url"], self.checks[1].url()) + self.assertEqual(checks["Alice 2"]["grace"], 3600) + self.assertEqual(checks["Alice 2"]["ping_url"], self.a2.url()) def test_it_shows_only_users_checks(self): bobs_check = Check(user=self.bob, name="Bob 1") bobs_check.save() - r = self.get("/api/v1/checks/", {"api_key": "abc"}) - + r = self.get() data = r.json() self.assertEqual(len(data["checks"]), 2) for check in data["checks"]: self.assertNotEqual(check["name"], "Bob 1") + + def test_it_accepts_api_key_from_request_body(self): + payload = json.dumps({"api_key": "abc"}) + r = self.client.generic("GET", "/api/v1/checks/", payload, + content_type="application/json") + + self.assertEqual(r.status_code, 200) + self.assertContains(r, "Alice") diff --git a/hc/api/urls.py b/hc/api/urls.py index 3a616f24..b0c84e17 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -5,6 +5,6 @@ from hc.api import views urlpatterns = [ url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"), url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"), - url(r'^api/v1/checks/$', views.create_check), + url(r'^api/v1/checks/$', views.checks), url(r'^badge/([\w-]+)/([\w-]{8})/([\w-]+).svg$', views.badge, name="hc-badge"), ] diff --git a/hc/api/views.py b/hc/api/views.py index f9164deb..0058815c 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -48,7 +48,7 @@ def ping(request, code): @csrf_exempt @check_api_key @validate_json(schemas.check) -def create_check(request): +def checks(request): if request.method == "GET": code = 200 response = { diff --git a/templates/front/docs_api.html b/templates/front/docs_api.html index 3a02830e..c27d9e74 100644 --- a/templates/front/docs_api.html +++ b/templates/front/docs_api.html @@ -17,9 +17,14 @@ API key. By default, an user account on healthchecks.io doesn't have an API key. You can create one in the Settings page.
--The API uses a simple authentication scheme: the API key should be -included in the request body (a JSON document) along other fields. +
The client can authenticate itself by sending an appropriate HTTP
+request header. The header's name should be X-Api-Key
and
+its value should be your API key.
+
Alternatively, for POST requests with a JSON request body,
+ the client can include an api_key
field in the JSON document.
+ See below the "Create a check" section for an example.
curl {{ SITE_ROOT }}/api/v1/checks/ \
- -X POST \
- -d '{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
+ --header "X-Api-Key: your-api-key" \
+ --data '{"name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
+
+# Or, alternatively:
+curl {{ SITE_ROOT }}/api/v1/checks/ \
+ --data '{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
curl {{ SITE_ROOT }}/api/v1/checks/ \
- -X GET \
- -d '{"api_key": "your-api-key"}'
+curl --header "X-Api-Key: your-api-key" {{ SITE_ROOT }}/api/v1/checks/
diff --git a/templates/front/snippets/list_checks_request.txt b/templates/front/snippets/list_checks_request.txt
index 18f91aca..079a6ba2 100644
--- a/templates/front/snippets/list_checks_request.txt
+++ b/templates/front/snippets/list_checks_request.txt
@@ -1,3 +1 @@
-curl SITE_ROOT/api/v1/checks/ \
- -X GET \
- -d '{"api_key": "your-api-key"}'
\ No newline at end of file
+curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/
\ No newline at end of file