Subscription Security¶
In Delivery Security Checks, we covered the checks that are applied when determining whether to deliver an event’s object to a particular subscription’s target.
This document will cover the security that’s applied to the subscriptions themselves.
Subscription security is based on zope.securitypolicy
, using
its concepts of principals, roles, permissions, and granting or
denying permissions to principals or roles,
Each subscription that has an owner grants read (zope.View
) and
delete (nti.actions.delete
) access to that owner. No other
permissions are defined or used; any other access must be inherited
from the subscription’s lineage. Typically this will mean that
principals with the role zope.Manager
will have complete access;
if you load the securitypolicy.zcml
configuration from
zope.securitypolicy
, then anonymous users will also have
access. If there is no owner, then no specific
grants will be made.
This applies equally for both static and dynamic subscriptions, and can be customized; see Customizing The Permission Grants for more information.
Set Up¶
The following documentation will use the basic set up presented here. We’ll begin by loading the necessary ZCML configuration, including the default Zope security policy (but we’ll disable the anonymous access for demonstration purposes).
>>> 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" />
... <!-- This defines permission nti.actions.delete and must be
... before zope.securitypolicy:securitypolicy.zcml
... for the zope.Manager role to be granted it. -->
... <include package="nti.webhooks" />
... <include package="zope.securitypolicy" file="securitypolicy.zcml" />
... <include package="nti.webhooks" file="subscribers_promiscuous.zcml" />
... <deny role="zope.Anonymous" permission="zope.View" />
... </configure>
... """)
Next, we’ll define a few principals (‘Alice’ and ‘Bob’), including
one (‘Manager’) that we grant the zope.Manager
role to.
>>> 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="webhook.alice"
... title="Alice"
... login="alice"
... password_manager="SHA1"
... password="40bd001563085fc35165329ea1ff5c5ecbdbbeef"
... />
... <principal
... id="webhook.bob"
... title="Bob"
... login="bob"
... password_manager="SHA1"
... password="40bd001563085fc35165329ea1ff5c5ecbdbbeef"
... />
... <principal
... id="webhook.manager"
... title="Manager"
... login="manager"
... password_manager="SHA1"
... password="40bd001563085fc35165329ea1ff5c5ecbdbbeef"
... />
... <grant role="zope.Manager" principal="webhook.manager" />
... </configure>
... """)
Lastly, we’ll define a subscription for Alice.
>>> conf_context = xmlconfig.string("""
... <configure
... xmlns="http://namespaces.zope.org/zope"
... xmlns:webhooks="http://nextthought.com/ntp/webhooks"
... >
... <include package="nti.webhooks" />
... <webhooks:staticSubscription
... to="https://example.com/some/path"
... for="zope.container.interfaces.IContentContainer"
... when="zope.lifecycleevent.interfaces.IObjectCreatedEvent"
... permission="zope.View"
... owner="webhook.alice" />
... </configure>
... """)
Access To The Subscription¶
Todo
Write a subscriber and example for when the owner_id of a subscription changes. Currently that’s forbidden by the docs.
We’ll use a helper function to demonstrate access rights.
>>> from zope.security.testing import interaction
>>> from zope.security import checkPermission
>>> from collections import namedtuple
>>> Permissions = namedtuple("Permissions",
... ('view', 'delete', 'other'))
>>> def checkPermissions(principal_id, object):
... def check(permission_id):
... with interaction(principal_id):
... return checkPermission(permission_id, object)
... return Permissions(
... check('zope.View'),
... check('nti.actions.delete'),
... check('zope.ManageContent'),
... )
The Alice principal has view and delete access to the subscription. We’ll use a helper function to demonstrate this.
>>> from zope import component
>>> from nti.webhooks import interfaces
>>> sub_manager = component.getUtility(interfaces.IWebhookSubscriptionManager)
>>> subscription = sub_manager['Subscription']
>>> checkPermissions('webhook.alice', subscription)
Permissions(view=True, delete=True, other=False)
The manager also has view and delete access, plus a bunch of other inherited things:
>>> checkPermissions('webhook.manager', subscription)
Permissions(view=True, delete=True, other=True)
The other principal has no access:
>>> checkPermissions('webhook.bob', subscription)
Permissions(view=False, delete=False, other=False)
Access to Delivery Attempts¶
The same access rights flow down to delivery attempts contained within a subscription. A helper to fire the deliveries is already defined.
import transaction
from zope import lifecycleevent
from zope import interface
from zope.container.folder import Folder
from zope.securitypolicy.interfaces import IPrincipalPermissionManager
from zope.annotation.interfaces import IAttributeAnnotatable
from nti.testing.time import time_monotonically_increases
from nti.webhooks.testing import wait_for_deliveries
__all__ = [
'deliver_some',
'wait_for_deliveries',
]
@time_monotonically_increases
def deliver_some(how_many=1, note=None, grants=None, event='created'):
for _ in range(how_many):
tx = transaction.begin()
if note:
tx.note(note)
content = Folder()
if grants:
# Make sure we can use the default (annotatable)
# permission managers.
interface.alsoProvides(content, IAttributeAnnotatable)
prin_perm = IPrincipalPermissionManager(content)
for principal_id, perm_id in grants.items():
prin_perm.grantPermissionToPrincipal(perm_id, principal_id)
sender = getattr(lifecycleevent, event)
sender(content)
transaction.commit()
To avoid actually trying to talk to example.com, we’ll be using some mocks.
>>> from nti.webhooks.testing import mock_delivery_to
>>> mock_delivery_to('https://example.com/some/path', method='POST', status=200)
We’ll perform the delivery as someone with high-privileges (the manager); we have to perform the delivery as someone because we need to be able to check permissions on the object.
>>> from delivery_helper import deliver_some, wait_for_deliveries
>>> with interaction('webhook.manager'):
... deliver_some(how_many=1, grants={'webhook.alice': 'zope.View'})
>>> wait_for_deliveries()
Now we can check the access rights for the recorded delivery attempt. All three principals should have the same access to the attempt that they had to the subscription.
>>> attempt = subscription.pop()
>>> checkPermissions('webhook.alice', attempt)
Permissions(view=False, delete=False, other=False)
>>> checkPermissions('webhook.manager', attempt)
Permissions(view=True, delete=True, other=True)
>>> checkPermissions('webhook.bob', attempt)
Permissions(view=False, delete=False, other=False)
Wait, how come no one except the manager had any access? What happened
to Alice? When we popped the attempt out of the subscription, it lost
its __parent__
and disconnected from the lineage and hence the
inherited flow of permissions. Let’s put it back and check again.
>>> attempt.__parent__ is None
True
>>> subscription['attempt'] = attempt
>>> checkPermissions('webhook.alice', attempt)
Permissions(view=True, delete=True, other=False)
>>> checkPermissions('webhook.manager', attempt)
Permissions(view=True, delete=True, other=True)
>>> checkPermissions('webhook.bob', attempt)
Permissions(view=False, delete=False, other=False)
Security Proxies¶
The global subscription manager (used here) is not registered with any permission. That means it does not have a security proxy wrapped around it, and thus, any user could potentially trigger deliveries. Likewise, the site-specific subscription managers (documented in Dynamic Webhook Subscriptions) also do not have security proxies created for them.
Traversal, however, is free to create security proxies. If you’re not using security proxies, then take the appropriate care to honor these permissions. For example, Pyramid view registrations should use these permissions before allowing access.
Customizing The Permission Grants¶
For information on customizing the permission grants, see
nti.webhooks.subscribers.apply_security_to_subscription()
.
-
nti.webhooks.subscribers.
apply_security_to_subscription
(subscription, event)[source] Set the permissions for the subscription when it is added to a container.
By default, only the owner_id of the subscription gets any permissions (and those permissions are
zope.View
andnti.actions.delete
). If there is no owner, no permissions are added.If you want to add additional permissions, simply add an additional subscriber. If you want to change or replace the default permissions, add an adapter for the subscription (in the current site) implementing
IWebhookSubscriptionSecuritySetter
; in that case you will be completely responsible for all security declarations.
Here’s a demonstration using a
nti.webhooks.interfaces.IWebhookSubscriptionSecuritySetter
that does nothing. When we create a new subscription for Bob, no one
(other than the manager, who inherits access) has any access.
>>> from nti.webhooks.interfaces import IWebhookSubscriptionSecuritySetter
>>> from nti.webhooks.interfaces import IWebhookSubscription
>>> from zope.interface import implementer
>>> from nti.webhooks.subscriptions import resetGlobals
>>> resetGlobals()
>>> @component.adapter(IWebhookSubscription)
... @implementer(IWebhookSubscriptionSecuritySetter)
... class NoOpSetter(object):
... def __init__(self, context, request=None):
... pass
... def __call__(self, context, event=None):
... pass
>>> from zope import component
>>> component.provideAdapter(NoOpSetter)
>>> conf_context = xmlconfig.string("""
... <configure
... xmlns="http://namespaces.zope.org/zope"
... xmlns:webhooks="http://nextthought.com/ntp/webhooks"
... >
... <include package="nti.webhooks" />
... <webhooks:staticSubscription
... to="https://example.com/some/path"
... for="zope.container.interfaces.IContentContainer"
... when="zope.lifecycleevent.interfaces.IObjectCreatedEvent"
... permission="zope.View"
... owner="webhook.bob" />
... </configure>
... """)
>>> subscription = sub_manager['Subscription']
>>> checkPermissions('webhook.alice', subscription)
Permissions(view=False, delete=False, other=False)
>>> checkPermissions('webhook.bob', subscription)
Permissions(view=False, delete=False, other=False)
>>> checkPermissions('webhook.manager', subscription)
Permissions(view=True, delete=True, other=True)