diff --git a/api/admin.py b/api/admin.py index 611bdba0..100dcd69 100644 --- a/api/admin.py +++ b/api/admin.py @@ -17,6 +17,15 @@ class ApiClientAdmin(admin.ModelAdmin): "owner__nick_name", ) autocomplete_fields = ("owner", "groups", "client_permissions") + readonly_fields = ("hmac_key",) + actions = ("reset_hmac_key",) + + @admin.action(permissions=["change"], description=_("Reset HMAC key")) + def reset_hmac_key(self, _request: HttpRequest, queryset: QuerySet[ApiClient]): + objs = list(queryset) + for obj in objs: + obj.reset_hmac(commit=False) + ApiClient.objects.bulk_update(objs, fields=["hmac_key"]) @admin.register(ApiKey) diff --git a/api/migrations/0002_apiclient_hmac_key.py b/api/migrations/0002_apiclient_hmac_key.py new file mode 100644 index 00000000..d0b3fad4 --- /dev/null +++ b/api/migrations/0002_apiclient_hmac_key.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.3 on 2025-10-26 10:15 + +from django.db import migrations, models + +import api.models + + +class Migration(migrations.Migration): + dependencies = [("api", "0001_initial")] + + operations = [ + migrations.AddField( + model_name="apiclient", + name="hmac_key", + field=models.CharField( + default=api.models.get_hmac_key, max_length=128, verbose_name="HMAC Key" + ), + ), + ] diff --git a/api/models.py b/api/models.py index 4c802b55..c0d9c291 100644 --- a/api/models.py +++ b/api/models.py @@ -1,3 +1,4 @@ +import secrets from typing import Iterable from django.contrib.auth.models import Permission @@ -10,6 +11,10 @@ from django.utils.translation import pgettext_lazy from core.models import Group, User +def get_hmac_key(): + return secrets.token_hex(64) + + class ApiClient(models.Model): name = models.CharField(_("name"), max_length=64) owner = models.ForeignKey( @@ -28,6 +33,7 @@ class ApiClient(models.Model): help_text=_("Specific permissions for this api client."), related_name="clients", ) + hmac_key = models.CharField(_("HMAC Key"), max_length=128, default=get_hmac_key) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -59,6 +65,13 @@ class ApiClient(models.Model): raise ValueError("perm_list must be an iterable of permissions.") return all(self.has_perm(perm) for perm in perm_list) + def reset_hmac(self, *, commit: bool = True) -> str: + """Reset and return the HMAC key for this client.""" + self.hmac_key = get_hmac_key() + if commit: + self.save() + return self.hmac_key + class ApiKey(models.Model): PREFIX_LENGTH = 5