Creating the Plugin

by Jeremy Stark last modified Jun 22, 2009 08:27 PM
Create a new Plone product using paster and add a new PAS plugin.
Create a new Plone product to hold the 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.