Personal tools
You are here: Home Documentation Tutorials b-org: Creating content types the Plone 2.5 way Using membrane to provide membership behaviour
Support

Get Help

Join our chat rooms or support forums if you have more specific questions.

Plone Training
Learn how to design, build, and deploy a website in Plone through one of the numerous Plone training sessions around the world.
Find Plone training…
 
Document Actions

Using membrane to provide membership behaviour

How b-org uses membrane to let employees be users and departments be groups

Martin Aspeli

Plone 2.5 brings us closer to the promised land of Zope 3. Zope 3 brings us a new way of working. This tutorial will show how to marry the old and the new, to make Plone products that are more extensible, better tested and easier to maintain.
Page 10 of 15.

Since version 2.5, the user management infrastructure in Plone has been replaced by PAS, the Zope Pluggable Authentication Service, and PlonePAS, a Plone integration layer for this. PAS offers several advantages over plain user folders, mainly in terms of flexibility. Unfortunately, it is also more difficult to work with through-the-web and has a very decentralised API, based on the notion of plugin components, that can be difficult to understand at first.

Membrane (or rather, membrane with a lowercase m) is a component first developed by Plone Solutions and later improved by Rob Miller and others. It is similar to CMFMember in that it can turn content objects into users, although it is less concerned with replicating existing Plone functionality and more concerned with making a thin integration layer to plug into. It therefore fits b-org very well.

Membrane works on Archetypes objects (though theoretically it could be used with other objects as well). It adds a tool called membrane_tool which contains a registry of content types that are member- or group-sources, as well as a special catalog. Using the Archetypes catalog multiplex, it is able to catalog objects (which may also be cataloged in portal_catalog) and find them again based on various interfaces (that is, it catalogs the interfaces provided by an object). membrane provides a number of PAS plug-ins that will search this catalog when looking for users and delegate to the content objects (or rather, adapters on the content object) for obtaining user information, performing authentication and so on.

Registering with membrane

membrane_tool contains an API for registering content types as membership providers, but the easiest option is to use a GenericSetup profile (see the section on GenericSetup for the full story). In profiles/default/membrane_tool.xml, you will find:
<?xml version="1.0"?>
<object name="membrane_tool" meta_type="MembraneTool">
<membrane-type name="Department">
<active-workflow-state name="active" />
</membrane-type>
<membrane-type name="Employee">
<active-workflow-state name="active" />
</membrane-type>
<membrane-type name="Project">
<active-workflow-state name="published" />
<active-workflow-state name="private" />
</membrane-type>
</object>
This registers the three content types (by their portal type), and specifies the workflow states in which they are "active" as member and group sources.

Applying marker interfaces

When looking for content objects that provide group and member information, membrane will use a number of marker interfaces that indicate support for various types of behaviour. These are implemented by the three content type classes.

In content/department.py, you will find:
from Products.membrane.interfaces import IPropertiesProvider

...

class Department(ExtensibleSchemaSupport, BaseFolder):
"""A borg department.

Departments can contain other employees.
"""

implements(IDepartmentContent, IPropertiesProvider)
All this means is that the Department's schema is capable of providing properties to PAS. Properties (normally related to users, but groups can have properties as well) are just metadata about the user or group. Membrane supports as PAS properties plugin that will look for Archetypes schema fields with member_property=True set and report these back as user properties. Although Department does not use any such properties at the moment, we add this marker so that extension modules that use the schema extension mechanism can benefit from this.

The equivalent setup for Employees, in content/employee.py, is a little more interesting.
from Products.membrane.interfaces import IUserAuthProvider
from Products.membrane.interfaces import IPropertiesProvider
from Products.membrane.interfaces import IGroupsProvider
from Products.membrane.interfaces import IGroupAwareRolesProvider

...

class Employee(ExtensibleSchemaSupport, BaseContent):
"""A borg employee.

Employees are also users.
"""

implements(IEmployeeContent,
IUserAuthProvider,
IPropertiesProvider,
IGroupsProvider,
IGroupAwareRolesProvider,
IAttributeAnnotatable)
Here, we are saying that:
  • An Employee can be used as a source of authentication (i.e. as a user), since it is marked with IUserAuthProvider. Note that the actual authentication is performed by a different adapter.
  • An Employee can provide user properties to PAS via membrane, following IPropertiesProvider.
  • An Employee can be part of a group, because of IGroupsProvider.
  • An employee can be given roles. There is an IRolesProvider interface that we cold use for basic role awareness. The IGroupAwareRolesProvider is a sub-interface that will cause membrane to also look at the user's groups.
The IAttributeAnnotatable interface is part of Zope 3's annotations framework, discussed in a later section.

Projects does not require any particular marker interfaces.

Providing membership behaviour

When membrane looks for objects to provide membership-related behaviour, it will not only look for objects directly providing a particular interface, but also for objects that can be adapted to that interface. For example, the presence of the interface IGroup informs membrane that an object can act as a group, and contains methods that describe the members of that group.

Of course, we could have declared that Department implemented IGroup and written these methods directly in the Department content object. Hopefully you'll agree now that this would not be optimal, since it mixes the content-object aspect and the group-behaviour aspect of Department into a single monolithic object. Instead, we will use an adapter, which also means that if you require different behaviour in an extension to b-org, you have only to override the adapter, leaving the core content object alone.

In membership/department.py, you will see:
class Group(object):
"""Allow departments to act as groups for contained employees
"""
implements(IGroup)
adapts(IDepartmentContent)

def __init__(self, context):
self.context = context

def Title(self):
return self.context.Title()

def getRoles(self):
"""Get roles for this department-group.

Return an empty list of roles if the department is in a workflow state
that is not active in membrane_tool.
"""
mb = getToolByName(self.context, MEMBRANE_TOOL)
wf = getToolByName(self.context, 'portal_workflow')

reviewState = wf.getInfoFor(self.context, 'review_state')
wfmapper = ICategoryMapper(mb)
categories = generateCategorySetIdForType(self.context.portal_type)
if wfmapper.isInCategory(categories, ACTIVE_STATUS_CATEGORY, reviewState):
return self.context.getRoles()
else:
return ()

def getGroupId(self):
return self.context.getId()

def getGroupMembers(self):
mt = getToolByName(self.context, MEMBRANE_TOOL)
usr = mt.unrestrictedSearchResults
members = {}
for m in usr(object_implements=IMembraneUserAuth.__identifier__,
path='/'.join(self.context.getPhysicalPath())):
members[m.getUserId] = 1
return tuple(members.keys())
Mostly, this is about examining the Department content object to find roles (which are listed in an Archetypes field, editable by the Manager role). When calculating roles, we make sure that we don't give roles if the Department group-source is actually disabled (by virtue of its workflow state and the settings in membrane_tool). The group title and id are taken from the object as well.

The most interesting method is getGroupMembers(). Here, we perform a search in the membrane_tool catalog for objects adaptable to IMembraneUserAuth. This interface is the basic interface in membrane describing things that can act as users - there is an adapter from IUserAuthProvider to IMembraneUserAuth. We restrict this to objects inside the Department object. The net effect is that all Employee objects inside the Department are returned.

Now, let's say you had a need for a Department which in addition to acting as a group for all members inside it, also allowed some members from other departments to be in that group. In this case, you could use a schema extender to add a ReferenceField to the schema of Department that allowed the Department owner to reference other Employees. You would then provide an override adapter, perhaps subclassing Products.borg.membership.department.Group but overriding getGroupMembers() to append the ids of the referenced users as well as the contained ones ... or instead of, depending on your needs.

As it happens, Projects also act as groups, with members being assigned by reference, using two reference fields - one for project members, and one for project manangers. Here is the equivalent adapter from membership/project.py:
class Group(object):
"""Allow projects to be groups for related members and managers
"""
implements(IGroup)
adapts(IProjectContent)

def __init__(self, context):
self.context = context

def Title(self):
return self.context.Title()

def getRoles(self):
# The project does not imply any special roles *globally*, although
# the IWorkspace adapter above enables some local roles
return ()

def getGroupId(self):
return self.context.getId()

def getGroupMembers(self):
return [IUserRelated(m).getUserId() for m in
self.context.getRefs(PROJECT_RELATIONSHIP) +
self.context.getRefs(PROJECT_MANAGER_RELATIONSHIP)]
As may be expected, the membrane adapters for Employee are a bit more involved. They consist of the following:
IUserRelated adapter
Provides a user id for employees. Note that user ids and user names are possibly different when PAS is used: the user id must be globally unique; the user name is the named used for logging in.
IUserAuthentication adapter
Used to perform actual authentication by validating a supplied username and password.
IUserRoles adapter
Used to determine which roles the particular user is given.
IMembraneUserManagement
Used by membrane and Plone's UI to deal with changes to the user, such as the adding of a new user (not implemented here, since we
All these adapters are found in membership/employee.py.

The IUserRelated adapter is the simplest, as it simply invokes the user name. Note that by default, membrane will use the Archetypes UID() function as the user id. This is sensible, but unfortunately Plone's UI (and that of third party products) is not always aware of the distinction between user id and user name. Ideally, only the user name would ever be displayed, the user id being an internal concept, but in practice you may end up with things like member folder names that are long, unfriendly UID strings. Sometimes this may even be unavoidable in the general case, because it's possible that two different sources of users could use the same user name for two different user ids! For the purposes of b-org, however, we assume user names are unique and well-defined. The adapter is therefore quite trivial:
class UserRelated(object):
"""Provide a user id for employees.

The user id will simply be the id of the member object. This overrides the
use of UIDs
"""
implements(IUserRelated)
adapts(IEmployeeContent)

def __init__(self, context):
self.context = context

def getUserId(self):
return self.context.getId()
The id of the content object that represents the employee is used as the user id. This is also used as the user name, as defined in the IUserAuthentication adapter:
class UserAuthentication(object):
"""Provide authentication against employees.
"""
implements(IUserAuthentication)
adapts(IEmployeeContent)

def __init__(self, context):
self.context = context

def getUserName(self):
return self.context.getId()

def verifyCredentials(self, credentials):
login = credentials.get('login', None)
password = credentials.get('password', None)

if login is None or password is None:
return False

digest = sha(password).digest()

annotations = IAnnotations(self.context)
passwordDigest = annotations.get(PASSWORD_KEY, None)

return (login == self.getUserName() and digest == passwordDigest)
In the verifyCredentials() method, the adapter is passed the login and password as entered by the user in a dict (credentials) and then compares those to the values stored on its context (the Employee content object). The password is stored as a SHA1 digest in an annotation to make sure it cannot be read back by examining the content object - more on this in the section on annotations. Be aware also that the IUserAuthentication adapter is called on every request after a user is logged in and can deny access for whatever reason by returning non-True. This means that it is important that the method is as efficient as possible - expensive database lookups, for example, are probably not a good idea here!

The IUserRoles adapter is trivial. Roles are stored on the content object in a field that is editable only by managers. Of course, we could have picked roles from some other rule if necessary:
class UserRoles(object):
"""Provide roles for employee users.

Roles may be set (by sufficiently privilged users) on the user object.
"""
implements(IUserRoles)
adapts(IEmployeeContent)

def __init__(self, context):
self.context = context

def getRoles(self):
return self.context.getRoles()
The getRoles() method returns an iterable of strings representing applicable roles. Note that depending on group membership (and the IGroupAwareRolesProvider marker as described above) and local roles the user may in fact have more roles than what this method returns! The IUserRoles interface is concerned only with global roles intrinsic to the user.

Finally, we have the IMembraneUserManagement adapter. This lets membrane know what to do when it is asked by Plone's UI to add, edit or remove users. In particular, the doChangeUser() method enables the PasswordResetTool to do its magic. Note that we have not implemented doAddUser(), because there is no well-defined global policy for where the actual Employee content object should be added! Recently membrane has gained some functionality whereby a site-local utility providing IUserAdder from membrane can be queried for this policy. That may be useful for b-org extension products, but b-org is still not in a position to make a general policy for this, so it is not implemented out of the box.
class UserManagement(object):
"""Provides methods for adding deleting and changing users

This is an implementation of IUserManagement from PlonePAS
"""
implements(IMembraneUserManagement)
adapts(IEmployeeContent)

def __init__(self, context):
self.context = context

def doAddUser(self, login, password):
"""This can't be done unless we have a canonical place to store users
some implementations may wish to define one and implement this.
"""
raise NotImplementedError

def doChangeUser(self, login, password, **kw):
self.context.setPassword(password)
if kw:
self.context.edit(**kw)

def doDeleteUser(self, login):
parent = aq_parent(aq_inner(self.context))
parent.manage_delObjects([self.context.getId()])

That's it! Through these adapters, the three b-org content types are able to act as sources of groups and users. Hopefully, you will appreciate the flexibility of the separation of concerns into adapters for things like editing user properties, determining user id, calculating roles and performing authentication. If you extend b-org, you can provide a more specific adapter to any of the above interfaces to customise the membership behaviour.

 
by Martin Aspeli last modified October 25, 2006 - 22:50 All content is copyright Plone Foundation and the individual contributors.

For any issues with the web site functionality, please file a ticket.

Please consult the policy on plone.org content if you want your content published on this site.

Servers and hosting by