Delivery Security Checks

In Configured Global, Transient Webhook Subscriptions, we discussed how to use ZCML to register for webhook deliveries when certain events on certain resources happen. Conspicuously absent from that discussion was any mention of two of the more interesting and important attributes of the ZCML directive: owner and permission. This document will fill in those details.

Owners

Owners, for both static subscriptions and other types of subscriptions, are strings naming a IPrincipal as defined in zope.security.interfaces. At runtime, the string is turned into an IPrincipal object using zope.authentication.interfaces.IAuthentication.getPrincipal() from the IAuthentication utility closest to the object of the event (i.e., the for part of the directive).

Note

IAuthentication utilities are usually arranged into a hierarchy related to the object hierarchy, and principals may exist in some parts of the tree and not in others. (While steps such as prefixing the principal ID are usually taken to avoid having the same principal ID be valid in two different parts of the site hierarchy but mean two completely different principals, that is a valid possibility.)

If no principal can be found, the unauthenticated (anonymous) principal will be used instead; depending on the security policy and permission structure in use, the subscription may or may not be applicable.

Permissions

Permissions are defined by IPermission, but the important part is how they are checked.

The ISecurityPolicy is used to produce an IInteraction object. The IInteraction is then asked to confirm the permission using its checkPermission(permission, object) method. Normally, the security policy is a global object, and there can only be one active interaction at a time, but temporary, sub-interactions are possible. This module follows that model.

Important

When creating the temporary interactions to check permissions, this module will use the global security policy to create an interaction containing two participations: one for the principal found relating to the object, and one for the principal that was previously participating, if there was one. In this way, the permission must be allowed to both the initiator of the action (the logged in user) as well as the owner of the subscription.

Example

Lets put these pieces together and check how security applies.

We’ll begin by defining the same subscription we used previously, but we’ll establish a security policy, and require a permission and owner for the subscription. We’ll use zope.principalregistry to provide a global IAuthentication utility; this also provides a global IUnauthenticatedPrincipal that is used if the owner cannot be found at runtime:

>>> from zope.configuration import xmlconfig
>>> conf_context = xmlconfig.string("""
... <configure
...     xmlns="http://namespaces.zope.org/zope"
...     xmlns:webhooks="http://nextthought.com/ntp/webhooks"
...     >
...   <include package="zope.component" />
...   <include package="zope.container" />
...   <include package="zope.principalregistry" />
...   <include package="zope.securitypolicy" />
...   <include package="zope.securitypolicy" file="securitypolicy.zcml" />
...   <include package="nti.webhooks" />
...   <include package="nti.webhooks" file="subscribers_promiscuous.zcml" />
...   <webhooks:staticSubscription
...             to="https://this_domain_does_not_exist"
...             for="zope.container.interfaces.IContentContainer"
...             when="zope.lifecycleevent.interfaces.IObjectCreatedEvent"
...             permission="zope.View"
...             owner="some.one" />
... </configure>
... """)

Next, we can find the active subscription, just as before:

>>> from nti.webhooks.subscribers import find_active_subscriptions_for
>>> from zope.container.folder import Folder
>>> from zope.lifecycleevent import ObjectCreatedEvent
>>> event = ObjectCreatedEvent(Folder())
>>> len(find_active_subscriptions_for(event.object, event))
1
>>> find_active_subscriptions_for(event.object, event)
[<...Subscription ... to='https://this_domain_does_not_exist' for=IContentContainer when=IObjectCreatedEvent>]

Subscription Is Not Applicable By Default

Next, we need to know if the subscription is applicable to the data. Unlike before, since we have security constraints in place, the subscription is not applicable:

>>> subscriptions = find_active_subscriptions_for(event.object, event)
>>> [subscription.isApplicable(event.object) for subscription in subscriptions]
[True]

Wait, wait…what happened there? It turns out that since we don’t have any defined principal identified by some.one, we use the global IUnauthenticatedPrincipal, an anonymous user. In turn, the directives executed by loading securitypolicy.zcml from zope.securitypolicy give anonymous users the zope.View permission by default. Let’s reverse that and check again.

>>> from zope.securitypolicy.rolepermission import rolePermissionManager
>>> rolePermissionManager.denyPermissionToRole('zope.View', 'zope.Anonymous')
>>> [subscription.isApplicable(event.object) for subscription in subscriptions]
[False]

Ahh, that’s better. We could have also disabled that behaviour.

Note

Dynamic (persistent) subscriptions do not fallback to the unauthenticated principal by default.

>>> subscriptions[0].fallback_to_unauthenticated_principal
True
>>> subscriptions[0].fallback_to_unauthenticated_principal = False

Subscription Applicable Once Principals are Defined

To grant access in an expected way, we’ll use zope.principalregistry to globally define the prinicpal we’re looking for, as well as globally grant that principal the permissions necessary:

>>> conf_context = xmlconfig.string("""
... <configure
...     xmlns="http://namespaces.zope.org/zope"
...     xmlns:webhooks="http://nextthought.com/ntp/webhooks"
...     >
...   <include package="zope.securitypolicy" file="meta.zcml" />
...   <include package="zope.principalregistry" file="meta.zcml" />
...   <principal
...         id="some.one"
...         title="Some One"
...         login="some.one"
...         password_manager="SHA1"
...         password="40bd001563085fc35165329ea1ff5c5ecbdbbeef"
...         />
...   <grant principal="some.one" permission="zope.View" />
... </configure>
... """)

Now our webhook is applicable:

>>> [subscription.isApplicable(event.object) for subscription in subscriptions]
[True]

Existing Interactions

If there was already an interaction going on (e.g., for the logged in user that created the object), the owner of the subscription is added to that interaction for purposes of checking permissions. Security policies generally only grant access if all participations in the interaction have access.

We’ll demonstrate this by creating and acting as a new principal and then checking access. Because our new user has no permissions on the object being created (which of course is highly unusual), the permission check will fail.

>>> conf_context = xmlconfig.string("""
... <configure
...     xmlns="http://namespaces.zope.org/zope"
...     xmlns:webhooks="http://nextthought.com/ntp/webhooks"
...     >
...   <include package="zope.principalregistry" file="meta.zcml" />
...   <principal
...         id="some.one.else"
...         title="Some One Else"
...         login="some.one.else"
...         password_manager="SHA1"
...         password="40bd001563085fc35165329ea1ff5c5ecbdbbeef"
...         />
... </configure>
... """)
>>> from zope.security.testing import interaction
>>> with interaction('some.one.else'):
...    [subscription.isApplicable(event.object) for subscription in subscriptions]
[False]

Automatic Deactivation On Failure

For any type of subscription (static, dynamic, persistent, …) that implements nti.webhooks.interfaces.ILimitedApplicabilityPreconditionFailureWebhookSubscription, attempting to make deliveries to it while it is misconfigured (e.g., there is no such permission defined or no principal can be found) will eventually result in it becoming automatically disabled.

Our subscription is such an object:

>>> from nti.webhooks.interfaces import ILimitedApplicabilityPreconditionFailureWebhookSubscription
>>> from zope.interface import verify
>>> subscription = subscriptions[0]
>>> verify.verifyObject(ILimitedApplicabilityPreconditionFailureWebhookSubscription, subscription)
True
>>> subscription.applicable_precondition_failure_limit
50
>>> subscription.active
True
>>> print(subscription.status_message)
Active

This doesn’t apply when the permission check is simply denied; that’s normal and expected.

>>> from delivery_helper import deliver_some
>>> from nti.webhooks.testing import wait_for_deliveries
>>> with interaction('some.one.else'):
...      deliver_some(100)
>>> len(subscription)
0
>>> subscription.active
True

But if we remove the principal our subscription is using (being careful not to fire any events that might automatically remove or deactivate the subscription) we can see that it becomes inactive at the correct time.

First, we’ll deliver one, just to prove it works.

>>> deliver_some()
>>> wait_for_deliveries()
>>> len(subscription)
1

Now we’ll destroy the principal registration, making this object incapable of accepting deliveries. (This works because we disabled the fallback to the unauthenticated principal earlier.)

>>> from zope.principalregistry import principalregistry
>>> principalregistry.principalRegistry._clear()

When we attempt enough of them, it is deactivated.

>>> deliver_some(49)
>>> len(subscription)
1
>>> subscription.active
True
>>> deliver_some(2)
>>> len(subscription)
1
>>> subscription.active
False
>>> print(subscription.status_message)
Delivery suspended due to too many precondition failures.

Manually activating the subscription resets the counter.

>>> subscription.__parent__.activateSubscription(subscription)
True
>>> subscription.active
True
>>> print(subscription.status_message)
Active
>>> deliver_some(50)
>>> subscription.active
False