From af997446f3ae286f4d386a131ed5f1b58eb8187e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Thu, 7 Jul 2016 20:07:10 +0300
Subject: [PATCH] Add support for authentication with X-Api-Key header.
---
hc/api/decorators.py | 20 ++++---
hc/api/tests/test_create_check.py | 58 +++++++++++--------
hc/api/tests/test_list_checks.py | 51 ++++++++++------
hc/api/urls.py | 2 +-
hc/api/views.py | 2 +-
templates/front/docs_api.html | 11 +++-
.../front/snippets/create_check_request.html | 8 ++-
.../front/snippets/create_check_request.txt | 8 ++-
.../front/snippets/list_checks_request.html | 4 +-
.../front/snippets/list_checks_request.txt | 4 +-
10 files changed, 103 insertions(+), 65 deletions(-)
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.
API Requests
diff --git a/templates/front/snippets/create_check_request.html b/templates/front/snippets/create_check_request.html
index 4254cff7..8e497dfa 100644
--- a/templates/front/snippets/create_check_request.html
+++ b/templates/front/snippets/create_check_request.html
@@ -1,4 +1,8 @@
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}'
diff --git a/templates/front/snippets/create_check_request.txt b/templates/front/snippets/create_check_request.txt
index 8f760ed2..86d59fd3 100644
--- a/templates/front/snippets/create_check_request.txt
+++ b/templates/front/snippets/create_check_request.txt
@@ -1,3 +1,7 @@
curl SITE_ROOT/api/v1/checks/ \
- -X POST \
- -d '{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
\ No newline at end of file
+ --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}'
diff --git a/templates/front/snippets/list_checks_request.html b/templates/front/snippets/list_checks_request.html
index 0998b652..c0a277a6 100644
--- a/templates/front/snippets/list_checks_request.html
+++ b/templates/front/snippets/list_checks_request.html
@@ -1,4 +1,2 @@
-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