In our wiki example, we decided on using a session based credentials extraction together with a PrincipalsFolder.  Since the credentials extractor needs no persistent storage, we install it as a global utility. 

import grok
from grok4noobs import Grok4Noobs
from zope.pluggableauth.plugins.session import SessionCredentialsPlugin

class CredentialsPlugin(grok.GlobalUtility, SessionCredentialsPlugin):
    ''' Define the credentials plugin and challenge form fields
    '''
    grok.provides(ICredentialsPlugin)
    grok.name('credentials')

    loginpagename = 'login'
    loginfield = 'form.login'
    passwordfield = 'form.password'

The PrincipalsFolder on the other hand needs persistence, so we create a local utility for that.

from zope.pluggableauth.plugins.principalfolder import PrincipalFolder, InternalPrincipal
from zope.pluggableauth.interfaces import IAuthenticatorPlugin
from zope.securitypolicy.interfaces import IPrincipalRoleManager

class AuthenticatorPlugin(PrincipalFolder, grok.LocalUtility):
    ''' The Zope toolkit provides a folder based authenticator plugin 
        called PrincipalFolder.
        We build a PAU plugin as a local utility based on the PrincipalFolder,
        which already implements IAuthenticatorPlugin.
        To use an external authenticator, eg. LDAP, one would implement the
        IAuthenticatorPlugin interface directly.
    '''
    grok.provides(IAuthenticatorPlugin)
    grok.name('users')

    def __init__(self, *args, **kwargs):
        ''' Create an administrator with default login and password
        '''
        super(AuthenticatorPlugin, self).__init__(*args, **kwargs)

        su = InternalPrincipal(login='admin', password='Admin', 
                               title=u'Administrator', 
                               description=u'The SuperUser')
        self['admin'] = su

        roleMgr = IPrincipalRoleManager(grok.getSite())
        for uid, _setting in roleMgr.getPrincipalsForRole('gfn.Administrator'):
            roleMgr.unsetRoleForPrincipal('gfn.Administrator', 'gfn.'+uid)

        uid = self.getIdByLogin('admin')
        roleMgr.assignRoleToPrincipal('gfn.Administrator', 'gfn.'+uid)

Local utilities need to be registered with the local site.  In case you are wondering, the site is the same thing as your application.  grok.Application implements an ISite.  The hierarchy in the ZODB looks something like this:

App 1 is a container (if it subclasses a grok.Container) and so it can contain other grok.Model or grok.Container instances. Containers, of course, look and quack like dicts and so can contain other traversable elements.  Whether the site is a container or model, as with all Python objects it can also have attributes. One of the attributes of sites like App 1 will be "_sm".  This attribute contains the site manager for the site, accessible via:

site = grok.getSite()
sm = site.getSiteManager()

The site manager can be used to register local components such as utilities or adapters.  Such components are persistently stored inside the ZODB.

We built an adapter with the interface ISiteLocalInstaller which adapts an ISite to simplify the task of registering local utilities, and the source may be found in sitelocal.py:

#__________________________________________________________________________________________________
# A small module to allow registration of local utilities after the site has been created
# and added via the management UI
import grok
from zope.component import Interface
from zope.location.interfaces import ISite

class ISiteLocalInstaller(Interface):
    '''  Describes the registration interface
    '''
    def unregisterUtility(self, provided, name=None, name_in_container=None):
        '''  Unregister a local utility
        '''
    def registerUtility(self, factory, provided, name=None, name_in_container=None, setup=None):
        '''  Register a local utility
        '''

class SiteLocalInstaller(grok.Adapter):
    '''  An adapter which adapts an ISite (or grok.Application) to return
         a local utility installer
    '''
    grok.context(ISite)
    grok.implements(ISiteLocalInstaller)

    def unregisterUtility(self, provided, name='', name_in_container=None):
        ''' Unregister a local utility from the site
        '''
        if name_in_container is None:
            if name and len(name):
                name_in_container = name
            else:
                raise Exception(u'We need either a name, or a name_in_container to unregister local components')

        sm = self.context.getSiteManager()

        util = sm.queryUtility(provided, name=name)
        if util is not None:
            print 'delete %s' % util
            sm.unregisterUtility(provided=provided)
            del util


    def registerUtility(self, factory, provided, name='', name_in_container=None, setup=None):
        ''' Register a new local utility with the site.  If it already exists
            we remove the old one first
        '''
        if name_in_container is None:
            if name and len(name):
                name_in_container = name
            else:
                raise Exception(u'We need either a name or a name_in_container to register local components')

        sm = self.context.getSiteManager()
        old = sm.queryUtility(provided, name=name)
        if old is not None:
            sm.unregisterUtility(component=old, provided=provided)
            del old

        if name_in_container in sm: del sm[name_in_container]
        try:
            obj = factory()
        except:
            obj = factory

        sm[name_in_container] = obj
        sm.registerUtility(obj, provided=provided, name=name)
        if setup: setup(obj)

You can see how first the site is retrieved, then the site manager, and finally the new utility is registered with the site manager.  Please note the difference between the name and name_in_container arguments; the name argument is the utility name, used in functions like getUtility() and queryUtility() to retrieve the utility by interface and name.  It is important that if a name is not specified, the default name will be an empty string. 

Querying a registered utility without specifying a name will not retrieve a named utility, and retrieving a utility while specifying a name will not match a registered utility with an empty name.

When using queryUtility() where there is both a local and global utility registered with matching names, the local utility will be returned.  This lets us override the default global IAuthentication utility for our application by defining our own local utility and registering it with the site manager.

class PluggableAuthenticatorPlugin(PluggableAuthentication, grok.LocalUtility):
    ''' The Pluggable Authentication Utility mechanism provided by the
        Zope Toolkit is very flexible.  It allows registration of utilities
        which retrieve credentials from the request, or provide authentication.
        One way to use it, is to use component lookup via the ZCA by name.
        Another way is to simply include instances of the plugins directly
        inside the PluggableAuthentication, as it is a persistent container
        in it's own right.
    '''
    grok.provides(IAuthentication)
    grok.name('pau')

    def __init__(self, *args, **kwargs):
        super(PluggableAuthenticatorPlugin, self).__init__(*args, **kwargs)
        self.credentialsPlugins = ['credentials']       # Name of utility for ICredentialsPlugin
        self.authenticatorPlugins = ['users']           # Name of utility for IAuthenticatorPlugin
        self.prefix = 'gfn.'

The 'gfn.' prefix is defined here to ensure that principals will have unique identities.

The usual way to register local utilities in Grok is to use the grok.local_utility() directive inside your application class definition. However, this has two problems:

  • It only works when the application is first installed.  You cannot use it to install local utilities after the fact.
  • The module implementing the application would have to import the class definitions of the local utilities being defined.  This may lead to circular imports and dependency issues.

In our case, we install our local utilities if they don't exist at the time we are extracting credentials from our request:

class ILoginForm(Interface):
    ''' Our login form implements login and password schema fields.
    '''
    login = schema.BytesLine(title=u'Username', required=True)
    password = schema.Password(title=u'Password', required=True)


class Login(forms.AddForm):
    ''' This is our login form.  It will render when and where we need it.
    '''
    grok.context(Interface)
    grok.require('zope.Public')

    form_fields = grok.Fields(ILoginForm)

    @grok.action('login')
    def handle_login(self, **data):
        ''' If the authentication plugins are not yet installed, install them
        '''
        pau = queryUtility(IAuthentication)
        if pau is None or type(pau) is not PluggableAuthenticatorPlugin:
            installer = ISiteLocalInstaller(grok.getSite())

            installer.registerUtility(PluggableAuthenticatorPlugin,
                                      provided=IAuthentication,
                                      name_in_container='pau')

            installer.registerUtility(AuthenticatorPlugin,
                                      provided=IAuthenticatorPlugin,
                                      name='users')
            pau = queryUtility(IAuthentication)
            if pau is not None: pau.authenticate(self.request)
            self.redirect(self.url(self.context, data=data))

Our credentials are extracted from the 'login' view as specified in our credentials plugin.  If the local utilities do not yet exist, we use the event of a 'login' action to check and install the two local utilities.  The first is an IAuthentication utility, being our instance of a PluggableAuthentication with an empty string as a name.  The second utility is our PrincipalsFolder which exports the interface for an IAuthenticatorPlugin.

If we try to access a protected view for which we do not have access, Grok will render the login page instead, so that the user can enter a name and password.  This behaviour sounds like a good idea, and can be for simple sites. In our case we prefer to have a login view displayed for unauthenticated users, and a welcome message displayed for those who are authenticated.  To do this, we need to ensure that we never try to visit a page which is protected, which we can do with relative ease by protecting viewlets.  Protected viewlets do not try to render anything at all unless the user has access to that content.

To render our welcome message or login prompt depending on login status, we define the following viewlet:

class Status(grok.Viewlet):
    ''' Renders the login form in Authentication area for layout
    '''
    grok.context(ILayout)
    grok.viewletmanager(AuthSection)
    grok.require('zope.Public')

    def loggedIn(self):
        ''' Tries to authenticate if not already authenticated. Returns status.
        '''
        if not isLoggedIn(self.request):
            auth = queryUtility(IAuthentication)
            if auth is not None:
                auth.authenticate(self.request)
        return isLoggedIn(self.request)

    def greeting(self):
        ''' Returns a greeting depending on the time of day
        '''
        from datetime import datetime as dt
        hour = dt.now().hour
        tod = "Morning" if hour < 12 else "Afternoon" if hour < 18 else "Evening"
        return "Good %s, %s" % (tod, self.request.principal.title)

    def zopeLogin(self):
        ''' Zope management by default uses a Basic Auth login with a 'zope' prefix
        '''
        if self.loggedIn():
            ns = self.request.principal.id.split('.')
            if len(ns) > 1: ns = ns[0]
            if ns=='zope': return True
        return False

    def logoutLink(self):
        ''' If this is a Basic Auth login, redirect to challenge site,
            otherwise to our own logout view
        '''
        if self.zopeLogin():
            site = self.view.url("/").split("//")[1].split("/")[0]
            return "http://log:out@%s/." % site
        else:
            return self.view.url(self.context, "logout")

The matching page template for this viewlet is in auth_templates/status.pt:

<div>
    <div tal:condition="viewlet/loggedIn">
        <p tal:content="viewlet/greeting" />
        <a tal:attributes="href viewlet/logoutLink"><button>Logout</button></a>
    </div>
    <div tal:condition="not:viewlet/loggedIn" tal:content="structure context/@@login" />
</div>

The condition viewlet/loggedIn triggers the actual authenticate() process which attempts to authenticate the request. If the request contains the login form, this will succeed for valid credentials, providing the greeting instead of the login page.

Note that the logoutLink() points either at a Basic Auth logout or a logout view depending on whether the ZMI is logged in or not. The only way to log out of an HTML Basic Auth connection, is to click "cancel" when presented with the credentials challenge form.

The helper function which determines whether or not we are logged in, is simple:

def isLoggedIn(request):
    ''' Convenience function tells us if we are logged in
    '''
    return not IUnauthenticatedPrincipal.providedBy(request.principal)

The view which logs out from a session based authenticated connection, is also quite simple:

class Logout(grok.View):
    ''' We can call this view to log out from the cookie based login session.
    '''
    grok.context(Interface)
    grok.require('zope.Public')

    def update(self):
        if isLoggedIn(self.request):
            auth = queryUtility(IAuthentication)
            ILogout(auth).logout(self.request)

    def render(self):
        self.redirect(self.url(self.context))

 Since Logout uses a generic Interface as a context, it can be used to log out for any model or container.

 

Grok 4 Noobs

Installing a Pluggable Authentication Utility