[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