models.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. __package__ = 'archivebox.api'
  2. import secrets
  3. from datetime import timedelta
  4. from django.conf import settings
  5. from django.db import models
  6. from django.utils import timezone
  7. from signal_webhooks.models import WebhookBase
  8. from django_stubs_ext.db.models import TypedModelMeta
  9. from abid_utils.models import ABIDModel, ABIDField, AutoDateTimeField
  10. def generate_secret_token() -> str:
  11. # returns cryptographically secure string with len() == 32
  12. return secrets.token_hex(16)
  13. class APIToken(ABIDModel):
  14. """
  15. A secret key generated by a User that's used to authenticate REST API requests to ArchiveBox.
  16. """
  17. # ABID: apt_<created_ts>_<token_hash>_<user_id_hash>_<uuid_rand>
  18. abid_prefix = 'apt_'
  19. abid_ts_src = 'self.created_at'
  20. abid_uri_src = 'self.created_by_id'
  21. abid_subtype_src = '"01"'
  22. abid_rand_src = 'self.id'
  23. abid_drift_allowed = True
  24. id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
  25. abid = ABIDField(prefix=abid_prefix)
  26. created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=None, null=False)
  27. created_at = AutoDateTimeField(default=None, null=False, db_index=True)
  28. modified_at = models.DateTimeField(auto_now=True)
  29. token = models.CharField(max_length=32, default=generate_secret_token, unique=True)
  30. expires = models.DateTimeField(null=True, blank=True)
  31. class Meta(TypedModelMeta):
  32. verbose_name = "API Key"
  33. verbose_name_plural = "API Keys"
  34. def __str__(self) -> str:
  35. return self.token
  36. def __repr__(self) -> str:
  37. return f'<APIToken user={self.created_by.username} token={self.token_redacted}>'
  38. def __json__(self) -> dict:
  39. return {
  40. "TYPE": "APIToken",
  41. "id": str(self.pk),
  42. "abid": str(self.ABID),
  43. "created_by_id": str(self.created_by_id),
  44. "token": self.token,
  45. "created_at": self.created_at.isoformat(),
  46. "expires": self.expires_as_iso8601,
  47. }
  48. @property
  49. def expires_as_iso8601(self):
  50. """Returns the expiry date of the token in ISO 8601 format or a date 100 years in the future if none."""
  51. expiry_date = self.expires or (timezone.now() + timedelta(days=365 * 100))
  52. return expiry_date.isoformat()
  53. @property
  54. def token_redacted(self):
  55. return f'************{self.token[-4:]}'
  56. def is_valid(self, for_date=None):
  57. for_date = for_date or timezone.now()
  58. if self.expires and self.expires < for_date:
  59. return False
  60. return True
  61. # monkey patch django-signals-webhooks to change how it shows up in Admin UI
  62. class OutboundWebhook(ABIDModel, WebhookBase):
  63. """
  64. Model used in place of (extending) signals_webhooks.models.WebhookModel. Swapped using:
  65. settings.SIGNAL_WEBHOOKS_CUSTOM_MODEL = 'api.models.OutboundWebhook'
  66. """
  67. abid_prefix = 'whk_'
  68. abid_ts_src = 'self.created_at'
  69. abid_uri_src = 'self.endpoint'
  70. abid_subtype_src = 'self.ref'
  71. abid_rand_src = 'self.id'
  72. abid_drift_allowed = True
  73. id = models.UUIDField(primary_key=True, default=None, null=False, editable=False, unique=True, verbose_name='ID')
  74. abid = ABIDField(prefix=abid_prefix)
  75. created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=None, null=False)
  76. created_at = AutoDateTimeField(default=None, null=False, db_index=True)
  77. modified_at = models.DateTimeField(auto_now=True)
  78. # More fields here: WebhookBase...
  79. WebhookBase._meta.get_field('name').help_text = (
  80. 'Give your webhook a descriptive name (e.g. Notify ACME Slack channel of any new ArchiveResults).')
  81. WebhookBase._meta.get_field('signal').help_text = (
  82. 'The type of event the webhook should fire for (e.g. Create, Update, Delete).')
  83. WebhookBase._meta.get_field('ref').help_text = (
  84. 'Dot import notation of the model the webhook should fire for (e.g. core.models.Snapshot or core.models.ArchiveResult).')
  85. WebhookBase._meta.get_field('endpoint').help_text = (
  86. 'External URL to POST the webhook notification to (e.g. https://someapp.example.com/webhook/some-webhook-receiver).')
  87. class Meta(WebhookBase.Meta):
  88. verbose_name = 'API Outbound Webhook'
  89. def __str__(self) -> str:
  90. return f'[{self.abid}] {self.ref} -> {self.endpoint}'