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.
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.
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:
The most important interfaces that you may want to configure are:
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.
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.
In the default Plone configuration there are two plugins enabled for this interface:
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.
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.
You can also go to the properties tab to edit settings specific for this plugin:
What you can configure will differ per plugin. Some plugins do not have any configurations options, others can be very complex.
There are a few basic concepts used in PAS:
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.
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 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 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.
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.
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:
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.
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.
The credentials as returned by the credential extraction plugins
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.
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.
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.
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.