Dive into PAS

Introduction

The Pluggable Authentication Service (PAS) is an alternative to the standard Zope User Folder or the popular Group User Folder (GRUF). PAS has a highly modular design, which is very powerful, but also a lot harder to understand.

PAS is built around the concepts of interfaces and plugins: all possible tasks related to user and group management and authentication are described in separate interfaces. These interfaces are implemented by plugins, which can be selectively enabled per interface.

Plone uses PlonePAS, which extends PAS with a couple of extra plugin types and which adds GRUF compatibility. Since PlonePAS extensions are rarely needed and are subject to change in the next Plone releases this tutorial will only focus on pure PAS features.

Using PAS

If you are using Plone 2.5 or newer your site will automatically use the PlonePAS product, which installs a PAS based user folder for your site and the Zope root user folder. For Plone 2.1 you can manually install a 1.x version of PlonePAS. It is highly recommended to use Plone 2.5 or newer though.

Features and interfaces

A user folder such as PAS provides a number of different services: it takes care of user authentication, it asks users to login if needed, it allows you to search for users and groups.

In order to make both configuration and implementation simpler and more powerful all these different tasks have been divided into different interfaces. Each interface describes how a specific feature, such as authenticating a user, has to be implemented.

Within PAS plugins are used to provide those features. Plugins are small pieces of logic which implement one or more functions as defined by these interfaces.

This separation is useful for different reasons:

  • it makes it possible to configure different aspects of the system separately. For example how users authenticate (cookies, login forms, etc.) can be configured separately from where user information is stored (ZODB, LDAP, RADIUS, SQL, etc.). This flexibility makes it very easy to tune the system to specific needs.
  • it makes it possible for developers to write small pieces of code that only perform a single task. This leads to code that is easier to understand, more testable and better maintainable.

The important interfaces

The most important interfaces that you may want to configure are:

Authentication
Authentication plugins are responsible for authenticating a set of credentials. Usually that will means verifying if a login name and password are correct by comparing them with a user record in a database such as the ZODB or an SQL database.
Extraction
Extraction plugins determine the credentials for a request. Credentials can take different forms such as a HTTP cookie, HTTP form data or the users IP address.
Groups
These plugins determine of which group(s) a user (or group) is a member.
Properties
Property plugins manage all properties for users. This includes the standard information such as the user's name and email address but can also be any other piece of data that you want to store for a user. Multiple properties plugins can be used in parallel, making it possible for example to use some information from a central system such as active directory while storing data specific for your Plone site in the ZODB
User Enumeration
User enumeration plugins implement the searching logic for users.

Configuring PAS

There is no Plone interface to configure PAS: you will need to use the Zope Management Interface (ZMI). In the ZMI you will see a acl_users folder in the site root. This is your PAS.

If you open the acl_users folder you will see a number of different items. Each item is a PAS plugin, which implements some PAS functionality.

pas-contents.png

The contents of a PAS user folder in the ZMI

There is one special item: the plugins objects manages all administrative bookkeeping within PAS. It remembers the which interfaces are active for each plugin and in what order the plugins should be called.

Let's take a look to see how this works. If you open the plugins object you will see a list of all the PAS interfaces, along with a short description of what they do.

We will take a look at the extraction plugins. These plugins take care of extracting the credentials such as your username and password from a request. These credentials can then be used to authenticate the user. If you click on the Extraction Plugins header you will see a screen which shows the plugins which implement this interface and allows you to configure which plugins will be used and in what order.

extraction-interface-config.png

Configuration for the extraction plugins.

In the default Plone configuration there are two plugins enabled for this interface:

  • the credentials_cookie_auth plugin can extract the login name and password from a HTTP cookie and HTTP form values from the login form or portlet
  • the credentials_basic_auth plugin can extract the login name and password from standard HTTP authentication headers.

In the default configuration the cookie plugin takes preference over the basic authentication plugin. This means that credentials from a HTTP cookie will be preferred over credentials form HTTP authentication headers if both are present You can try this by first logging in using standard HTTP authentication in the Zope root and then visiting your Plone site and logging in with a different user there: you will see that the new user is now the active user.

You can change the order of the plugins by clicking on a pluging and moving it up or down with the arrows. Using the left and right arrows you can enable and disable a plugin for this interface.

Configuring an individual PAS plugin

In addition to enabling and disabling plugins via the plugins object each plugin can also have its own configuration. You can access this by opening a plugin in the ZMI.

Taking the credentials_cookie_auth as example again you will see the screen for the Activate tab. This tab is mandatory and allows you to enable and disable PAS interfaces for a plugin. This corresponds to the plugin configuration we saw earlier, but does not allow you to change the ordering of different plugins for an interface. If you enable a new interface for a particular plugin, it will be activated and placed last in the list of plugins for a particular interface.

cookie-plugin.png

Interface configuration of the cookie plugin

You can also go to the properties tab to edit settings specific for this plugin:

cookie-plugin-properties.png

Configuration of the cookie plugin properties

What you can configure will differ per plugin. Some plugins do not have any configurations options, others can be very complex.

PAS development

Concepts

There are a few basic concepts used in PAS:

credentials
Credentials are a set of information which can be used to authenticate a user. This can be a login name and password, an IP address, a session cookie or something else.
user name
The user name is the name used by the user to log into the system. To avoid confusion between user id and user name this tutorial will use the term login name instead.
user id
All users must be uniquely identified by their user id. A user id can be different than the login name for a particular user.
principal
A principal is an identifier for any entity within the authentication system. This can be either a user or a group. This implies that it is not legal to have a user and a group with the same id!

A user object

Contrary to other user folders, a user does not have a single source in a PAS environment. Various aspects of a user (properties, groups, roles, etc.) are managed by PAS different plugins. To accommodate this, PAS features a user object which provides a single interface to all different aspects.

There are two basic user types: a normal user (as defined by the IBasicUser interface) and a user with member properties (defined by the IPropertiedUser interface). Since basic users are not used within Plone we will only consider IPropertiedUser users.

getId()
returns the user id. This is a unique identifier for a user.
getUserName()
Return the login name used by the user to log into the system.
getRoles()
Return the roles assigned to a user "globally".
getRolesInContext(context)
Return the roles assigned to the user within a specific context. This includes the global roles as returned by getRoles().

User creation

  1. A IUserFactoryPlugin plugin is used to create a new user object.
  2. All IPropertiesPlugin plugins are queried to get the property sheets.
  3. All IGroupsPlugin plugins are queried to get the groups.
  4. All IRolesPlugin plugins are queried to get the global roles

User factory plugin

PAS supports multiple user types. PAS contains two default user types: IBasicUser and IPropertiesUser. IBasicUser is a simple user type which supports a user id, login name, roles and domain restrictions. IPropertiedUser extends this type and adds user properties.

A user factory plugin creates a new user instance. PAS will add properties, groups and roles to this instance as part of its user creation process.

If no user factory plugin is able to create a user PAS will fall back to creating a standard PropertiedUser instance.

The IUserFactoryPlugin interface is a simple one containing a single method:

def createUser( user_id, name ):

    """ Return a user, if possible.

    o Return None to allow another plugin, or the default, to fire.
    """

The default PAS behaviour is demonstrated by this code:

def createUser(self, user_id, name):
    return ProperiedUser(user_id, name)

Properties plugins

Properties are stored in property sheets: mapping-like objecst, such as a standard python dictionary, which contain the properties for a principal. The property sheets are ordered: if a property is present in multiple property sheets only the property in the sheet with the highest priority is visible.

Property sheets are created by plugins implementing the IPropertesPlugin interface. This interface contains only a single method:

def getPropertiesForUser( user, request=None ):

    """ user -> {}

    o User will implement IPropertiedUser.

    o Plugin may scribble on the user, if needed (but must still
      return a mapping, even if empty).

    o May assign properties based on values in the REQUEST object, if
      present
    """

Here is a simple example:

def getPropertiesForUser(self, user, request=None):
    return { "email" : user.getId() + "@ourcompaony.com" }

this adds an email property to a user which is hardcoded to the user id followed by a companies domain name.

Group plugins

Group plugins return the identifiers for the groups a principal is a member of. Since a principal can be either a user or an group this implies that PAS can support nested group members. The default PAS configuration does not support this though.

Like other PAS interfaces the IGroupsPlugin interface is simple and only specifies a single method:

def getGroupsForPrincipal( principal, request=None ):

    """ principal -> ( group_1, ... group_N )

    o Return a sequence of group names to which the principal
      (either a user or another group) belongs.

    o May assign groups based on values in the REQUEST object, if present
    """

Here is a simple example:

def getGroupsForPrincipal(self, principal, request=None):
    # Manager can not be itself
    if principal=="Manager":
        return ()

    # Only act on the current user
    if getSecurityManager().getUser().getId()!=principal:
        return ()

    # Only act if the request originates from the local host
    if request is not None:
        ip=request.get("HTTP_X_FORWARDED_FOR", request.get("REMOTE_ADDR", ""))
        if ip!="127.0.0.1":
            return ()

    return ("Manager",)

this puts the current user in the Manager group if the site is being accessed from the Zope server itself.

Roles plugins

The IRolesPlugin plugins determine the global roles for a principal. Like the other interfaces the IRolesPlugin interface contains only a single method:

def getRolesForPrincipal( principal, request=None ):

    """ principal -> ( role_1, ... role_N )

    o Return a sequence of role names which the principal has.

    o May assign roles based on values in the REQUEST object, if present.
    """

Here is a simple example:

def getRolesForPrincipal(self, principal, request=None):
    # Only act on the current user
    if getSecurityManager().getUser().getId()!=principal:
        return ()

    # Only act if the request originates from the local host
    if request is not None:
        ip=request.get("HTTP_X_FORWARDED_FOR", request.get("REMOTE_ADDR", ""))
        if ip!="127.0.0.1":
            return ()

    return ("Manager",)

this gives the current user in Manager role if the site is being accessed from the Zope server itself.

Authorisation process

The Zope security machinery has to validate access to all protected resources. It does this by calling the validate method on the user folder. This method can determine which user is logged in and authorise the request. If a logged in and authorised user is found it is returned to the security system.

This is the steps the PAS user folder follows in its validate method:

  1. extract all credentials. This looks for any possible form of authentication information in a request: HTTP cookies, HTTP form parameters, HTTP authentication headers, originating IP address, etc. A request can have multiple (or no) sets of credentials.
  2. for each set of credentials found
    1. try to authorise the credentials. This checks if the credentials correspond to a known user and are valid.
    2. create a user instance
    3. try to authorise the request. if succesful use this user and stop further processing.
  3. create an anonymous user
  4. try to authorise the request using the anon user. if succesful use this, if not:
  5. issue a challenge

Credential extraction

Within PAS credentials are a set of information which can identify and authenticate a user. A users login name and password are for example are a very common credentials. You may also use a HTTP cookie to track users; if you do so the cookie will be your credential.

PAS users credential extraction plugins to find all credentials in a request. Authentication of these credentials is done at a later stage by seperate authentication plugin.

Writing a plugin

If you want to write your own credential extraction plugin it has to implement the IExtractionPlugin interface. This interface only has a single method:

def extractCredentials( request ):

    """ request -> {...}

    o Return a mapping of any derived credentials.

    o Return an empty mapping to indicate that the plugin found no
      appropriate credentials.
    """

Here is a simple example:

def extractCredentials(self, request):
    login=request.get("login", None)

    if login is None:
        return {}

    password="request.get("password", None)

    return { "login" : login, "password" : password }

This plugin extracts the login name and password from fields with the same name in the request object.

Credential authentication

The credentials as returned by the credential extraction plugins

Writing a plugin

The IAuthenticationPlugin interface is a simple one:

def authenticateCredentials( credentials ):

    """ credentials -> (userid, login)

    o 'credentials' will be a mapping, as returned by IExtractionPlugin.

    o Return a  tuple consisting of user ID (which may be different
      from the login name) and login

    o If the credentials cannot be authenticated, return None.
    """

Here is a simple example:

def authenticateCredentials(self, credentials):
    users={ "hanno" : "hannosch", "martin" : "optilude",
            "philipp" : "philiKON" }

    if "login" not in credentials or "password" not in credentials:
        return None

    login=credentials["login"]
    password=credentials["password"]
    if users.get(login, None)==password:
        return (login, login)

    return None

This plugin allows the users hanno, martin and philipp to login with their nickname as password.

Challenges

If the current (possibly anonymous) user is not authorised to access a resource Zope asks PAS to challenge the user. Generally this will result in a login form being shown, asking the user with a appropriately priviliged account.

The IChallengeProtocolChooser and IChallengePlugins plugins work together to do this. Since Zope can be accessed via methods (browsers, WebDAV, XML-RPC, etc.) PAS first needs to figure out what kind of protocol it is dealing with. This is done by quering all IChallengeProtocolChooser plugins. The default implementation is ChallengeProtocolChooser, which asks all IRequestTypeSniffer plugins to test for specific protocols.

Once the protocol list has been build PAS will look at all active IChallengePlugins plugins.

Writing a plugin

The IChallengePlugin interface is very simple: it only contains one method:

def challenge( request, response ):

    """ Assert via the response that credentials will be gathered.

    Takes a REQUEST object and a RESPONSE object.

    Returns True if it fired, False otherwise.

    Two common ways to initiate a challenge:

      - Add a 'WWW-Authenticate' header to the response object.

        NOTE: add, since the HTTP spec specifically allows for
        more than one challenge in a given response.

      - Cause the response object to redirect to another URL (a
        login form page, for instance)
    """

The plugin can look at the request object to determine what, or if, it needs to do. It can then modify the response object to issue its challenge to the user. For example:

def challenge(self, request, response):
    response.redirect("http://www.disney.com/")
    return True

this will redirect a user to the Disney homepage every time he tries to access something he is not authorised for.

Caveats

PAS eats exceptions

A broken user folder is one of the worst things that can happen in Zope: it can make it impossible to access any objects underneath the user folders level.

In order to secure itself against errors in plugins PAS ignores all exceptions of the common exception types: NameError, AttributeError, KeyError, TypeError and ValueError.

This can make debugging plugins hard: an error in a plugin can be silently ignored if its exception is swallowed by PAS.