Developing for PAS

« Return to page index

This reference manual documents the Pluggable Authentication Service (PAS), which is used by Plone 2.5 and later for user management. This manual is aimed towards administrators who need to configure user management in a Plone site and towards developers who are interested in PAS plugins.

1. Concepts

PAS has a few basic concepts that you must understand in order to develop PAS related code.

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 users id can be different than the login name.
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!

2. The 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 different plugins. To accommodate this, PAS features a user object which provides a single interface to all different aspects.

2.1. The 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 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().

2.2. User creation

PAS uses a multi-phase algorithm to create a user object

  1. An 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

2.3. 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)

2.4. 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 IPropertiesPlugin 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() + "@ourcompany.com" }

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

2.5. 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 a 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.

2.6. Roles plugin

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.

3. 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.

3.1. Authorisation algorithm

These are 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 anonymous user. If succesful use this, if not:
  5. issue a challenge

3.2. 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 very common credentials. You may also use an HTTP cookie to track users; if you do so the cookie will be your credential.

PAS user 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.

3.3. Credential authentication

The credentials as returned by the credential extraction plugins only reflect the authentication information provided by the user. These credentials need to be authenticated by an authentication plugin to check if they are correct for a real user.

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.

3.4. 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 various protocols (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.

4. Caveats

There are some caveats you need to take into account when developing PAS plugins

4.1. 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.