From 76ae42bc8fe85ba8d12a24a842a08e17cf13682c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Caune?= Date: Tue, 24 Mar 2020 16:10:42 +0200 Subject: [PATCH] "Get a single check" API call now supports read-only API keys. Fixes #346 --- CHANGELOG.md | 5 ++ hc/api/tests/test_create_check.py | 2 +- hc/api/tests/test_get_check.py | 14 ++- hc/api/tests/test_update_check.py | 7 ++ hc/api/views.py | 55 ++++++++---- templates/docs/api.html | 133 +++++++++++++++++------------ templates/docs/api.md | 137 ++++++++++++++++++------------ 7 files changed, 224 insertions(+), 129 deletions(-) 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" } ```