Changelog

1.5.0

release-date:2016-10-20 11:08 A.M PDT
release-by:Ask Solem
  • New API for ModelEvent.

    After having used Thorn for a while, there was a realization that passing lots of arguments to a decorator looks very messy when there are many events for a model.

    We have come up with a new way to add webhooks to models, that we believe is more tidy.

    The new API moves declaration of webhooks related things into a nested class:

    @webhook_model
    class Article(models.Model):
        uuid = models.UUIDField()
        title = models.CharField(max_length=128)
        state = models.CharField(max_length=128, default='PENDING')
        body = models.TextField()
        user = models.ForeignKey(settings.AUTH_USER_MODEL)
    
        class webhooks:
            on_create = ModelEvent('article.created')
            on_change = ModelEvent('article.changed')
            on_delete = ModelEvent('article.removed')
            on_publish = ModelEvent(
                'article.published', state__now_eq='PUBLISHED',
            ).dispatches_on_change()
    
            def payload(self, article):
                return {
                    'title': article.title,
                }
    
            def headers(self, article):
                return {
                    'Authorization':
                        'Bearer {}'.format(article.user.access_token),
                }
    

    Note

    The old API is still supported, and there’s no current plans of deprecating the arguments to the decorator itself.

  • Adds support for buffering events - moving dispatch out of signal handlers.

    See Buffering for more information.

  • Models can now define additional headers to be passed on to webhooks.

    See Modifying event headers.

    Contributed by Flavio Curella.

  • Django: ModelEvent now takes advantage of Model.get_absolute_url().

    Instead of defining a reverser you can now simply have your model define a get_absolute_url method (which is a convention already):

    class Article(models.Model):
    
        @models.permalink
        def get_absolute_url(self):
            return 'article:detail', (), {'slug': self.slug}
    

    For more information on defining this method, refer to the Django documentation: https://docs.djangoproject.com/en/stable/ref/models/instances/#get-absolute-url

  • New THORN_SIGNAL_HONORS_TRANSACTION setting.

    If enabled the Django model events will dispatch only after the transaction is committed, and should the transaction be rolled back the events are discarded.

    You can also enable this setting on individual events:

    ModelEvent(..., signal_honors_transaction=True)
    

    Disabled by default.

    To be enabled by default in Thorn 2.0.

  • ModelEvent now logs errors instead of propagating them.

    ModelEvent(propagate_errors=True) may be set to revert to the old behavior.

  • URLs now ordered by scheme,host,port (was host,port,scheme).

  • Documentation improvements by:

    • Flavio Curella

1.4.2

release-date:2016-07-28 11:45 A.M PDT
  • Serialize uuid as string when calling Celery tasks.

1.4.1

release-date:2016-07-26 01:06 P.M PDT
release-by:Ask Solem
  • Fixed webhook dispatch crash where validator was deserialized twice.
  • Celery dispatcher did not properly forward the Hook-Subscription HTTP header.
  • Source code now using Google-style docstrings.

1.4.0

release-date:2016-07-11 04:27 P.M PDT
release-by:Ask Solem
  • New HTTP header now sent with events: Hook-Subscription

    This contains the UUID of the subscription, which means a webhook consumer may now react by cancelling or modifying the subscription.

  • Fixed missing default value for the THORN_SUBSCRIBER_MODEL setting.

  • Fixed HMAC signing wrong message value.

1.3.0

release-date:2016-07-07 07:40 P.M PDT
release-by:Ask Solem
  • New and improved method for HMAC signing.

    The new method must be enabled manually by setting:

    THORN_HMAC_SIGNER = 'thorn.utils.hmac:sign'
    

    It turns out itsdangerous did not do what we expected it to, instead it does this:

    • The secret key is transformed into:

       key = hashlib.sha256(salt + 'signer' + HMAC_SECRET)
      # strip = from beginning and end of the base64 string
      key = key.strip('=')
      
    • If you don’t specify a salt, which we don’t, there is a default salt(!) which is:

      “itsdangerous.Signer”
      
    • The extra “signer” in the key transformation is there as the default key_derivation method is called “django-concat”.

    • The final signature is encoded using “urlsafe_b64encode”

      So in Python to recreate the signature using the built-in hmac library you would have to do:

      import hashlib
      import hmac
      from base64 import urlsafe_b64encode
      
      # everything hardcoded to SHA256 here
      
      def create_signature(secret_key, message):
          key = hashlib.sha256(
              'itsdangerous.Signer' + 'signer' + secret_key).digest()
          digest = hmac.new(key, message, digestmod=hashlib.sha256).digest()
          return urlsafe_b64encode(digest).replace('=')
      

      which is much more complicated than what we can expect of users.

    You’re highly encouraged to enable the new HMAC method, but sadly it’s not backwards compatible.

    We have also included new examples for verifying HMAC signatures in Django, Ruby, and PHP in the documentation.

  • New THORN_SUBSCRIBER_MODEL setting.

  • New THORN_HMAC_SIGNER setting.

  • Requirements: Tests now depends on case 1.2.2

  • JSON: Make sure simplejson does not convert Decimal to float.

  • class:~thorn.events.ModelEvent: name can now be a string format.

    Contributed by Flavio Curella.

    The format expands using the model instance affected, e.g:

    on_created=ModelEvent('created.{.occasion}')
    

    means the format will expand into instance.occasion.

    Subclasses of ModelEvent can override how the name is expanded by defining the _get_name method.

1.2.1

release-date:2016-06-06 06:30 P.M PDT
release-by:Ask Solem
  • Celery: Forward event signal context to the tasks.

1.2.0

release-date:2016-06-02 01:00 P.M PDT
release-by:Ask Solem
  • Event: Adds request_data option.

    This enables you to inject additional data into the webhook payload, used for integration with quirky HTTP endpoints.

  • Event: Adds allow_keepalive option.

    HTTP connections will not be reused for an event if this flag is set to False. Keepalive is enabled by default.

  • Event: Adds subscribers argument that can be used to add default subscribers for the event.

    This argument can hold the same values as the THORN_SUBSCRIBERS setting.

  • Decorator: model.webhook_events is now a UserDict proxy

    to model.webhook_events.events.

  • Subscriber: thorn.generic.models.AbstractSubscribers is a new abstract interface for subscriber models.

    This should be used if you want to check if an object is a subscriber in isinstance() checks.

  • Q: now__* operators now properly handles the case when there’s

    no previous version of the object.

  • Django: django.db.models.signals.pre_save signal handler now ignores ObjectDoesNotExist errors.

  • Events: Adds new prepare_recipient_validators method, enabling subclasses to e.g. set default validators.

  • Windows: Unit test suite now passing on win32/win64.

  • Module thorn.models renamed to thorn.generic.models.

1.1.0

release-date:2016-05-23 12:00 P.M PDT
release-by:Ask Solem
  • Fixed installation on Python 3

    Fix contributed by Josh Drake.

  • Now depends on

  • Security: Now provides HMAC signing by default.

    The Subscriber model has a new hmac_secret field which subscribers can provide to set the secret key for communication. A default secret will be created if none is provided, and can be found in the response of the subscribe endpoint.

    The signed HMAC message found in the Hook-HMAC HTTP header can then be used to verify the sender of the webhook.

    An example Django webhook consumer verifying the signature can be found in the Django guide.

    Thanks to Timothy Fitz for suggestions.

  • Security: No longer dispatches webhooks to internal networks.

    This means Thorn will refuse to deliver webhooks to networks considered internal, like fd00::/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and 127.0.0.1

    This behavior can be changed globally using the THORN_RECIPIENT_VALIDATORS setting, or on an per-event basis using the recipient_validators argument to event.

  • Security: Now only dispatches to HTTP and HTTPS URLs by default.

    This behavior can be changed globally using the THORN_RECIPIENT_VALIDATORS setting, or on an per-event basis using the recipient_validators argument to event.

  • Security: Now only dispatches to ports 80 and 443 by default.

    This behavior can be changed globally using the THORN_RECIPIENT_VALIDATORS setting, or on an per-event basis using the recipient_validators argument to event.

  • Security: Adds recipient validators

    You can now validate the recipient URL by providing a list of validators in the recipient_validators argument to Event.

    The default list of validators is provided by the new THORN_RECIPIENT_VALIDATORS setting.

    Thanks to Edmond Wong for reviewing, and Timothy Fitz for suggestions.

  • Django: Now properly supports custom user models by using UserModel.get_username().

    Fix contributed by Josh Drake.

  • ModelEvent: Adds new many-to-many signal dispatcher types

    • dispatches_on_m2m_add(related_field)

      Sent when a new object is added to a many-to-many relation.

    • dispatches_on_m2m_remove(related_field)

      Sent when an object is removed from a many-to-many relation.

    • dispatches_on_m2m_clear(related_field)

      Sent when a many-to-many relation is cleared.

    Example

    In this blog article model, events are sent whenever a new tag is added or removed:

    @webhook_model(
        on_add_tag=ModelEvent(
            'article.tagged').dispatches_on_m2m_add('tags'),
        on_remove_tag=ModelEvent(
            'article.untagged').dispatches_on_m2m_remove('tags'),
        on_clear_tags=ModelEvent(
            'article.tags_cleared').dispatches_on_m2m_clear('tags'),
    )
    class Article(models.Model):
        title = models.CharField(max_length=128)
        tags = models.ManyToManyField(Tag)
    
    
    class Tag(models.Model):
        name = models.CharField(max_length=64, unique=True)
    

    The article.tagged webhook is sent when:

    >>> python_tag, _ = Tag.objects.get_or_create(name='python')
    >>> article.tags.add(python_tag)  # <-- dispatches with this line
    

    and the article.untagged webhook is sent when:

    >>> article.tags.remove(python_tag)
    

    finally, the article.tags_cleared event is sent when:

    >>> article.tags.clear()
    
  • Documentation fixes contributed by:

    • Matthew Brener

1.0.0

release-date:2016-05-13 10:10 A.M PDT
release-by:Ask Solem
  • Initial release :o)