diff --git a/CHANGELOG.md b/CHANGELOG.md index ca881c46..94d92c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes to this project will be documented in this file. - Add a "Transfer Ownership" feature in Project Settings - In checks list, the pause button asks for confirmation (#356) - Added /api/v1/metrics/ endpoint, useful for monitoring the service itself +- Migrated the `unique_key` field on a Check from a Python property to a database property +- Allowed the /api/v1/checks/ endpoint to receive a UUID or `unique_key` ### Bug Fixes - "Get a single check" API call now supports read-only API keys (#346) diff --git a/hc/api/migrations/0071_check_unique_key.py b/hc/api/migrations/0071_check_unique_key.py new file mode 100644 index 00000000..1eb956a4 --- /dev/null +++ b/hc/api/migrations/0071_check_unique_key.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2020-05-28 03:20 + +from django.db import migrations, models +from hc.api.models import Check + +def generate_unique_keys(apps, schema_editor): + for o in Check.objects.all(): + o.save() + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0070_auto_20200411_1310'), + ] + + operations = [ + migrations.AddField( + model_name='check', + name='unique_key', + field=models.CharField(blank=True, max_length=40), + ), + migrations.RunPython(generate_unique_keys), + ] diff --git a/hc/api/models.py b/hc/api/models.py index 7089b5da..9b1a51fa 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -84,6 +84,7 @@ class Check(models.Model): has_confirmation_link = models.BooleanField(default=False) alert_after = models.DateTimeField(null=True, blank=True, editable=False) status = models.CharField(max_length=6, choices=STATUSES, default="new") + unique_key = models.CharField(max_length=40, blank=True) class Meta: indexes = [ @@ -96,6 +97,12 @@ class Check(models.Model): ) ] + def save(self, *args, **kwargs): + if not self.unique_key: + code_half = self.code.hex[:16] + self.unique_key = hashlib.sha1(code_half.encode()).hexdigest() + super().save(*args,**kwargs) + def __str__(self): return "%s (%d)" % (self.name or self.code, self.id) @@ -200,10 +207,6 @@ class Check(models.Model): codes = self.channel_set.order_by("code").values_list("code", flat=True) return ",".join(map(str, codes)) - @property - def unique_key(self): - code_half = self.code.hex[:16] - return hashlib.sha1(code_half.encode()).hexdigest() def to_dict(self, readonly=False): diff --git a/hc/api/tests/test_get_check.py b/hc/api/tests/test_get_check.py index f4ae91c8..2c36b3af 100644 --- a/hc/api/tests/test_get_check.py +++ b/hc/api/tests/test_get_check.py @@ -53,6 +53,23 @@ class GetCheckTestCase(BaseTestCase): r = self.get(made_up_code) self.assertEqual(r.status_code, 404) + def test_it_handles_unique_key(self): + r = self.get(self.a1.unique_key) + self.assertEqual(r.status_code, 200) + self.assertEqual(r["Access-Control-Allow-Origin"], "*") + + doc = r.json() + self.assertEqual(len(doc), 13) + + self.assertEqual(doc["timeout"], 3600) + self.assertEqual(doc["grace"], 900) + self.assertEqual(doc["ping_url"], self.a1.url()) + self.assertEqual(doc["last_ping"], None) + self.assertEqual(doc["n_pings"], 0) + self.assertEqual(doc["status"], "new") + self.assertEqual(doc["channels"], str(self.c1.code)) + self.assertEqual(doc["desc"], "This is description") + def test_readonly_key_works(self): self.project.api_key_readonly = "R" * 32 self.project.save() diff --git a/hc/api/urls.py b/hc/api/urls.py index bdd72182..16c98ed4 100644 --- a/hc/api/urls.py +++ b/hc/api/urls.py @@ -13,8 +13,18 @@ class QuoteConverter: def to_url(self, value): return quote(value, safe="") +class UniqueKeyConverter: + regex = "[A-z0-9]{40}" + + def to_python(self, value): + return value + + def to_url(self, value): + return value + register_converter(QuoteConverter, "quoted") +register_converter(UniqueKeyConverter, "unique_key") urlpatterns = [ path("ping//", views.ping, name="hc-ping-slash"), @@ -23,6 +33,7 @@ urlpatterns = [ path("ping//start", views.ping, {"action": "start"}, name="hc-start"), path("api/v1/checks/", views.checks), path("api/v1/checks/", views.single, name="hc-api-single"), + path("api/v1/checks/", views.single, name="hc-api-single"), path("api/v1/checks//pause", views.pause, name="hc-api-pause"), path("api/v1/notifications//bounce", views.bounce, name="hc-api-bounce"), path("api/v1/channels/", views.channels), diff --git a/hc/api/views.py b/hc/api/views.py index 3cd684a1..2cbfaddf 100644 --- a/hc/api/views.py +++ b/hc/api/views.py @@ -181,7 +181,10 @@ def channels(request): @validate_json() @authorize_read def get_check(request, code): - check = get_object_or_404(Check, code=code) + if type(code) == str: + check = get_object_or_404(Check, unique_key=code) + else: + check = get_object_or_404(Check, code=code) if check.project != request.project: return HttpResponseForbidden() diff --git a/templates/docs/api.md b/templates/docs/api.md index 3d6d7bee..9d969e92 100644 --- a/templates/docs/api.md +++ b/templates/docs/api.md @@ -9,6 +9,7 @@ Endpoint Name | Endpoint Address ------------------------------------------------------|------- [Get a list of existing checks](#list-checks) | `GET SITE_ROOT/api/v1/checks/` [Get a single check](#get-check) | `GET SITE_ROOT/api/v1/checks/` +[Get a single check (using Read Only API)](#get-check) | `GET SITE_ROOT/api/v1/checks/` [Create a new check](#create-check) | `POST SITE_ROOT/api/v1/checks/` [Update an existing check](#update-check) | `POST SITE_ROOT/api/v1/checks/` [Pause monitoring of a check](#pause-check) | `POST SITE_ROOT/api/v1/checks//pause` @@ -125,7 +126,7 @@ curl --header "X-Api-Key: your-api-key" SITE_ROOT/api/v1/checks/ 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. Example: +is added which can be used [to `GET` a check](#get-check) in place of the `UUID`. The `unique_key` is stable across API calls. Example: ```json { @@ -160,9 +161,9 @@ is added. This identifier is stable across API calls. Example: ``` ## Get a Single Check {: #get-check .rule } -`GET SITE_ROOT/api/v1/checks/` +`GET SITE_ROOT/api/v1/checks/` OR `GET SITE_ROOT/api/v1/checks/` -Returns a JSON representation of a single check. +Returns a JSON representation of a single check. Can take either the UUID or the `unique_key` (see [information above](#list-checks)) as the identifier of the check to return. ### Response Codes