forked from GithubBackups/healthchecks
Add support for authentication with X-Api-Key header.
This commit is contained in:
parent
44163c3758
commit
af997446f3
@ -26,22 +26,26 @@ def make_error(msg):
|
|||||||
def check_api_key(f):
|
def check_api_key(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(request, *args, **kwds):
|
def wrapper(request, *args, **kwds):
|
||||||
try:
|
request.json = {}
|
||||||
data = json.loads(request.body.decode("utf-8"))
|
if request.body:
|
||||||
except ValueError:
|
try:
|
||||||
return make_error("could not parse request body")
|
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 == "":
|
if api_key == "":
|
||||||
return make_error("wrong api_key")
|
return make_error("wrong api_key")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
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 make_error("wrong api_key")
|
||||||
|
|
||||||
request.json = data
|
|
||||||
request.user = user
|
|
||||||
return f(request, *args, **kwds)
|
return f(request, *args, **kwds)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -5,16 +5,23 @@ from hc.test import BaseTestCase
|
|||||||
|
|
||||||
|
|
||||||
class CreateCheckTestCase(BaseTestCase):
|
class CreateCheckTestCase(BaseTestCase):
|
||||||
|
URL = "/api/v1/checks/"
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CreateCheckTestCase, self).setUp()
|
super(CreateCheckTestCase, self).setUp()
|
||||||
|
|
||||||
def post(self, url, data):
|
def post(self, data, expected_error=None):
|
||||||
return self.client.post(url, json.dumps(data),
|
r = self.client.post(self.URL, json.dumps(data),
|
||||||
content_type="application/json")
|
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):
|
def test_it_works(self):
|
||||||
r = self.post("/api/v1/checks/", {
|
r = self.post({
|
||||||
"api_key": "abc",
|
"api_key": "abc",
|
||||||
"name": "Foo",
|
"name": "Foo",
|
||||||
"tags": "bar,baz",
|
"tags": "bar,baz",
|
||||||
@ -32,46 +39,51 @@ class CreateCheckTestCase(BaseTestCase):
|
|||||||
self.assertEqual(check.timeout.total_seconds(), 3600)
|
self.assertEqual(check.timeout.total_seconds(), 3600)
|
||||||
self.assertEqual(check.grace.total_seconds(), 60)
|
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):
|
def test_it_assigns_channels(self):
|
||||||
channel = Channel(user=self.alice)
|
channel = Channel(user=self.alice)
|
||||||
channel.save()
|
channel.save()
|
||||||
|
|
||||||
r = self.post("/api/v1/checks/", {
|
r = self.post({"api_key": "abc", "channels": "*"})
|
||||||
"api_key": "abc",
|
|
||||||
"channels": "*"
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertEqual(r.status_code, 201)
|
self.assertEqual(r.status_code, 201)
|
||||||
check = Check.objects.get()
|
check = Check.objects.get()
|
||||||
self.assertEqual(check.channel_set.get(), channel)
|
self.assertEqual(check.channel_set.get(), channel)
|
||||||
|
|
||||||
def test_it_handles_missing_request_body(self):
|
def test_it_handles_missing_request_body(self):
|
||||||
r = self.client.post("/api/v1/checks/",
|
r = self.client.post(self.URL, content_type="application/json")
|
||||||
content_type="application/json")
|
|
||||||
self.assertEqual(r.status_code, 400)
|
self.assertEqual(r.status_code, 400)
|
||||||
self.assertEqual(r.json()["error"], "wrong api_key")
|
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):
|
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")
|
content_type="application/json")
|
||||||
|
self.assertEqual(r.status_code, 400)
|
||||||
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):
|
||||||
|
self.post({"api_key": "wrong"},
|
||||||
|
expected_error="wrong api_key")
|
||||||
|
|
||||||
def test_it_rejects_small_timeout(self):
|
def test_it_rejects_small_timeout(self):
|
||||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 0})
|
self.post({"api_key": "abc", "timeout": 0},
|
||||||
self.assertEqual(r.json()["error"], "timeout is too small")
|
expected_error="timeout is too small")
|
||||||
|
|
||||||
def test_it_rejects_large_timeout(self):
|
def test_it_rejects_large_timeout(self):
|
||||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": 604801})
|
self.post({"api_key": "abc", "timeout": 604801},
|
||||||
self.assertEqual(r.json()["error"], "timeout is too large")
|
expected_error="timeout is too large")
|
||||||
|
|
||||||
def test_it_rejects_non_number_timeout(self):
|
def test_it_rejects_non_number_timeout(self):
|
||||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "timeout": "oops"})
|
self.post({"api_key": "abc", "timeout": "oops"},
|
||||||
self.assertEqual(r.json()["error"], "timeout is not a number")
|
expected_error="timeout is not a number")
|
||||||
|
|
||||||
def test_it_rejects_non_string_name(self):
|
def test_it_rejects_non_string_name(self):
|
||||||
r = self.post("/api/v1/checks/", {"api_key": "abc", "name": False})
|
self.post({"api_key": "abc", "name": False},
|
||||||
self.assertEqual(r.json()["error"], "name is not a string")
|
expected_error="name is not a string")
|
||||||
|
@ -10,38 +10,51 @@ class ListChecksTestCase(BaseTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ListChecksTestCase, self).setUp()
|
super(ListChecksTestCase, self).setUp()
|
||||||
|
|
||||||
self.checks = [
|
self.a1 = Check(user=self.alice, name="Alice 1")
|
||||||
Check(user=self.alice, name="Alice 1", timeout=td(seconds=3600), grace=td(seconds=900)),
|
self.a1.timeout = td(seconds=3600)
|
||||||
Check(user=self.alice, name="Alice 2", timeout=td(seconds=86400), grace=td(seconds=3600)),
|
self.a1.grace = td(seconds=900)
|
||||||
]
|
self.a1.save()
|
||||||
for check in self.checks:
|
|
||||||
check.save()
|
|
||||||
|
|
||||||
def get(self, url, data):
|
self.a2 = Check(user=self.alice, name="Alice 2")
|
||||||
return self.client.generic('GET', url, json.dumps(data), 'application/json')
|
self.a2.timeout = td(seconds=86400)
|
||||||
|
self.a2.grace = td(seconds=3600)
|
||||||
|
self.a2.save()
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
return self.client.get("/api/v1/checks/", HTTP_X_API_KEY="abc")
|
||||||
|
|
||||||
def test_it_works(self):
|
def test_it_works(self):
|
||||||
r = self.get("/api/v1/checks/", { "api_key": "abc" })
|
r = self.get()
|
||||||
|
|
||||||
self.assertEqual(r.status_code, 200)
|
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"]["timeout"], 3600)
|
||||||
self.assertEqual(checks["Alice 1"]["grace"], 900)
|
self.assertEqual(checks["Alice 1"]["grace"], 900)
|
||||||
self.assertEqual(checks["Alice 1"]["ping_url"], self.checks[0].url())
|
self.assertEqual(checks["Alice 1"]["ping_url"], self.a1.url())
|
||||||
|
|
||||||
self.assertEqual(checks["Alice 2"]["timeout"], 86400)
|
self.assertEqual(checks["Alice 2"]["timeout"], 86400)
|
||||||
self.assertEqual(checks["Alice 2"]["grace"], 3600)
|
self.assertEqual(checks["Alice 2"]["grace"], 3600)
|
||||||
self.assertEqual(checks["Alice 2"]["ping_url"], self.checks[1].url())
|
self.assertEqual(checks["Alice 2"]["ping_url"], self.a2.url())
|
||||||
|
|
||||||
def test_it_shows_only_users_checks(self):
|
def test_it_shows_only_users_checks(self):
|
||||||
bobs_check = Check(user=self.bob, name="Bob 1")
|
bobs_check = Check(user=self.bob, name="Bob 1")
|
||||||
bobs_check.save()
|
bobs_check.save()
|
||||||
|
|
||||||
r = self.get("/api/v1/checks/", {"api_key": "abc"})
|
r = self.get()
|
||||||
|
|
||||||
data = r.json()
|
data = r.json()
|
||||||
self.assertEqual(len(data["checks"]), 2)
|
self.assertEqual(len(data["checks"]), 2)
|
||||||
for check in data["checks"]:
|
for check in data["checks"]:
|
||||||
self.assertNotEqual(check["name"], "Bob 1")
|
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")
|
||||||
|
@ -5,6 +5,6 @@ from hc.api import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
url(r'^ping/([\w-]+)/$', views.ping, name="hc-ping-slash"),
|
||||||
url(r'^ping/([\w-]+)$', views.ping, name="hc-ping"),
|
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"),
|
url(r'^badge/([\w-]+)/([\w-]{8})/([\w-]+).svg$', views.badge, name="hc-badge"),
|
||||||
]
|
]
|
||||||
|
@ -48,7 +48,7 @@ def ping(request, code):
|
|||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@check_api_key
|
@check_api_key
|
||||||
@validate_json(schemas.check)
|
@validate_json(schemas.check)
|
||||||
def create_check(request):
|
def checks(request):
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
code = 200
|
code = 200
|
||||||
response = {
|
response = {
|
||||||
|
@ -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 <a href="{% url 'hc-profile' %}">Settings</a> page.
|
an API key. You can create one in the <a href="{% url 'hc-profile' %}">Settings</a> page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>The client can authenticate itself by sending an appropriate HTTP
|
||||||
The API uses a simple authentication scheme: the API key should be
|
request header. The header's name should be <code>X-Api-Key</code> and
|
||||||
included in the request body (a JSON document) along other fields.
|
its value should be your API key.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p> Alternatively, for POST requests with a JSON request body,
|
||||||
|
the client can include an <code>api_key</code> field in the JSON document.
|
||||||
|
See below the "Create a check" section for an example.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="rule">API Requests</h2>
|
<h2 class="rule">API Requests</h2>
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
<div class="highlight"><pre><span></span>curl {{ SITE_ROOT }}/api/v1/checks/ <span class="se">\</span>
|
<div class="highlight"><pre><span></span>curl {{ SITE_ROOT }}/api/v1/checks/ <span class="se">\</span>
|
||||||
-X POST <span class="se">\</span>
|
--header <span class="s2">"X-Api-Key: your-api-key"</span> <span class="se">\</span>
|
||||||
-d <span class="s1">'{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'</span>
|
--data <span class="s1">'{"name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'</span>
|
||||||
|
|
||||||
|
<span class="c1"># Or, alternatively:</span>
|
||||||
|
curl {{ SITE_ROOT }}/api/v1/checks/ <span class="se">\</span>
|
||||||
|
--data <span class="s1">'{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'</span>
|
||||||
</pre></div>
|
</pre></div>
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
curl SITE_ROOT/api/v1/checks/ \
|
curl SITE_ROOT/api/v1/checks/ \
|
||||||
-X POST \
|
--header "X-Api-Key: your-api-key" \
|
||||||
-d '{"api_key": "your-api-key", "name": "Backups", "tags": "prod www", "timeout": 3600, "grace": 60}'
|
--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}'
|
||||||
|
@ -1,4 +1,2 @@
|
|||||||
<div class="highlight"><pre><span></span>curl {{ SITE_ROOT }}/api/v1/checks/ <span class="se">\</span>
|
<div class="highlight"><pre><span></span>curl --header <span class="s2">"X-Api-Key: your-api-key"</span> {{ SITE_ROOT }}/api/v1/checks/
|
||||||
-X GET <span class="se">\</span>
|
|
||||||
-d <span class="s1">'{"api_key": "your-api-key"}'</span>
|
|
||||||
</pre></div>
|
</pre></div>
|
||||||
|
@ -1,3 +1 @@
|
|||||||
curl SITE_ROOT/api/v1/checks/ \
|
curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/
|
||||||
-X GET \
|
|
||||||
-d '{"api_key": "your-api-key"}'
|
|
Loading…
x
Reference in New Issue
Block a user