Writing a custom PAS plug-in
Workspace adapters
b-org ships with a bit of framework, adapted from some similar code in an unreleased version of teamspace by Wichert Akkerman, which can provide local roles in a "workspace" - in this case a Project. It relies on an adapter to the IWorkspace interface to determine the mapping of users and roles in the particular context. Before showing how this plug-in is written and registered, however, let's look at how it is used by a Project.In membership/project.py you will find:
class LocalRoles(object):This queries the lists of managers and members assigned (by reference) to the project and specifies that both managers and members should get the role TeamMember and managers should also get the role Manager.
"""Provide a local role manager for projects
"""
implements(IWorkspace)
adapts(IProjectContent)
def __init__(self, context):
self.context=context
def getLocalRoles(self):
project = IProject(self.context)
roles = {}
for m in project.getManagers():
roles[m.id] = ('Manager',)
for m in project.getMembers():
if m.id in roles:
roles[m.id] += ('TeamMember',)
else:
roles[m.id] = ('TeamMember',)
return roles
def getLocalRolesForPrincipal(self, principal):
r = self.getLocalRoles()
return r.get(principal, ())
As it turns out, this behaviour is also useful in Departments, which can be given one or more department managers by reference. The idea is that department managers should be allowed to add and remove Employees within that Department (recall that Department is a folderish container for Employee objects). The analogous adapter in membership/department.py reads:
class LocalRoles(object):Thus, a container wanting to use the PAS plug-in we're about to see to manage local roles only need to be adaptable to IWorkspace. In fact, this whole machinery ought to be factored out into a separate component, possibly sharing code to teamspace, another product which provides similar functionality. Mostly, this is down to laziness - creating another product (with all its boilerplate) and managing another dependency in the Products folder seemed too onerous when b-org was being developed. Hopefully, with Zope 2.10/Plone 3.0 and a growing preference for plain-Python packages and "eggs", it will seem a little less of an obstacle to split products up into multiple smaller pieces. So much for making excuses.
"""Provide a local role manager for departments
"""
implements(IWorkspace)
adapts(IDepartmentContent)
def __init__(self, context):
self.context = context
def getLocalRoles(self):
project = IDepartment(self.context)
roles = {}
for m in project.getManagers():
roles[m.id] = ('Manager',)
return roles
def getLocalRolesForPrincipal(self, principal):
r = self.getLocalRoles()
return r.get(principal, ())
The plug-in
The PAS plug-in that uses the IWorkspace interface can be found in pas/localrole.py. It looks like this:# Borrowed from Project pasification branch - written primarily byOn first glance, there is quite a lot going on here, but it is not so hard to understand. First, we define a good old-fashioned Zope 2 factory and ZMI add form. This is good practice, because PAS plug-ins can be managed via acl_users in the ZMI. If you find yourself wandering there, however, remember to bring a torch and keep a trail of breadcrumbs to find your way out. A backup wouldn't hurt either if you try to change things. It is, unfortunately, not the most intuitive of interfaces.
# Wichert Akkerman and Copyright Amaze Internet Services
# This module is releasd under the Zope Public License
from sets import Set
from Globals import InitializeClass
from Acquisition import aq_inner, aq_chain, aq_parent
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PlonePAS.interfaces.plugins import ILocalRolesPlugin
from Products.borg.interfaces import IWorkspace
manage_addWorkspaceLocalRoleManagerForm = PageTemplateFile(
"../zmi/WorkspaceLocalRoleManagerForm.pt", globals(),
__name__="manage_addProjectRoleManagerForm")
def manage_addWorkspaceLocalRoleManager(dispatcher, id, title=None, REQUEST=None):
"""Add a WorkspaceLocalRoleManager to a Pluggable Authentication Services."""
plrm = WorkspaceLocalRoleManager(id, title)
dispatcher._setObject(plrm.getId(), plrm)
if REQUEST is not None:
REQUEST.RESPONSE.redirect(
'%s/manage_workspace?manage_tabs_message=WorkspaceLocalRoleManager+added.'
% dispatcher.absolute_url())
class WorkspaceLocalRoleManager(BasePlugin):
meta_type = "Workspace Roles Manager"
security = ClassSecurityInfo()
def __init__(self, id, title=None):
self.id = id
self.title = title
#
# ILocalRolesPlugin implementation
#
security.declarePrivate("getRolesInContext")
def getRolesInContext(self, user, object):
roles = []
uid = user.getId()
obj, workspace = self._findWorkspace(object)
if workspace is not None:
if user._check_context(obj):
roles.extend(workspace.getLocalRolesForPrincipal(uid))
return roles
security.declarePrivate("checkLocalRolesAllowed")
def checkLocalRolesAllowed(self, user, object, object_roles):
roles = []
uid = user.getId()
obj, workspace = self._findWorkspace(object)
if workspace is not None:
if not user._check_context(obj):
return 0
roles = workspace.getLocalRolesForPrincipal(uid)
for role in roles:
if role in object_roles:
return 1
return None
security.declarePrivate("getAllLocalRolesInContext")
def getAllLocalRolesInContext(self, object):
rolemap = {}
obj, workspace = self._findWorkspace(object)
if workspace is not None:
localRoleMap = workspace.getLocalRoles()
for (principal, roles) in localRoleMap.items():
rolemap.setdefault(principal, Set()).update(roles)
return rolemap
# Helper methods
security.declarePrivate("_findWorkspace")
def _findWorkspace(self, object):
"""Find the first workspace, if any, in the acquistion chain of this
object. Returns a tuple obj, workspace where workspace is the adapted
IWorkspace.
"""
for obj in self._chain(object):
workspace = IWorkspace(obj, None)
if workspace is not None:
return obj, workspace
return None, None
security.declarePrivate("_chain")
def _chain(self, object):
"""Generator to walk the acquistion chain of object, considering that it
could be a function.
"""
# Walk up the acquisition chain of the object, to be able to check
# each one for IWorkspace.
# If the thing we are accessing is actually a bound method on an
# instance, then after we've checked the method itself, get the
# instance it's bound to using im_self, so that we can continue to
# walk up the acquistion chain from it (incidentally, this is why we
# can't juse use aq_chain()).
context = aq_inner(object)
while context is not None:
yield context
funcObject = getattr(context, 'im_self', None)
if funcObject is not None:
context = aq_inner(funcObject)
else:
# Don't use aq_inner() since portal_factory (and probably other)
# things, depends on being able to wrap itself in a fake context.
context = aq_parent(context)
classImplements(WorkspaceLocalRoleManager, ILocalRolesPlugin)
InitializeClass(WorkspaceLocalRoleManager)
We will see how the plug-in is registered and activated in a moment, but first notice that the plug-in implements an interface, ILocalRolesPlugin, which is defined by PlonePAS, the PAS-in-Plone integration layer. This defines methods that will be called by the PAS machinery to determine, in this case, local roles. Note that this is not an adapter (perhaps it would have been if PAS had been invented in Zope 3, though Zope 3 has its own authentication machinery that is evolved from PAS and works slightly differently). When created, the ProjectLocalRoleManager is an Zope 2 object that lives in the ZODB in acl_users.
The methods of the ILocalRolesPlugin interface are fairly self-explanatory in purpose. They allow PAS to extract the local roles for a particular user in a particular context (getRolesInContext()), to check whether a user in fact has one of the roles required to access a particular method attribute in a particular context (checkLocalRolesAllowed()), and to get a map of users-to-roles in a particular context.
The complex parts are, as often is the case, concerned with acquistion. The helper method _findWorkspace() attempts to walk up the object hierarchy to find the first possible IWorkspace (it will only consider one) to get hold of the appropriate IWorkspace adapter that is then used to determine the actual roles that apply, as above. Without walking up the content hierarchy, it would not be possible to let the local roles of a particular project apply when in the context of a piece of content inside that project (i.e. a sub-object of the folderish Project object). There is some reasonably hairy acqusition-juggling going on in the _chain() method to return this chain as a generator. The hairiness comes from the fact that the thing that is being checked may in fact be a method that is being accessed, and aqusition chains can get themselves in all kinds of knots, especially when Five is in the mix.
Lastly, we need to declare a ClassSecurityInfo and call InitializeClass to get Zope 2 to play ball.
Registering the plug-in
To be able to use this plug-in, we must first register it with PAS. This is done when the product is loaded, in borg/__init__.py:from Products.PluggableAuthService import registerMultiPluginThis is similar to how CMF content types are initialised with ContentInit().initialize() and context.registerClass(). In other words, copy-and-paste and the less you know the happier you will be.
...
from pas import localrole
...
registerMultiPlugin(localrole.WorkspaceLocalRoleManager.meta_type)
def initialize(context):
context.registerClass(localrole.WorkspaceLocalRoleManager,
permission = AddUserFolders,
constructors = (localrole.manage_addWorkspaceLocalRoleManagerForm,
localrole.manage_addWorkspaceLocalRoleManager),
visibility = None)
...
By registering the plug-in, we could now ask our users to instantiate a Workspace Roles Manager within acl_users.... er... somwhere. Like we said - not necessarily obvious. Better to do it once, in the setup code for b-org. Please refer to the section on GenericSetup to learn how b-org is actually installed, but notice that the relevant code is in setuphandlers.py:
from Products.CMFCore.utils import getToolByNameAll we do here is get hold of the factory dispatcher for the user folder (from manage_addProduct, which has something to do with that registerClass call for the WorkspaceLocalRoleManager seen in the previous code example, but like we said, it's dont-ask, don't-tell) and if it is not there already, we create an instance of the plugin using the factory. We then need to activate it so that it actually takes effect. out is a StringIO output stream used for logging.
from Products.PlonePAS.Extensions.Install import activatePluginInterfaces
from config import LOCALROLES_PLUGIN_NAME
...
def setupPlugins(portal, out):
"""Install and prioritize the project local-role PAS plug-in.
"""
uf = getToolByName(portal, 'acl_users')
borg = uf.manage_addProduct['borg']
existing = uf.objectIds()
if LOCALROLES_PLUGIN_NAME not in existing:
borg.manage_addWorkspaceLocalRoleManager(LOCALROLES_PLUGIN_NAME)
print >> out, "Added Local Roles Manager."
activatePluginInterfaces(portal, LOCALROLES_PLUGIN_NAME, out)