[Checkins] SVN: zope.pluggableauth/trunk/ merge -r 117491:117503 svn+ssh://svn.zope.org/repos/main/zope.pluggableauth/branches/jw-authenticator-plugins

Jan-Wijbrand Kolman janwijbrand at gmail.com
Mon Oct 18 05:12:53 EDT 2010


Log message for revision 117625:
  merge -r 117491:117503 svn+ssh://svn.zope.org/repos/main/zope.pluggableauth/branches/jw-authenticator-plugins

Changed:
  U   zope.pluggableauth/trunk/CHANGES.txt
  U   zope.pluggableauth/trunk/setup.py
  U   zope.pluggableauth/trunk/src/zope/pluggableauth/__init__.py
  U   zope.pluggableauth/trunk/src/zope/pluggableauth/interfaces.py
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.py
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.txt
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.zcml
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/idpicker.py
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.py
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.txt
  A   zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.zcml
  U   zope.pluggableauth/trunk/src/zope/pluggableauth/tests.py

-=-
Modified: zope.pluggableauth/trunk/CHANGES.txt
===================================================================
--- zope.pluggableauth/trunk/CHANGES.txt	2010-10-18 08:52:55 UTC (rev 117624)
+++ zope.pluggableauth/trunk/CHANGES.txt	2010-10-18 09:12:52 UTC (rev 117625)
@@ -2,18 +2,20 @@
 Changes
 =======
 
-1.0.4 (unreleased)
-------------------
+1.1 (unreleased)
+----------------
 
-* ...
+* Moved concrete IAuthenticatorPlugin implementations from
+  zope.app.authentication to zope.pluggableauth.plugins.
 
+  As a result, projects that do not need the ZMI views for the authenticator
+  plugins registered do not pull in zope.app.* packages anymore.
 
 1.0.3 (2010-07-09)
 ------------------
 
 * Fixed dependency declaration.
 
-
 1.0.2 (2010-07-90)
 ------------------
 
@@ -23,7 +25,6 @@
   won't be changed.
   (https://mail.zope.org/pipermail/zope-dev/2010-July/040898.html)
 
-
 1.0.1 (2010-02-11)
 ------------------
 
@@ -31,7 +32,6 @@
   `principalfactories.zcml`. This avoids duplication errors in
   ``zope.app.authentication``.
 
-
 1.0 (2010-02-05)
 ----------------
 

Modified: zope.pluggableauth/trunk/setup.py
===================================================================
--- zope.pluggableauth/trunk/setup.py	2010-10-18 08:52:55 UTC (rev 117624)
+++ zope.pluggableauth/trunk/setup.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -33,7 +33,7 @@
       author_email='zope-dev at zope.org',
       description='Pluggable Authentication Utility',
       long_description= "%s\n\n%s\n\n%s" % (
-        read('README.txt'), 
+        read('README.txt'),
         read('src', 'zope', 'pluggableauth', 'README.txt'),
         read('CHANGES.txt')),
       url='http://pypi.python.org/pypi/zope.pluggableauth',
@@ -54,12 +54,14 @@
           'zope.event',
           'zope.i18nmessageid',
           'zope.interface',
+          'zope.password >= 3.5.1',
           'zope.publisher>=3.12',
           'zope.schema',
           'zope.security',
           'zope.session',
           'zope.site',
-          'zope.traversing'],
+          'zope.traversing',
+          ],
      classifiers = [
           'Development Status :: 5 - Production/Stable',
           'Environment :: Web Environment',

Modified: zope.pluggableauth/trunk/src/zope/pluggableauth/__init__.py
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/__init__.py	2010-10-18 08:52:55 UTC (rev 117624)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/__init__.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -14,5 +14,4 @@
 """Pluggable Authentication Utility
 """
 
-from zope.pluggableauth import interfaces
 from zope.pluggableauth.authentication import PluggableAuthentication

Modified: zope.pluggableauth/trunk/src/zope/pluggableauth/interfaces.py
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/interfaces.py	2010-10-18 08:52:55 UTC (rev 117624)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/interfaces.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -247,3 +247,66 @@
 
 class IQueriableAuthenticator(zope.interface.Interface):
     """Indicates the authenticator provides a search UI for principals."""
+
+
+class IPrincipal(zope.security.interfaces.IGroupClosureAwarePrincipal):
+
+    groups = zope.schema.List(
+        title=_("Groups"),
+        description=_(
+            """ids of groups to which the principal directly belongs.
+
+            Plugins may append to this list.  Mutating the list only affects
+            the life of the principal object, and does not persist (so
+            persistently adding groups to a principal should be done by working
+            with a plugin that mutates this list every time the principal is
+            created, like the group folder in this package.)
+            """),
+        value_type=zope.schema.TextLine(),
+        required=False)
+
+
+class IQuerySchemaSearch(zope.interface.Interface):
+    """An interface for searching using schema-constrained input."""
+
+    schema = zope.interface.Attribute("""
+        The schema that constrains the input provided to the search method.
+
+        A mapping of name/value pairs for each field in this schema is used
+        as the query argument in the search method.
+        """)
+
+    def search(query, start=None, batch_size=None):
+        """Returns an iteration of principal IDs matching the query.
+
+        query is a mapping of name/value pairs for fields specified by the
+        schema.
+
+        If the start argument is provided, then it should be an
+        integer and the given number of initial items should be
+        skipped.
+
+        If the batch_size argument is provided, then it should be an
+        integer and no more than the given number of items should be
+        returned.
+        """
+
+
+class IGroupAdded(zope.interface.Interface):
+    """A group has been added."""
+
+    group = zope.interface.Attribute("""The group that was defined""")
+
+
+class IPrincipalsAddedToGroup(zope.interface.Interface):
+    group_id = zope.interface.Attribute(
+        'the id of the group to which the principal was added')
+    principal_ids = zope.interface.Attribute(
+        'an iterable of one or more ids of principals added')
+
+
+class IPrincipalsRemovedFromGroup(zope.interface.Interface):
+    group_id = zope.interface.Attribute(
+        'the id of the group from which the principal was removed')
+    principal_ids = zope.interface.Attribute(
+        'an iterable of one or more ids of principals removed')

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.py (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/groupfolder.py)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.py	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,405 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Zope Groups Folder implementation
+
+$Id$
+
+"""
+import BTrees.OOBTree
+import persistent
+
+from zope import interface, event, schema, component
+from zope.interface import alsoProvides, implements
+from zope.security.interfaces import (
+    IGroup, IGroupAwarePrincipal, IMemberAwareGroup)
+
+from zope.container.btree import BTreeContainer
+import zope.container.constraints
+import zope.container.interfaces
+from zope.i18nmessageid import MessageFactory
+import zope.authentication.principal
+
+from zope.authentication.interfaces import (
+    IAuthentication, IAuthenticatedGroup, IEveryoneGroup)
+
+from zope.pluggableauth.interfaces import (
+    IPrincipalInfo, IFoundPrincipalCreated,
+    IAuthenticatorPlugin, IQuerySchemaSearch,
+    IPrincipalsAddedToGroup, IPrincipalsRemovedFromGroup, IGroupAdded)
+
+from zope.pluggableauth import factories
+
+_ = MessageFactory('zope')
+
+class IGroupInformation(interface.Interface):
+
+    title = schema.TextLine(
+        title=_("Title"),
+        description=_("Provides a title for the permission."),
+        required=True)
+
+    description = schema.Text(
+        title=_("Description"),
+        description=_("Provides a description for the permission."),
+        required=False)
+
+    principals = schema.List(
+        title=_("Principals"),
+        value_type=schema.Choice(
+            source=zope.authentication.principal.PrincipalSource()),
+        description=_(
+        "List of ids of principals which belong to the group"),
+        required=False)
+
+
+class IGroupFolder(zope.container.interfaces.IContainer):
+
+    zope.container.constraints.contains(IGroupInformation)
+
+    prefix = schema.TextLine(
+        title=_("Group ID prefix"),
+        description=_("Prefix added to IDs of groups in this folder"),
+        readonly=True,
+        )
+
+    def getGroupsForPrincipal(principalid):
+        """Get groups the given principal belongs to"""
+
+    def getPrincipalsForGroup(groupid):
+        """Get principals which belong to the group"""
+
+
+class IGroupContained(zope.container.interfaces.IContained):
+
+    zope.container.constraints.containers(IGroupFolder)
+
+class IGroupSearchCriteria(interface.Interface):
+
+    search = schema.TextLine(
+        title=_("Group Search String"),
+        required=False,
+        missing_value=u'',
+        )
+
+class IGroupPrincipalInfo(IPrincipalInfo):
+    members = interface.Attribute('an iterable of members of the group')
+
+class GroupInfo(object):
+    """An implementation of IPrincipalInfo used by the group folder.
+
+    A group info is created with id, title, and description:
+
+      >>> class DemoGroupInformation(object):
+      ...     interface.implements(IGroupInformation)
+      ...     def __init__(self, title, description, principals):
+      ...         self.title = title
+      ...         self.description = description
+      ...         self.principals = principals
+      ...
+      >>> i = DemoGroupInformation(
+      ...     'Managers', 'Taskmasters', ('joe', 'jane'))
+      ...
+      >>> info = GroupInfo('groups.managers', i)
+      >>> info
+      GroupInfo('groups.managers')
+      >>> info.id
+      'groups.managers'
+      >>> info.title
+      'Managers'
+      >>> info.description
+      'Taskmasters'
+      >>> info.members
+      ('joe', 'jane')
+      >>> info.members = ('joe', 'jane', 'jaime')
+      >>> info.members
+      ('joe', 'jane', 'jaime')
+
+    """
+    interface.implements(IGroupPrincipalInfo)
+
+    def __init__(self, id, information):
+        self.id = id
+        self._information = information
+
+    @property
+    def title(self):
+        return self._information.title
+
+    @property
+    def description(self):
+        return self._information.description
+
+    @apply
+    def members():
+        def get(self):
+            return self._information.principals
+        def set(self, value):
+            self._information.principals = value
+        return property(get, set)
+
+    def __repr__(self):
+        return 'GroupInfo(%r)' % self.id
+
+
+class GroupFolder(BTreeContainer):
+
+    interface.implements(
+        IAuthenticatorPlugin, IQuerySchemaSearch, IGroupFolder)
+
+    schema = IGroupSearchCriteria
+
+    def __init__(self, prefix=u''):
+        super(GroupFolder, self).__init__()
+        self.prefix = prefix
+        # __inversemapping is used to map principals to groups
+        self.__inverseMapping = BTrees.OOBTree.OOBTree()
+
+    def __setitem__(self, name, value):
+        BTreeContainer.__setitem__(self, name, value)
+        group_id = self._groupid(value)
+        self._addPrincipalsToGroup(value.principals, group_id)
+        if value.principals:
+            event.notify(
+                PrincipalsAddedToGroup(
+                    value.principals, self.__parent__.prefix + group_id))
+        group = factories.Principal(self.prefix + name)
+        event.notify(GroupAdded(group))
+
+    def __delitem__(self, name):
+        value = self[name]
+        group_id = self._groupid(value)
+        self._removePrincipalsFromGroup(value.principals, group_id)
+        if value.principals:
+            event.notify(
+                PrincipalsRemovedFromGroup(
+                    value.principals, self.__parent__.prefix + group_id))
+        BTreeContainer.__delitem__(self, name)
+
+    def _groupid(self, group):
+        return self.prefix+group.__name__
+
+    def _addPrincipalsToGroup(self, principal_ids, group_id):
+        for principal_id in principal_ids:
+            self.__inverseMapping[principal_id] = (
+                self.__inverseMapping.get(principal_id, ())
+                + (group_id,))
+
+    def _removePrincipalsFromGroup(self, principal_ids, group_id):
+        for principal_id in principal_ids:
+            groups = self.__inverseMapping.get(principal_id)
+            if groups is None:
+                return
+            new = tuple([id for id in groups if id != group_id])
+            if new:
+                self.__inverseMapping[principal_id] = new
+            else:
+                del self.__inverseMapping[principal_id]
+
+    def getGroupsForPrincipal(self, principalid):
+        """Get groups the given principal belongs to"""
+        return self.__inverseMapping.get(principalid, ())
+
+    def getPrincipalsForGroup(self, groupid):
+        """Get principals which belong to the group"""
+        return self[groupid].principals
+
+    def search(self, query, start=None, batch_size=None):
+        """ Search for groups"""
+        search = query.get('search')
+        if search is not None:
+            n = 0
+            search = search.lower()
+            for i, (id, groupinfo) in enumerate(self.items()):
+                if (search in groupinfo.title.lower() or
+                    (groupinfo.description and
+                     search in groupinfo.description.lower())):
+                    if not ((start is not None and i < start)
+                            or
+                            (batch_size is not None and n >= batch_size)):
+                        n += 1
+                        yield self.prefix + id
+
+    def authenticateCredentials(self, credentials):
+        # user folders don't authenticate
+        pass
+
+    def principalInfo(self, id):
+        if id.startswith(self.prefix):
+            id = id[len(self.prefix):]
+            info = self.get(id)
+            if info is not None:
+                return GroupInfo(
+                    self.prefix+id, info)
+
+class GroupCycle(Exception):
+    """There is a cyclic relationship among groups
+    """
+
+class InvalidPrincipalIds(Exception):
+    """A user has a group id for a group that can't be found
+    """
+
+class InvalidGroupId(Exception):
+    """A user has a group id for a group that can't be found
+    """
+
+def nocycles(principal_ids, seen, getPrincipal):
+    for principal_id in principal_ids:
+        if principal_id in seen:
+            raise GroupCycle(principal_id, seen)
+        seen.append(principal_id)
+        principal = getPrincipal(principal_id)
+        nocycles(principal.groups, seen, getPrincipal)
+        seen.pop()
+
+class GroupInformation(persistent.Persistent):
+
+    interface.implements(IGroupInformation, IGroupContained)
+
+    __parent__ = __name__ = None
+
+    _principals = ()
+
+    def __init__(self, title='', description=''):
+        self.title = title
+        self.description = description
+
+    def setPrincipals(self, prinlist, check=True):
+        # method is not a part of the interface
+        parent = self.__parent__
+        old = self._principals
+        self._principals = tuple(prinlist)
+
+        if parent is not None:
+            oldset = set(old)
+            new = set(prinlist)
+            group_id = parent._groupid(self)
+            removed = oldset - new
+            added = new - oldset
+            try:
+                parent._removePrincipalsFromGroup(removed, group_id)
+            except AttributeError:
+                removed = None
+
+            try:
+                parent._addPrincipalsToGroup(added, group_id)
+            except AttributeError:
+                added = None
+
+            if check:
+                try:
+                    principalsUtility = component.getUtility(IAuthentication)
+                    nocycles(new, [], principalsUtility.getPrincipal)
+                except GroupCycle:
+                    # abort
+                    self.setPrincipals(old, False)
+                    raise
+            # now that we've gotten past the checks, fire the events.
+            if removed:
+                event.notify(
+                    PrincipalsRemovedFromGroup(
+                        removed, self.__parent__.__parent__.prefix + group_id))
+            if added:
+                event.notify(
+                    PrincipalsAddedToGroup(
+                        added, self.__parent__.__parent__.prefix + group_id))
+
+    principals = property(lambda self: self._principals, setPrincipals)
+
+
+def specialGroups(event):
+    principal = event.principal
+    if (IGroup.providedBy(principal) or
+        not IGroupAwarePrincipal.providedBy(principal)):
+        return
+
+    everyone = component.queryUtility(IEveryoneGroup)
+    if everyone is not None:
+        principal.groups.append(everyone.id)
+
+    auth = component.queryUtility(IAuthenticatedGroup)
+    if auth is not None:
+        principal.groups.append(auth.id)
+
+
+def setGroupsForPrincipal(event):
+    """Set group information when a principal is created"""
+
+    principal = event.principal
+    if not IGroupAwarePrincipal.providedBy(principal):
+        return
+
+    authentication = event.authentication
+
+    for name, plugin in authentication.getAuthenticatorPlugins():
+        if not IGroupFolder.providedBy(plugin):
+            continue
+        groupfolder = plugin
+        principal.groups.extend(
+            [authentication.prefix + id
+             for id in groupfolder.getGroupsForPrincipal(principal.id)
+             ])
+        id = principal.id
+        prefix = authentication.prefix + groupfolder.prefix
+        if id.startswith(prefix) and id[len(prefix):] in groupfolder:
+            alsoProvides(principal, IGroup)
+
+ at component.adapter(IFoundPrincipalCreated)
+def setMemberSubscriber(event):
+    """adds `getMembers`, `setMembers` to groups made from IGroupPrincipalInfo.
+    """
+    info = event.info
+    if IGroupPrincipalInfo.providedBy(info):
+        principal = event.principal
+        principal.getMembers = lambda : info.members
+        def setMembers(value):
+            info.members = value
+        principal.setMembers = setMembers
+        alsoProvides(principal, IMemberAwareGroup)
+
+
+class GroupAdded:
+    """
+    >>> from zope.interface.verify import verifyObject
+    >>> event = GroupAdded("group")
+    >>> verifyObject(IGroupAdded, event)
+    True
+    """
+
+    zope.interface.implements(IGroupAdded)
+
+    def __init__(self, group):
+        self.group = group
+
+    def __repr__(self):
+        return "<GroupAdded %r>" % self.group.id
+
+
+class AbstractMembersChanged(object):
+
+    def __init__(self, principal_ids, group_id):
+        self.principal_ids = principal_ids
+        self.group_id = group_id
+
+    def __repr__(self):
+        return "<%s %r %r>" % (
+            self.__class__.__name__, sorted(self.principal_ids), self.group_id)
+
+
+class PrincipalsAddedToGroup(AbstractMembersChanged):
+    implements(IPrincipalsAddedToGroup)
+
+
+class PrincipalsRemovedFromGroup(AbstractMembersChanged):
+    implements(IPrincipalsRemovedFromGroup)

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.txt (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/groupfolder.txt)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.txt	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.txt	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,425 @@
+=============
+Group Folders
+=============
+
+Group folders provide support for groups information stored in the ZODB.  They
+are persistent, and must be contained within the PAUs that use them.
+
+Like other principals, groups are created when they are needed.
+
+Group folders contain group-information objects that contain group information.
+We create group information using the `GroupInformation` class:
+
+  >>> import zope.pluggableauth.plugins.groupfolder
+  >>> g1 = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group 1")
+
+  >>> groups = zope.pluggableauth.plugins.groupfolder.GroupFolder('group.')
+  >>> groups['g1'] = g1
+
+Note that when group-info is added, a GroupAdded event is generated:
+
+  >>> from zope.pluggableauth import interfaces
+  >>> from zope.component.eventtesting import getEvents
+  >>> getEvents(interfaces.IGroupAdded)
+  [<GroupAdded 'group.g1'>]
+
+Groups are defined with respect to an authentication service.  Groups
+must be accessible via an authentication service and can contain
+principals accessible via an authentication service.
+
+To illustrate the group interaction with the authentication service,
+we'll create a sample authentication service:
+
+  >>> from zope import interface
+  >>> from zope.authentication.interfaces import IAuthentication
+  >>> from zope.authentication.interfaces import PrincipalLookupError
+  >>> from zope.security.interfaces import IGroupAwarePrincipal
+  >>> from zope.pluggableauth.plugins.groupfolder import setGroupsForPrincipal
+
+  >>> class Principal:
+  ...     interface.implements(IGroupAwarePrincipal)
+  ...     def __init__(self, id, title='', description=''):
+  ...         self.id, self.title, self.description = id, title, description
+  ...         self.groups = []
+
+  >>> class PrincipalCreatedEvent:
+  ...     def __init__(self, authentication, principal):
+  ...         self.authentication = authentication
+  ...         self.principal = principal
+
+  >>> from zope.pluggableauth.plugins import principalfolder
+
+  >>> class Principals:
+  ...
+  ...     interface.implements(IAuthentication)
+  ...
+  ...     def __init__(self, groups, prefix='auth.'):
+  ...         self.prefix = prefix
+  ...         self.principals = {
+  ...            'p1': principalfolder.PrincipalInfo('p1', '', '', ''),
+  ...            'p2': principalfolder.PrincipalInfo('p2', '', '', ''),
+  ...            'p3': principalfolder.PrincipalInfo('p3', '', '', ''),
+  ...            'p4': principalfolder.PrincipalInfo('p4', '', '', ''),
+  ...            }
+  ...         self.groups = groups
+  ...         groups.__parent__ = self
+  ...
+  ...     def getAuthenticatorPlugins(self):
+  ...         return [('principals', self.principals), ('groups', self.groups)]
+  ...
+  ...     def getPrincipal(self, id):
+  ...         if not id.startswith(self.prefix):
+  ...             raise PrincipalLookupError(id)
+  ...         id = id[len(self.prefix):]
+  ...         info = self.principals.get(id)
+  ...         if info is None:
+  ...             info = self.groups.principalInfo(id)
+  ...             if info is None:
+  ...                raise PrincipalLookupError(id)
+  ...         principal = Principal(self.prefix+info.id,
+  ...                               info.title, info.description)
+  ...         setGroupsForPrincipal(PrincipalCreatedEvent(self, principal))
+  ...         return principal
+
+This class doesn't really implement the full `IAuthentication` interface, but
+it implements the `getPrincipal` method used by groups. It works very much
+like the pluggable authentication utility.  It creates principals on demand. It
+calls `setGroupsForPrincipal`, which is normally called as an event subscriber,
+when principals are created. In order for `setGroupsForPrincipal` to find out
+group folder, we have to register it as a utility:
+
+  >>> from zope.pluggableauth.interfaces import IAuthenticatorPlugin
+  >>> from zope.component import provideUtility
+  >>> provideUtility(groups, IAuthenticatorPlugin)
+
+We will create and register a new principals utility:
+
+  >>> principals = Principals(groups)
+  >>> provideUtility(principals, IAuthentication)
+
+Now we can set the principals on the group:
+
+  >>> g1.principals = ['auth.p1', 'auth.p2']
+  >>> g1.principals
+  ('auth.p1', 'auth.p2')
+
+Adding principals fires an event.
+
+  >>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
+  <PrincipalsAddedToGroup ['auth.p1', 'auth.p2'] u'auth.group.g1'>
+
+We can now look up groups for the principals:
+
+  >>> groups.getGroupsForPrincipal('auth.p1')
+  (u'group.g1',)
+
+Note that the group id is a concatenation of the group-folder prefix
+and the name of the group-information object within the folder.
+
+If we delete a group:
+
+  >>> del groups['g1']
+
+then the groups folder loses the group information for that group's
+principals:
+
+  >>> groups.getGroupsForPrincipal('auth.p1')
+  ()
+
+but the principal information on the group is unchanged:
+
+  >>> g1.principals
+  ('auth.p1', 'auth.p2')
+
+It also fires an event showing that the principals are removed from the group
+(g1 is group information, not a zope.security.interfaces.IGroup).
+
+  >>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1]
+  <PrincipalsRemovedFromGroup ['auth.p1', 'auth.p2'] u'auth.group.g1'>
+
+Adding the group sets the folder principal information.  Let's use a
+different group name:
+
+  >>> groups['G1'] = g1
+
+  >>> groups.getGroupsForPrincipal('auth.p1')
+  (u'group.G1',)
+
+Here we see that the new name is reflected in the group information.
+
+An event is fired, as usual.
+
+  >>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
+  <PrincipalsAddedToGroup ['auth.p1', 'auth.p2'] u'auth.group.G1'>
+
+In terms of member events (principals added and removed from groups), we have
+now seen that events are fired when a group information object is added and
+when it is removed from a group folder; and we have seen that events are fired
+when a principal is added to an already-registered group.  Events are also
+fired when a principal is removed from an already-registered group.  Let's
+quickly see some more examples.
+
+  >>> g1.principals = ('auth.p1', 'auth.p3', 'auth.p4')
+  >>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
+  <PrincipalsAddedToGroup ['auth.p3', 'auth.p4'] u'auth.group.G1'>
+  >>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1]
+  <PrincipalsRemovedFromGroup ['auth.p2'] u'auth.group.G1'>
+  >>> g1.principals = ('auth.p1', 'auth.p2')
+  >>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
+  <PrincipalsAddedToGroup ['auth.p2'] u'auth.group.G1'>
+  >>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1]
+  <PrincipalsRemovedFromGroup ['auth.p3', 'auth.p4'] u'auth.group.G1'>
+
+Groups can contain groups:
+
+  >>> g2 = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group Two")
+  >>> groups['G2'] = g2
+  >>> g2.principals = ['auth.group.G1']
+
+  >>> groups.getGroupsForPrincipal('auth.group.G1')
+  (u'group.G2',)
+
+  >>> old = getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
+  >>> old
+  <PrincipalsAddedToGroup ['auth.group.G1'] u'auth.group.G2'>
+
+Groups cannot contain cycles:
+
+  >>> g1.principals = ('auth.p1', 'auth.p2', 'auth.group.G2')
+  ... # doctest: +NORMALIZE_WHITESPACE
+  Traceback (most recent call last):
+  ...
+  GroupCycle: (u'auth.group.G1',
+               ['auth.p2', u'auth.group.G1', u'auth.group.G2'])
+
+Trying to do so does not fire an event.
+
+  >>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1] is old
+  True
+
+They need not be hierarchical:
+
+  >>> ga = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group A")
+  >>> groups['GA'] = ga
+
+  >>> gb = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group B")
+  >>> groups['GB'] = gb
+  >>> gb.principals = ['auth.group.GA']
+
+  >>> gc = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group C")
+  >>> groups['GC'] = gc
+  >>> gc.principals = ['auth.group.GA']
+
+  >>> gd = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group D")
+  >>> groups['GD'] = gd
+  >>> gd.principals = ['auth.group.GA', 'auth.group.GB']
+
+  >>> ga.principals = ['auth.p1']
+
+Group folders provide a very simple search interface.  They perform
+simple string searches on group titles and descriptions.
+
+  >>> list(groups.search({'search': 'grou'})) # doctest: +NORMALIZE_WHITESPACE
+  [u'group.G1', u'group.G2',
+   u'group.GA', u'group.GB', u'group.GC', u'group.GD']
+
+  >>> list(groups.search({'search': 'two'}))
+  [u'group.G2']
+
+They also support batching:
+
+  >>> list(groups.search({'search': 'grou'}, 2, 3))
+  [u'group.GA', u'group.GB', u'group.GC']
+
+
+If you don't supply a search key, no results will be returned:
+
+  >>> list(groups.search({}))
+  []
+
+Identifying groups
+------------------
+The function, `setGroupsForPrincipal`, is a subscriber to
+principal-creation events.  It adds any group-folder-defined groups to
+users in those groups:
+
+  >>> principal = principals.getPrincipal('auth.p1')
+
+  >>> principal.groups
+  [u'auth.group.G1', u'auth.group.GA']
+
+Of course, this applies to groups too:
+
+  >>> principal = principals.getPrincipal('auth.group.G1')
+  >>> principal.id
+  'auth.group.G1'
+
+  >>> principal.groups
+  [u'auth.group.G2']
+
+In addition to setting principal groups, the `setGroupsForPrincipal`
+function also declares the `IGroup` interface on groups:
+
+  >>> [iface.__name__ for iface in interface.providedBy(principal)]
+  ['IGroup', 'IGroupAwarePrincipal']
+
+  >>> [iface.__name__
+  ...  for iface in interface.providedBy(principals.getPrincipal('auth.p1'))]
+  ['IGroupAwarePrincipal']
+
+Special groups
+--------------
+Two special groups, Authenticated, and Everyone may apply to users
+created by the pluggable-authentication utility.  There is a
+subscriber, specialGroups, that will set these groups on any non-group
+principals if IAuthenticatedGroup, or IEveryoneGroup utilities are
+provided.
+
+Lets define a group-aware principal:
+
+  >>> import zope.security.interfaces
+  >>> class GroupAwarePrincipal(Principal):
+  ...     interface.implements(zope.security.interfaces.IGroupAwarePrincipal)
+  ...     def __init__(self, id):
+  ...         Principal.__init__(self, id)
+  ...         self.groups = []
+
+If we notify the subscriber with this principal, nothing will happen
+because the groups haven't been defined:
+
+  >>> prin = GroupAwarePrincipal('x')
+  >>> event = interfaces.FoundPrincipalCreated(42, prin, {})
+  >>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
+  >>> prin.groups
+  []
+
+Now, if we define the Everybody group:
+
+  >>> import zope.authentication.interfaces
+  >>> class EverybodyGroup(Principal):
+  ...     interface.implements(zope.authentication.interfaces.IEveryoneGroup)
+
+  >>> everybody = EverybodyGroup('all')
+  >>> provideUtility(everybody, zope.authentication.interfaces.IEveryoneGroup)
+
+Then the group will be added to the principal:
+
+  >>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
+  >>> prin.groups
+  ['all']
+
+Similarly for the authenticated group:
+
+  >>> class AuthenticatedGroup(Principal):
+  ...     interface.implements(
+  ...         zope.authentication.interfaces.IAuthenticatedGroup)
+
+  >>> authenticated = AuthenticatedGroup('auth')
+  >>> provideUtility(authenticated, zope.authentication.interfaces.IAuthenticatedGroup)
+
+Then the group will be added to the principal:
+
+  >>> prin.groups = []
+  >>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
+  >>> prin.groups.sort()
+  >>> prin.groups
+  ['all', 'auth']
+
+These groups are only added to non-group principals:
+
+  >>> prin.groups = []
+  >>> interface.directlyProvides(prin, zope.security.interfaces.IGroup)
+  >>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
+  >>> prin.groups
+  []
+
+And they are only added to group aware principals:
+
+  >>> class SolitaryPrincipal:
+  ...     interface.implements(zope.security.interfaces.IPrincipal)
+  ...     id = title = description = ''
+
+  >>> event = interfaces.FoundPrincipalCreated(42, SolitaryPrincipal(), {})
+  >>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
+  >>> prin.groups
+  []
+
+Member-aware groups
+-------------------
+The groupfolder includes a subscriber that gives group principals the
+zope.security.interfaces.IGroupAware interface and an implementation thereof.
+This allows groups to be able to get and set their members.
+
+Given an info object and a group...
+
+    >>> class DemoGroupInformation(object):
+    ...     interface.implements(
+    ...         zope.pluggableauth.plugins.groupfolder.IGroupInformation)
+    ...     def __init__(self, title, description, principals):
+    ...         self.title = title
+    ...         self.description = description
+    ...         self.principals = principals
+    ...
+    >>> i = DemoGroupInformation(
+    ...     'Managers', 'Taskmasters', ('joe', 'jane'))
+    ...
+    >>> info = zope.pluggableauth.plugins.groupfolder.GroupInfo(
+    ...     'groups.managers', i)
+    >>> class DummyGroup(object):
+    ...     interface.implements(IGroupAwarePrincipal)
+    ...     def __init__(self, id, title=u'', description=u''):
+    ...         self.id = id
+    ...         self.title = title
+    ...         self.description = description
+    ...         self.groups = []
+    ...
+    >>> principal = DummyGroup('foo')
+    >>> zope.security.interfaces.IMemberAwareGroup.providedBy(principal)
+    False
+
+...when you call the subscriber, it adds the two pseudo-methods to the
+principal and makes the principal provide the IMemberAwareGroup interface.
+
+    >>> zope.pluggableauth.plugins.groupfolder.setMemberSubscriber(
+    ...     interfaces.FoundPrincipalCreated(
+    ...         'dummy auth (ignored)', principal, info))
+    >>> principal.getMembers()
+    ('joe', 'jane')
+    >>> principal.setMembers(('joe', 'jane', 'jaimie'))
+    >>> principal.getMembers()
+    ('joe', 'jane', 'jaimie')
+    >>> zope.security.interfaces.IMemberAwareGroup.providedBy(principal)
+    True
+
+The two methods work with the value on the IGroupInformation object.
+
+    >>> i.principals == principal.getMembers()
+    True
+
+Limitation
+==========
+
+The current group-folder design has an important limitation!
+
+There is no point in assigning principals to a group
+from a group folder unless the principal is from the same pluggable
+authentication utility.
+
+o If a principal is from a higher authentication utility, the user
+  will not get the group definition. Why? Because the principals
+  group assignments are set when the principal is authenticated. At
+  that point, the current site is the site containing the principal
+  definition. Groups defined in lower sites will not be consulted,
+
+o It is impossible to assign users from lower authentication
+  utilities because they can't be seen when managing the group,
+  from the site containing the group.
+
+A better design might be to store user-role assignments independent of
+the group definitions and to look for assignments during (url)
+traversal.  This could get quite complex though.
+
+While it is possible to have multiple authentication utilities long a
+URL path, it is generally better to stick to a simpler model in which
+there is only one authentication utility along a URL path (in addition
+to the global utility, which is used for bootstrapping purposes).

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.zcml (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/groupfolder.zcml)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.zcml	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/groupfolder.zcml	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,21 @@
+<configure
+  xmlns="http://namespaces.zope.org/zope"
+  i18n_domain="zope"
+  >
+  <adapter
+    provides="zope.container.interfaces.INameChooser"
+    for=".groupfolder.IGroupFolder"
+    factory=".idpicker.IdPicker"
+  />
+  <subscriber
+    for="zope.pluggableauth.interfaces.IPrincipalCreated"
+    handler=".groupfolder.specialGroups"
+  />
+  <subscriber
+    for="zope.pluggableauth.interfaces.IPrincipalCreated"
+    handler=".groupfolder.setGroupsForPrincipal"
+  />
+  <subscriber
+    handler=".groupfolder.setMemberSubscriber"
+  />
+</configure>

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/idpicker.py (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/idpicker.py)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/idpicker.py	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/idpicker.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,112 @@
+# -*- coding: utf-8 -*-
+# ##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Helper base class that picks principal ids
+
+$Id: idpicker.py 117492 2010-10-13 08:17:55Z janwijbrand $
+"""
+__docformat__ = 'restructuredtext'
+
+import re
+from zope.container.contained import NameChooser
+from zope.exceptions.interfaces import UserError
+from zope.i18nmessageid import MessageFactory
+
+_ = MessageFactory('zope')
+
+ok = re.compile('[!-~]+$').match
+class IdPicker(NameChooser):
+    """Helper base class that picks principal ids.
+
+    Add numbers to ids given by users to make them unique.
+
+    The Id picker is a variation on the name chooser that picks numeric
+    ids when no name is given.
+
+      >>> from zope.pluggableauth.plugins.idpicker import IdPicker
+      >>> IdPicker({}).chooseName('', None)
+      u'1'
+
+      >>> IdPicker({'1': 1}).chooseName('', None)
+      u'2'
+
+      >>> IdPicker({'2': 1}).chooseName('', None)
+      u'1'
+
+      >>> IdPicker({'1': 1}).chooseName('bob', None)
+      u'bob'
+
+      >>> IdPicker({'bob': 1}).chooseName('bob', None)
+      u'bob1'
+
+    """
+    def chooseName(self, name, object):
+        i = 0
+        name = unicode(name)
+        orig = name
+        while (not name) or (name in self.context):
+            i += 1
+            name = orig+str(i)
+
+        self.checkName(name, object)
+        return name
+
+    def checkName(self, name, object):
+        """Limit ids
+
+        Ids can only contain printable, non-space, 7-bit ASCII strings:
+
+        >>> from zope.pluggableauth.plugins.idpicker import IdPicker
+        >>> IdPicker({}).checkName(u'1', None)
+        True
+
+        >>> IdPicker({}).checkName(u'bob', None)
+        True
+
+        >>> try:
+        ...     IdPicker({}).checkName(u'bob\xfa', None)
+        ... except UserError, e:
+        ...     print e
+        ...     # doctest: +NORMALIZE_WHITESPACE
+        Ids must contain only printable 7-bit non-space ASCII characters
+
+        >>> try:
+        ...     IdPicker({}).checkName(u'big bob', None)
+        ... except UserError, e:
+        ...     print e
+        ...     # doctest: +NORMALIZE_WHITESPACE
+        Ids must contain only printable 7-bit non-space ASCII characters
+
+        Ids also can't be over 100 characters long:
+
+        >>> IdPicker({}).checkName(u'x' * 100, None)
+        True
+
+        >>> IdPicker({}).checkName(u'x' * 101, None)
+        Traceback (most recent call last):
+        ...
+        UserError: Ids can't be more than 100 characters long.
+
+        """
+        NameChooser.checkName(self, name, object)
+        if not ok(name):
+            raise UserError(
+                _("Ids must contain only printable 7-bit non-space"
+                  " ASCII characters")
+                )
+        if len(name) > 100:
+            raise UserError(
+                _("Ids can't be more than 100 characters long.")
+                )
+        return True

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.py (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/principalfolder.py)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.py	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,284 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ZODB-based Authentication Source
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from persistent import Persistent
+from zope.component import getUtility
+from zope.container.btree import BTreeContainer
+from zope.container.constraints import contains, containers
+from zope.container.contained import Contained
+from zope.container.interfaces import DuplicateIDError
+from zope.i18nmessageid import MessageFactory
+from zope.interface import implements, Interface
+from zope.password.interfaces import IPasswordManager
+from zope.schema import Text, TextLine, Password, Choice
+from zope.pluggableauth.interfaces import (
+    IAuthenticatorPlugin, IQuerySchemaSearch)
+from zope.pluggableauth.factories import PrincipalInfo
+
+_ = MessageFactory('zope')
+
+class IInternalPrincipal(Interface):
+    """Principal information"""
+
+    login = TextLine(
+        title=_("Login"),
+        description=_("The Login/Username of the principal. "
+                      "This value can change."))
+
+    def setPassword(password, passwordManagerName=None):
+        pass
+
+    password = Password(
+        title=_("Password"),
+        description=_("The password for the principal."))
+
+    passwordManagerName = Choice(
+        title=_("Password Manager"),
+        vocabulary="Password Manager Names",
+        description=_("The password manager will be used"
+            " for encode/check the password"),
+        default="SSHA",
+        # TODO: The password manager name may be changed only
+        # if the password changed
+        readonly=True
+        )
+
+    title = TextLine(
+        title=_("Title"),
+        description=_("Provides a title for the principal."))
+
+    description = Text(
+        title=_("Description"),
+        description=_("Provides a description for the principal."),
+        required=False,
+        missing_value='',
+        default=u'')
+
+
+class IInternalPrincipalContainer(Interface):
+    """A container that contains internal principals."""
+
+    prefix = TextLine(
+        title=_("Prefix"),
+        description=_(
+        "Prefix to be added to all principal ids to assure "
+        "that all ids are unique within the authentication service"),
+        missing_value=u"",
+        default=u'',
+        readonly=True)
+
+    def getIdByLogin(login):
+        """Return the principal id currently associated with login.
+
+        The return value includes the container prefix, but does not
+        include the PAU prefix.
+
+        KeyError is raised if no principal is associated with login.
+
+        """
+
+    contains(IInternalPrincipal)
+
+
+class IInternalPrincipalContained(Interface):
+    """Principal information"""
+
+    containers(IInternalPrincipalContainer)
+
+
+class ISearchSchema(Interface):
+    """Search Interface for this Principal Provider"""
+
+    search = TextLine(
+        title=_("Search String"),
+        description=_("A Search String"),
+        required=False,
+        default=u'',
+        missing_value=u'')
+
+
+class InternalPrincipal(Persistent, Contained):
+    """An internal principal for Persistent Principal Folder."""
+
+    implements(IInternalPrincipal, IInternalPrincipalContained)
+
+    # If you're searching for self._passwordManagerName, or self._password
+    # probably you just need to evolve the database to new generation
+    # at /++etc++process/@@generations.html
+
+    # NOTE: All changes needs to be synchronized with the evolver at
+    # zope.app.zopeappgenerations.evolve2
+
+    def __init__(self, login, password, title, description=u'',
+            passwordManagerName="SSHA"):
+        self._login = login
+        self._passwordManagerName = passwordManagerName
+        self.password = password
+        self.title = title
+        self.description = description
+
+    def getPasswordManagerName(self):
+        return self._passwordManagerName
+
+    passwordManagerName = property(getPasswordManagerName)
+
+    def _getPasswordManager(self):
+        return getUtility(IPasswordManager, self.passwordManagerName)
+
+    def getPassword(self):
+        return self._password
+
+    def setPassword(self, password, passwordManagerName=None):
+        if passwordManagerName is not None:
+            self._passwordManagerName = passwordManagerName
+        passwordManager = self._getPasswordManager()
+        self._password = passwordManager.encodePassword(password)
+
+    password = property(getPassword, setPassword)
+
+    def checkPassword(self, password):
+        passwordManager = self._getPasswordManager()
+        return passwordManager.checkPassword(self.password, password)
+
+    def getLogin(self):
+        return self._login
+
+    def setLogin(self, login):
+        oldLogin = self._login
+        self._login = login
+        if self.__parent__ is not None:
+            try:
+                self.__parent__.notifyLoginChanged(oldLogin, self)
+            except ValueError:
+                self._login = oldLogin
+                raise
+
+    login = property(getLogin, setLogin)
+
+
+class PrincipalFolder(BTreeContainer):
+    """A Persistent Principal Folder and Authentication plugin.
+
+    See principalfolder.txt for details.
+    """
+
+    implements(IAuthenticatorPlugin,
+               IQuerySchemaSearch,
+               IInternalPrincipalContainer)
+
+    schema = ISearchSchema
+
+    def __init__(self, prefix=''):
+        self.prefix = unicode(prefix)
+        super(PrincipalFolder, self).__init__()
+        self.__id_by_login = self._newContainerData()
+
+    def notifyLoginChanged(self, oldLogin, principal):
+        """Notify the Container about changed login of a principal.
+
+        We need this, so that our second tree can be kept up-to-date.
+        """
+        # A user with the new login already exists
+        if principal.login in self.__id_by_login:
+            raise ValueError('Principal Login already taken!')
+
+        del self.__id_by_login[oldLogin]
+        self.__id_by_login[principal.login] = principal.__name__
+
+    def __setitem__(self, id, principal):
+        """Add principal information.
+
+        Create a Principal Folder
+
+            >>> pf = PrincipalFolder()
+
+        Create a principal with 1 as id
+        Add a login attr since __setitem__ is in need of one
+
+            >>> from zope.pluggableauth.factories import Principal
+            >>> principal = Principal(1)
+            >>> principal.login = 1
+
+        Add the principal within the Principal Folder
+
+            >>> pf.__setitem__(u'1', principal)
+
+        Try to add another principal with the same id.
+        It should raise a DuplicateIDError
+
+            >>> try:
+            ...     pf.__setitem__(u'1', principal)
+            ... except DuplicateIDError, e:
+            ...     pass
+            >>>
+        """
+        # A user with the new login already exists
+        if principal.login in self.__id_by_login:
+            raise DuplicateIDError('Principal Login already taken!')
+
+        super(PrincipalFolder, self).__setitem__(id, principal)
+        self.__id_by_login[principal.login] = id
+
+    def __delitem__(self, id):
+        """Remove principal information."""
+        principal = self[id]
+        super(PrincipalFolder, self).__delitem__(id)
+        del self.__id_by_login[principal.login]
+
+    def authenticateCredentials(self, credentials):
+        """Return principal info if credentials can be authenticated
+        """
+        if not isinstance(credentials, dict):
+            return None
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+        id = self.__id_by_login.get(credentials['login'])
+        if id is None:
+            return None
+        internal = self[id]
+        if not internal.checkPassword(credentials["password"]):
+            return None
+        return PrincipalInfo(self.prefix + id, internal.login, internal.title,
+                             internal.description)
+
+    def principalInfo(self, id):
+        if id.startswith(self.prefix):
+            internal = self.get(id[len(self.prefix):])
+            if internal is not None:
+                return PrincipalInfo(id, internal.login, internal.title,
+                                     internal.description)
+
+    def getIdByLogin(self, login):
+        return self.prefix + self.__id_by_login[login]
+
+    def search(self, query, start=None, batch_size=None):
+        """Search through this principal provider."""
+        search = query.get('search')
+        if search is None:
+            return
+        search = search.lower()
+        n = 1
+        for i, value in enumerate(self.values()):
+            if (search in value.title.lower() or
+                search in value.description.lower() or
+                search in value.login.lower()):
+                if not ((start is not None and i < start)
+                        or (batch_size is not None and n > batch_size)):
+                    n += 1
+                    yield self.prefix + value.__name__

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.txt (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/principalfolder.txt)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.txt	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.txt	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,168 @@
+================
+Principal Folder
+================
+
+Principal folders contain principal-information objects that contain principal
+information. We create an internal principal using the `InternalPrincipal`
+class:
+
+  >>> from zope.pluggableauth.plugins.principalfolder import InternalPrincipal
+  >>> p1 = InternalPrincipal('login1', '123', "Principal 1",
+  ...     passwordManagerName="SHA1")
+  >>> p2 = InternalPrincipal('login2', '456', "The Other One")
+
+and add them to a principal folder:
+
+  >>> from zope.pluggableauth.plugins.principalfolder import PrincipalFolder
+  >>> principals = PrincipalFolder('principal.')
+  >>> principals['p1'] = p1
+  >>> principals['p2'] = p2
+
+Authentication
+--------------
+
+Principal folders provide the `IAuthenticatorPlugin` interface. When we
+provide suitable credentials:
+
+  >>> from pprint import pprint
+  >>> principals.authenticateCredentials({'login': 'login1', 'password': '123'})
+  PrincipalInfo(u'principal.p1')
+
+We get back a principal id and supplementary information, including the
+principal title and description.  Note that the principal id is a concatenation
+of the principal-folder prefix and the name of the principal-information object
+within the folder.
+
+None is returned if the credentials are invalid:
+
+  >>> principals.authenticateCredentials({'login': 'login1',
+  ...                                     'password': '1234'})
+  >>> principals.authenticateCredentials(42)
+
+Search
+------
+
+Principal folders also provide the IQuerySchemaSearch interface.  This
+supports both finding principal information based on their ids:
+
+  >>> principals.principalInfo('principal.p1')
+  PrincipalInfo('principal.p1')
+
+  >>> principals.principalInfo('p1')
+
+and searching for principals based on a search string:
+
+  >>> list(principals.search({'search': 'other'}))
+  [u'principal.p2']
+
+  >>> list(principals.search({'search': 'OTHER'}))
+  [u'principal.p2']
+
+  >>> list(principals.search({'search': ''}))
+  [u'principal.p1', u'principal.p2']
+
+  >>> list(principals.search({'search': 'eek'}))
+  []
+
+  >>> list(principals.search({}))
+  []
+
+If there are a large number of matches:
+
+  >>> for i in range(20):
+  ...     i = str(i)
+  ...     p = InternalPrincipal('l'+i, i, "Dude "+i)
+  ...     principals[i] = p
+
+  >>> pprint(list(principals.search({'search': 'D'})))
+  [u'principal.0',
+   u'principal.1',
+   u'principal.10',
+   u'principal.11',
+   u'principal.12',
+   u'principal.13',
+   u'principal.14',
+   u'principal.15',
+   u'principal.16',
+   u'principal.17',
+   u'principal.18',
+   u'principal.19',
+   u'principal.2',
+   u'principal.3',
+   u'principal.4',
+   u'principal.5',
+   u'principal.6',
+   u'principal.7',
+   u'principal.8',
+   u'principal.9']
+
+We can use batching parameters to specify a subset of results:
+
+  >>> pprint(list(principals.search({'search': 'D'}, start=17)))
+  [u'principal.7', u'principal.8', u'principal.9']
+
+  >>> pprint(list(principals.search({'search': 'D'}, batch_size=5)))
+  [u'principal.0',
+   u'principal.1',
+   u'principal.10',
+   u'principal.11',
+   u'principal.12']
+
+  >>> pprint(list(principals.search({'search': 'D'}, start=5, batch_size=5)))
+  [u'principal.13',
+   u'principal.14',
+   u'principal.15',
+   u'principal.16',
+   u'principal.17']
+
+There is an additional method that allows requesting the principal id
+associated with a login id.  The method raises KeyError when there is
+no associated principal::
+
+  >>> principals.getIdByLogin("not-there")
+  Traceback (most recent call last):
+  KeyError: 'not-there'
+
+If there is a matching principal, the id is returned::
+
+  >>> principals.getIdByLogin("login1")
+  u'principal.p1'
+
+Changing credentials
+--------------------
+
+Credentials can be changed by modifying principal-information objects:
+
+  >>> p1.login = 'bob'
+  >>> p1.password = 'eek'
+
+  >>> principals.authenticateCredentials({'login': 'bob', 'password': 'eek'})
+  PrincipalInfo(u'principal.p1')
+
+  >>> principals.authenticateCredentials({'login': 'login1',
+  ...                                     'password': 'eek'})
+
+  >>> principals.authenticateCredentials({'login': 'bob',
+  ...                                     'password': '123'})
+
+
+It is an error to try to pick a login name that is already taken:
+
+  >>> p1.login = 'login2'
+  Traceback (most recent call last):
+  ...
+  ValueError: Principal Login already taken!
+
+If such an attempt is made, the data are unchanged:
+
+  >>> principals.authenticateCredentials({'login': 'bob', 'password': 'eek'})
+  PrincipalInfo(u'principal.p1')
+
+Removing principals
+-------------------
+
+Of course, if a principal is removed, we can no-longer authenticate it:
+
+  >>> del principals['p1']
+  >>> principals.authenticateCredentials({'login': 'bob',
+  ...                                     'password': 'eek'})

Copied: zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.zcml (from rev 117503, zope.pluggableauth/branches/jw-authenticator-plugins/src/zope/pluggableauth/plugins/principalfolder.zcml)
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.zcml	                        (rev 0)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/plugins/principalfolder.zcml	2010-10-18 09:12:52 UTC (rev 117625)
@@ -0,0 +1,10 @@
+<configure xmlns="http://namespaces.zope.org/zope" i18n_domain="zope">
+
+  <include package="zope.pluggableauth" file="principalfactories.zcml" />
+
+  <adapter
+    provides="zope.container.interfaces.INameChooser"
+    for=".principalfolder.IInternalPrincipalContainer"
+    factory=".idpicker.IdPicker"
+  />
+</configure>

Modified: zope.pluggableauth/trunk/src/zope/pluggableauth/tests.py
===================================================================
--- zope.pluggableauth/trunk/src/zope/pluggableauth/tests.py	2010-10-18 08:52:55 UTC (rev 117624)
+++ zope.pluggableauth/trunk/src/zope/pluggableauth/tests.py	2010-10-18 09:12:52 UTC (rev 117625)
@@ -18,12 +18,11 @@
 import doctest
 import unittest
 import zope.component
-from zope.component.eventtesting import getEvents, clearEvents
 from zope.component.interfaces import IComponentLookup
 from zope.container.interfaces import ISimpleReadContainer
 from zope.container.traversal import ContainerTraversable
+from zope.interface import implements
 from zope.interface import Interface
-from zope.interface import implements
 from zope.pluggableauth.plugins.session import SessionCredentialsPlugin
 from zope.publisher import base
 from zope.publisher.interfaces import IRequest
@@ -32,6 +31,8 @@
 from zope.site.site import LocalSiteManager, SiteManagerAdapter
 from zope.traversing.interfaces import ITraversable
 from zope.traversing.testing import setUp
+import zope.component.eventtesting
+import zope.password
 from zope.session.interfaces import (
     IClientId, IClientIdManager, ISession, ISessionDataContainer)
 from zope.session.session import (
@@ -112,6 +113,13 @@
         self.assertEqual(
             plugin.logout(base.TestRequest('/')), False)
 
+def setupPassword(test):
+    from zope.password.interfaces import IPasswordManager
+    from zope.password.password import SHA1PasswordManager, SSHAPasswordManager
+    zope.component.provideUtility(
+        SHA1PasswordManager(), IPasswordManager, 'SHA1')
+    zope.component.provideUtility(
+        SSHAPasswordManager(), IPasswordManager, 'SSHA')
 
 def test_suite():
     suite = unittest.TestSuite((
@@ -120,10 +128,22 @@
         doctest.DocTestSuite('zope.pluggableauth.plugins.generic'),
         doctest.DocTestSuite('zope.pluggableauth.plugins.ftpplugins'),
         doctest.DocTestSuite('zope.pluggableauth.plugins.httpplugins'),
+
+        doctest.DocTestSuite('zope.pluggableauth.plugins.principalfolder'),
+        doctest.DocFileSuite(
+            'plugins/principalfolder.txt',
+            setUp=setupPassword),
+
+        doctest.DocTestSuite('zope.pluggableauth.plugins.groupfolder'),
+        doctest.DocFileSuite(
+            'plugins/groupfolder.txt',
+            setUp=zope.component.eventtesting.setUp),
+
         doctest.DocTestSuite(
             'zope.pluggableauth.plugins.session',
             setUp=siteSetUp,
             tearDown=siteTearDown),
+
         doctest.DocFileSuite(
             'README.txt',
             setUp=siteSetUp,
@@ -131,10 +151,10 @@
             globs={'provideUtility': zope.component.provideUtility,
                    'provideAdapter': zope.component.provideAdapter,
                    'provideHandler': zope.component.provideHandler,
-                   'getEvents': getEvents,
-                   'clearEvents': clearEvents,
+                   'getEvents': zope.component.eventtesting.getEvents,
+                   'clearEvents': zope.component.eventtesting.clearEvents,
                    }),
-        ))
+           ))
     return suite
 
 



More information about the checkins mailing list