diff --git a/CHANGELOG.md b/CHANGELOG.md index d95f72cb..41e0d8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog All notable changes to this project will be documented in this file. +## v1.15.0-dev - Unreleased + +### Bug Fixes +- "Get a single check" API call now supports read-only API keys (#346) + ## v1.14.0 - 2020-03-23 ### Improvements diff --git a/hc/api/tests/test_create_check.py b/hc/api/tests/test_create_check.py index 9f8e3d96..bc04f68c 100644 --- a/hc/api/tests/test_create_check.py +++ b/hc/api/tests/test_create_check.py @@ -219,7 +219,7 @@ class CreateCheckTestCase(BaseTestCase): r = self.post({"api_key": "X" * 32}) self.assertEqual(r.status_code, 403) - def test_readonly_key_does_not_work(self): + def test_it_rejects_readonly_key(self): self.project.api_key_readonly = "R" * 32 self.project.save() diff --git a/hc/api/tests/test_get_check.py b/hc/api/tests/test_get_check.py index b3f916ec..f4ae91c8 100644 --- a/hc/api/tests/test_get_check.py +++ b/hc/api/tests/test_get_check.py @@ -23,9 +23,9 @@ class GetCheckTestCase(BaseTestCase): self.c1 = Channel.objects.create(project=self.project) self.a1.channel_set.add(self.c1) - def get(self, code): + def get(self, code, api_key="X" * 32): url = "/api/v1/checks/%s" % code - return self.client.get(url, HTTP_X_API_KEY="X" * 32) + return self.client.get(url, HTTP_X_API_KEY=api_key) def test_it_works(self): r = self.get(self.a1.code) @@ -52,3 +52,13 @@ class GetCheckTestCase(BaseTestCase): made_up_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02" r = self.get(made_up_code) self.assertEqual(r.status_code, 404) + + def test_readonly_key_works(self): + self.project.api_key_readonly = "R" * 32 + self.project.save() + + r = self.get(self.a1.code, api_key=self.project.api_key_readonly) + self.assertEqual(r.status_code, 200) + + # When using readonly keys, the ping URLs should not be exposed: + self.assertNotContains(r, self.a1.url()) diff --git a/hc/api/tests/test_update_check.py b/hc/api/tests/test_update_check.py index 7c9225b6..c18eb54f 100644 --- a/hc/api/tests/test_update_check.py +++ b/hc/api/tests/test_update_check.py @@ -218,3 +218,10 @@ class UpdateCheckTestCase(BaseTestCase): # Schedule should be unchanged self.check.refresh_from_db() self.assertEqual(self.check.schedule, "5 * * * *") + + def test_it_rejects_readonly_key(self): + self.project.api_key_readonly = "R" * 32 + self.project.save() + + r = self.post(self.check.code, {"api_key": "R" * 32, "name": "Foo"}) + self.assertEqual(r.status_code, 401) diff --git a/hc/api/views.py b/hc/api/views.py index f0225702..fed66d6c 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -178,34 +178,53 @@ def channels(request): return JsonResponse({"channels": channels}) -@csrf_exempt -@cors("POST", "DELETE", "GET") @validate_json() +@authorize_read +def get_check(request, code): + check = get_object_or_404(Check, code=code) + if check.project != request.project: + return HttpResponseForbidden() + + return JsonResponse(check.to_dict(readonly=request.readonly)) + + +@validate_json(schemas.check) @authorize -def single(request, code): +def update_check(request, code): check = get_object_or_404(Check, code=code) if check.project != request.project: return HttpResponseForbidden() - if request.method == "POST": - try: - validate(request.json, schemas.check) - _update(check, request.json) - except (BadChannelException, ValidationError) as e: - return JsonResponse({"error": str(e)}, status=400) + try: + _update(check, request.json) + except BadChannelException as e: + return JsonResponse({"error": str(e)}, status=400) + + return JsonResponse(check.to_dict()) - return JsonResponse(check.to_dict()) - elif request.method == "DELETE": - response = check.to_dict() - check.delete() - return JsonResponse(response) +@validate_json() +@authorize +def delete_check(request, code): + check = get_object_or_404(Check, code=code) + if check.project != request.project: + return HttpResponseForbidden() + + response = check.to_dict() + check.delete() + return JsonResponse(response) + + +@csrf_exempt +@cors("POST", "DELETE", "GET") +def single(request, code): + if request.method == "POST": + return update_check(request, code) - elif request.method == "GET": - return JsonResponse(check.to_dict()) + if request.method == "DELETE": + return delete_check(request, code) - # Otherwise, method not allowed - return HttpResponse(status=405) + return get_check(request, code) @cors("POST") diff --git a/templates/docs/api.html b/templates/docs/api.html index 1ce17fe5..8a330021 100644 --- a/templates/docs/api.html +++ b/templates/docs/api.html @@ -109,35 +109,35 @@ specified value.
{
"checks": [
{
- "channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc",
- "desc": "Longer free-form description goes here",
- "grace": 900,
- "last_ping": "2017-01-04T13:24:39.903464+00:00",
+ "name": "Filesystem Backup",
+ "tags": "backup fs",
+ "desc": "Runs incremental backup every hour",
+ "grace": 600,
"n_pings": 1,
- "name": "Api test 1",
- "next_ping": "2017-01-04T14:24:39.903464+00:00",
- "pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
- "ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6",
"status": "up",
- "tags": "foo",
- "timeout": 3600,
- "update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"
+ "last_ping": "2020-03-24T14:02:03+00:00",
+ "next_ping": "2020-03-24T15:02:03+00:00",
+ "ping_url": "PING_ENDPOINT31365bce-8da9-4729-8ff3-aaa71d56b712",
+ "update_url": "SITE_ROOT/api/v1/checks/31365bce-8da9-4729-8ff3-aaa71d56b712",
+ "pause_url": "SITE_ROOT/api/v1/checks/31365bce-8da9-4729-8ff3-aaa71d56b712/pause",
+ "channels": "1bdea468-03bf-47b8-ab27-29a9dd0e4b94,51c6eb2b-2ae1-456b-99fe-6f1e0a36cd3c",
+ "timeout": 3600
},
{
- "channels": "",
- "desc": "",
- "grace": 3600,
- "last_ping": null,
- "n_pings": 0,
- "name": "Api test 2",
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
"next_ping": null,
- "pause_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause",
- "ping_url": "PING_ENDPOINT9d17c61f-5c4f-4cab-b517-11e6b2679ced",
- "schedule": "0/10 * * * *",
- "status": "new",
- "tags": "bar baz",
- "tz": "UTC",
- "update_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced"
+ "ping_url": "PING_ENDPOINT803f680d-e89b-492b-82ef-2be7b774a92d",
+ "update_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d",
+ "pause_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d/pause",
+ "channels": "1bdea468-03bf-47b8-ab27-29a9dd0e4b94,51c6eb2b-2ae1-456b-99fe-6f1e0a36cd3c",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
}
]
}
@@ -150,28 +150,29 @@ is added. This identifier is stable across API calls. Example:
{
"checks": [
{
- "desc": "Longer free-form description goes here",
- "grace": 900,
- "last_ping": "2017-01-04T13:24:39.903464+00:00",
+ "name": "Filesystem Backup",
+ "tags": "backup fs",
+ "desc": "Runs incremental backup every hour",
+ "grace": 600,
"n_pings": 1,
- "name": "Api test 1",
"status": "up",
- "tags": "foo",
- "timeout": 3600,
- "unique_key": "2872190d95224bad120f41d3c06aab94b8175bb6"
+ "last_ping": "2020-03-24T14:02:03+00:00",
+ "next_ping": "2020-03-24T15:02:03+00:00",
+ "unique_key": "a6c7b0a8a66bed0df66abfdab3c77736861703ee",
+ "timeout": 3600
},
{
- "desc": "",
- "grace": 3600,
- "last_ping": null,
- "n_pings": 0,
- "name": "Api test 2",
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
"next_ping": null,
- "schedule": "0/10 * * * *",
- "status": "new",
- "tags": "bar baz",
- "tz": "UTC",
- "unique_key": "9b5fc29129560ff2c5c1803803a7415e4f80cf7e"
+ "unique_key": "124f983e0e3dcaeba921cfcef46efd084576e783",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
}
]
}
@@ -199,19 +200,43 @@ is added. This identifier is stable across API calls. Example:
Example Response
{
- "channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc",
- "desc": "Longer free-form description goes here",
- "grace": 900,
- "last_ping": "2017-01-04T13:24:39.903464+00:00",
- "n_pings": 1,
- "name": "Api test 1",
- "next_ping": "2017-01-04T14:24:39.903464+00:00",
- "pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
- "ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6",
- "status": "up",
- "tags": "foo",
- "timeout": 3600,
- "update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
+ "next_ping": null,
+ "ping_url": "PING_ENDPOINT803f680d-e89b-492b-82ef-2be7b774a92d",
+ "update_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d",
+ "pause_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d/pause",
+ "channels": "1bdea468-03bf-47b8-ab27-29a9dd0e4b94,51c6eb2b-2ae1-456b-99fe-6f1e0a36cd3c",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
+}
+
+
+
+Example Read-Only Response
+When using the read-only API key, the following fields are omitted:
+ping_url
, update_url
, pause_url
, channels
. An extra unique_key
field is
+added. This identifier is stable across API calls.
+Note: the ping_url
, update_url
and pause_url
fields, although omitted, are not
+really secret. The client already knows the check's unique UUID and so can easily
+construct these URLs by itself.
+{
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
+ "next_ping": null,
+ "unique_key": "124f983e0e3dcaeba921cfcef46efd084576e783",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
}
diff --git a/templates/docs/api.md b/templates/docs/api.md
index 8c3a1e47..d1eed45a 100644
--- a/templates/docs/api.md
+++ b/templates/docs/api.md
@@ -89,35 +89,35 @@ curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/
{
"checks": [
{
- "channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc",
- "desc": "Longer free-form description goes here",
- "grace": 900,
- "last_ping": "2017-01-04T13:24:39.903464+00:00",
+ "name": "Filesystem Backup",
+ "tags": "backup fs",
+ "desc": "Runs incremental backup every hour",
+ "grace": 600,
"n_pings": 1,
- "name": "Api test 1",
- "next_ping": "2017-01-04T14:24:39.903464+00:00",
- "pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
- "ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6",
"status": "up",
- "tags": "foo",
- "timeout": 3600,
- "update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"
+ "last_ping": "2020-03-24T14:02:03+00:00",
+ "next_ping": "2020-03-24T15:02:03+00:00",
+ "ping_url": "PING_ENDPOINT31365bce-8da9-4729-8ff3-aaa71d56b712",
+ "update_url": "SITE_ROOT/api/v1/checks/31365bce-8da9-4729-8ff3-aaa71d56b712",
+ "pause_url": "SITE_ROOT/api/v1/checks/31365bce-8da9-4729-8ff3-aaa71d56b712/pause",
+ "channels": "1bdea468-03bf-47b8-ab27-29a9dd0e4b94,51c6eb2b-2ae1-456b-99fe-6f1e0a36cd3c",
+ "timeout": 3600
},
{
- "channels": "",
- "desc": "",
- "grace": 3600,
- "last_ping": null,
- "n_pings": 0,
- "name": "Api test 2",
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
"next_ping": null,
- "pause_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced/pause",
- "ping_url": "PING_ENDPOINT9d17c61f-5c4f-4cab-b517-11e6b2679ced",
- "schedule": "0/10 * * * *",
- "status": "new",
- "tags": "bar baz",
- "tz": "UTC",
- "update_url": "SITE_ROOT/api/v1/checks/9d17c61f-5c4f-4cab-b517-11e6b2679ced"
+ "ping_url": "PING_ENDPOINT803f680d-e89b-492b-82ef-2be7b774a92d",
+ "update_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d",
+ "pause_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d/pause",
+ "channels": "1bdea468-03bf-47b8-ab27-29a9dd0e4b94,51c6eb2b-2ae1-456b-99fe-6f1e0a36cd3c",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
}
]
}
@@ -131,28 +131,29 @@ is added. This identifier is stable across API calls. Example:
{
"checks": [
{
- "desc": "Longer free-form description goes here",
- "grace": 900,
- "last_ping": "2017-01-04T13:24:39.903464+00:00",
+ "name": "Filesystem Backup",
+ "tags": "backup fs",
+ "desc": "Runs incremental backup every hour",
+ "grace": 600,
"n_pings": 1,
- "name": "Api test 1",
"status": "up",
- "tags": "foo",
- "timeout": 3600,
- "unique_key": "2872190d95224bad120f41d3c06aab94b8175bb6"
+ "last_ping": "2020-03-24T14:02:03+00:00",
+ "next_ping": "2020-03-24T15:02:03+00:00",
+ "unique_key": "a6c7b0a8a66bed0df66abfdab3c77736861703ee",
+ "timeout": 3600
},
{
- "desc": "",
- "grace": 3600,
- "last_ping": null,
- "n_pings": 0,
- "name": "Api test 2",
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
"next_ping": null,
- "schedule": "0/10 * * * *",
- "status": "new",
- "tags": "bar baz",
- "tz": "UTC",
- "unique_key": "9b5fc29129560ff2c5c1803803a7415e4f80cf7e"
+ "unique_key": "124f983e0e3dcaeba921cfcef46efd084576e783",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
}
]
}
@@ -188,19 +189,47 @@ curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/
```json
{
- "channels": "4ec5a071-2d08-4baa-898a-eb4eb3cd6941,746a083e-f542-4554-be1a-707ce16d3acc",
- "desc": "Longer free-form description goes here",
- "grace": 900,
- "last_ping": "2017-01-04T13:24:39.903464+00:00",
- "n_pings": 1,
- "name": "Api test 1",
- "next_ping": "2017-01-04T14:24:39.903464+00:00",
- "pause_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6/pause",
- "ping_url": "PING_ENDPOINT662ebe36-ecab-48db-afe3-e20029cb71e6",
- "status": "up",
- "tags": "foo",
- "timeout": 3600,
- "update_url": "SITE_ROOT/api/v1/checks/662ebe36-ecab-48db-afe3-e20029cb71e6"
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
+ "next_ping": null,
+ "ping_url": "PING_ENDPOINT803f680d-e89b-492b-82ef-2be7b774a92d",
+ "update_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d",
+ "pause_url": "SITE_ROOT/api/v1/checks/803f680d-e89b-492b-82ef-2be7b774a92d/pause",
+ "channels": "1bdea468-03bf-47b8-ab27-29a9dd0e4b94,51c6eb2b-2ae1-456b-99fe-6f1e0a36cd3c",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
+}
+```
+
+### Example Read-Only Response
+
+When using the read-only API key, the following fields are omitted:
+`ping_url`, `update_url`, `pause_url`, `channels`. An extra `unique_key` field is
+added. This identifier is stable across API calls.
+
+
+Note: the `ping_url`, `update_url` and `pause_url` fields, although omitted, are not
+really secret. The client already knows the check's unique UUID and so can easily
+construct these URLs by itself.
+
+```json
+{
+ "name": "Database Backup",
+ "tags": "production db",
+ "desc": "Runs ~/db-backup.sh",
+ "grace": 1200,
+ "n_pings": 7,
+ "status": "down",
+ "last_ping": "2020-03-23T10:19:32+00:00",
+ "next_ping": null,
+ "unique_key": "124f983e0e3dcaeba921cfcef46efd084576e783",
+ "schedule": "15 5 * * *",
+ "tz": "UTC"
}
```