Simple PlonePAS Example
In this tutorial you will build a simple authentication plugin using PlonePAS. There are a couple documents that provide various details for writing PlonePAS plugins but nothing that covered all of the things I had to do in order to get something working. This tutorial will hopefully accomplish that.
Overview
This tutorial will leave you with a working PlonePAS plugin that can authenticate using a repository of users external to Plone.
I am not going to go into the details of PlonePAS. For an overview of PlonePAS itself, check out:
Creating the Plugin
Create a new Plone product using paster and add a new PAS plugin.
paster create -t plone example.pas
Add the new product to your buildout:
eggs =
example.pas
develop =
src/example.pas
zcml =
example.pas
In order to hook into PlonePAS authentication from an external source we will need to supply implementations for the IAuthenticationPlugin and IUserEnumerationPlugin. We can do that in a single class that implements both interfaces. Create a "plugins" directory in the example.pas product we just created:
cd example.pas/example/pas mkdir plugins touch plugins/__init__.py
Paste the following into "example.py" in the new plugins directory:
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from AccessControl.SecurityInfo import ClassSecurityInfo
from Products.PluggableAuthService.utils import classImplements
from Globals import InitializeClass
from Products.PluggableAuthService.interfaces.plugins import \
IAuthenticationPlugin, IUserEnumerationPlugin
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
manage_addExamplePluginForm = PageTemplateFile('../www/addExamplePAS',
globals(), __name__='manage_addExamplePluginForm')
def addExamplePlugin(self, id, title='', REQUEST=None):
''' Add a Example PAS Plugin to Plone PAS
'''
o = ExamplePlugin(id, title)
self._setObject(o.getId(), o)
if REQUEST is not None:
REQUEST['RESPONSE'].redirect('%s/manage_main'
'?manage_tabs_message=Example+PAS+Plugin+added.' %
self.absolute_url())
class ExamplePlugin(BasePlugin):
''' Plugin for Example PAS
'''
meta_type = 'Example PAS'
security = ClassSecurityInfo()
def __init__(self, id, title=None):
self._setId(id)
self.title = title
# IAuthenticationPlugin implementation
def authenticateCredentials(self, credentials):
''' Authenticate credentials against the fake external database
'''
if 'login' not in credentials or 'password' not in credentials:
return None
users = {'foo' : 'bar'}
login = credentials['login']
password = credentials['password']
if users.get(login, None) == password:
self._getPAS().updateCredentials(self.REQUEST,
self.REQUEST.RESPONSE, login, password)
return (login, login)
return None
def enumerateUsers(self, id=None, login=None, exact_match=False,
sort_by=None, max_results=None, **kw):
''' Return a list of valid users identified by this plugin.
'''
key = id or login
if not key or key != 'foo':
return None
return [{'id' : 'foo',
'login' : 'foo',
'pluginid' : self.getId()
}]
classImplements(ExamplePlugin, IAuthenticationPlugin, IUserEnumerationPlugin)
InitializeClass(ExamplePlugin)
The manage_addExamplePluginForm attribute is used by the ZMI when you add the plugin to an "acl_users" folder. We are going to automatically add the plugin to the user folder using Generic Setup but I will also show you how to register it with PAS so it shows up in the drop down. The addExamplePlugin function is the factory that Zope uses when a new ExamplePlugin is created. Both the template and the function get registered with Zope in the products initialization code.
The ExamplePlugin class has two methods of note: authenticateCredentials and enumerateUsers. Both of these methods are necessary to authenticate users who's details do not reside in Plone. the authenticateCredencials method takes a dictionary of credential information as an argument. When the method is called, the dictionary will look something like this:
{'extractor': 'credentials_cookie_auth',
'login': 'foo',
'password': 'bar',
'remote_host': '',
'remote_address': '10.1.100.204'}
The "extractor" field contains the id of the plugin that populated the credentials. Any plugin that implements IExtractionPlugin and is activated in PAS will have it's extractCredentials method called during the extraction step. If the plugin provides the credentials it must populate the "extractor" field with self.getId(). The rest of the fields should be self explanatory. This plugin relies on the default Plone extraction capabilities.
The authenticateCredentials in this plugin uses a simple hard coded dictionary to create a user "foo" with a password "bar". Obviously, you will want to replace the dictionary with some sort of query into your external system (e.g. select password from users where login='foo'). Connecting to the external source is beyond the scope of this document.
The authentication process will succeed but fail to persist without some way of storing the results. The call to updateCrendentials accomplishes this using the default PAS session as a storage container for the authenticated token. However, PAS will not store session information for a login that is not found in the user database. The updateCredentials method makes a call to PluggableAuthService._verifyUser which attempts to locate the supplied login in the Plone user database. It does this by calling the enumerateUsers method on all plugins that implement the IUserEnumerationPlugin. So all our plugin has to do is return a baked record if it is queried with the login id of 'foo'. Note that we actually need to check that the query was, in fact, for 'foo' and not some other user. Returning incorrect query results will cause things to break. There is a simple query language that enumerateUsers should implement. It is described in the doc string for the interface.
Btw, a (somewhat) quick way to find the file for a given module is to start Zope in debug mode, load the module and than print it's __file__ attribute:
bin/instance debug >>> from Products.PluggableAuthService import interfaces >>> interfaces.__file__ /home/jstark/wrk/plone/sand/parts/plone/PluggableAuthService/interfaces/__init__.pyc
I find myself getting lost in Plone's src layout far to often and just want a deterministic way to locate what I am looking for.
The classImplements informs Zope of the interfaces we are implementing. PlonePAS will use this information to include our plugin in the appropriate steps.
Next we need to add the Zope Page Template we specified for the manage_addExamplePlginForm attribute. Create a directory called "www" and add the following code to a new file called "addExamplePAS.zpt":
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<body>
<h1 tal:replace="structure here/manage_page_header">Header</h1>
<h2 tal:define="form_title string:Add an Example PAS plugin"
tal:replace="structure here/manage_form_title">Form Title</h2>
<p class="form-help">
The Example PAS plugin adds support for authentication against a hard coded dict of user/pass.
</p>
<form action="addExamplePlugin" method="post">
<table>
<tr>
<td class="form-label">Id</td>
<td><input type="text" name="id"/></td>
</tr>
<tr>
<td class="form-label">Title</td>
<td><input type="text" name="title"/></td>
</tr>
<tr>
<td colspan="2">
<div class="form-element">
<input type="submit" value="add Example plugin"/>
</div>
</td>
</tr>
</table>
</form>
</body>
</html>
This is the form the ZMI will use if you choose to add the ExamplePas plugin from the drop down menu. If you remove the option to add it via the menu than the template is not needed. You will see how to do this in the next section.
With the plugin in place it's now just a matter of wiring it up with Zope, Plone and PAS.
Wiring It All Up
Having created the plugin it is now time to register it with the framework.
First, lets register the ExamplePlugin class with Zope. In the products __init__.py file add the following code:
from Products.PluggableAuthService.PluggableAuthService import \
registerMultiPlugin
from plugins import example
def initialize(context):
''' Initialize product
'''
registerMultiPlugin(example.ExamplePlugin.meta_type) # Add to PAS menu
context.registerClass(example.ExamplePlugin,
constructors = (example.manage_addExamplePluginForm,
example.addExamplePlugin),
visibility = None)
The call to registerMultiPlugin will add our plugin to the drop down in the "acl_users" folder. A user can thus add the plugin manually. We are going to configure Generic Setup to automatically install the plugin so this line is optional. The call to registerClass registers the factory methods used to create an ExamplePlugin with Zope.
Next, we need to register our initialize method with Zope so that it gets called when Zope starts up. We do this in the configure.zcml file:
<five:registerPackage package="." initialize=".initialize" />
Be sure the "five" name space is defined as an attribute of the "configure" tag:
<configure
xmlns:five="http://namespaces.zope.org/five"
...
While we are in the configure.zcml file, register the Generic Setup profile we are going to create in the net step:
<genericsetup:registerProfile
name="default"
title="examplepas"
directory="profiles/default"
description="Add PlonePAS authentication ExamplePlugin"
provides="Products.GenericSetup.interfaces.EXTENSION"
/>
Be sure to add the genericsetup name space:
<configure
xmlns:five="http://namespaces.zope.org/five"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"
...
Now lets add the generic setup profile. We specified the profile will be in the directory "profiles/default" so go ahead and create that:
mkdir -p profiles/default cd profiles/default
Create a new file called "import_steps.xml" and add the following code:
?xml version="1.0"?>
<import-steps>
<import-step id="examplepas_varius" version="0.1"
handler="example.pas.setuphandlers.importVarius"
title="ExamplePAS Plugin">
</import-step>
</import-steps>
This will cause our plugin to be registered with Generic Setup and be selectable from the "Extension Profiles" menu when creating a new Plone site. We next need to implement the importVariuos method we just told Generic Setup to execute.
In the base plugin directory create a file called "setuphandlers.py" and add the following code:
from Products.PlonePAS.Extensions.Install import activatePluginInterfaces
from Products.CMFCore.utils import getToolByName
from StringIO import StringIO
from plugins.example import addExamplePlugin
def importVarius(context):
''' Install the ExamplePAS plugin
'''
out = StringIO()
portal = context.getSite()
uf = getToolByName(portal, 'acl_users')
installed = uf.objectIds()
if 'examplepas' not in installed:
addExamplePlugin(uf, 'examplepas', 'Example PAS')
activatePluginInterfaces(portal, 'examplepas', out)
else:
print >> out, 'examplepas already installed'
print out.getvalue()
This code first checks to see if our plugin is already installed. If it is not than it makes a call to the addExamplePlugin factory method to create an instance of the plugin in the "acl_users" folder. This Generic Setup step is also available from the "portal_setup" tool and can be rerun by selecting it there.
If you left the registerMultiPlugin function call in your initialize function than you can also go to your "acl_users" folder and add "Example PAS" from the drop down menu.
At this point you should be able to add the ExamplePlugin to the "acl_users" folder and login to your Plone site as user "foo" with a password of "bar".
Final Notes
Having created a basic authorization plugin here are some thoughts for further development.
Because our enumerateUsers method does not implement the full search interface you will not be able to search for "foo" in Plone's user management console. You can select "show all" and "foo" will be listed. From there you can assign privileges and add extended profile information that will be stored in Plone.
There are probably other oddities that will be caused by not fully implementing enumerateUsers and so it is important to implement that interface fully using whatever facilities are available given the external system you are accessing.
Extending this plugin is simply a matter of implementing additional interfaces. You can find these interfaces and some documentation in the "PluggableAuthServices/interfaces" directory.
