diff -Nru zope.pluggableauth-1.0.3/CHANGES.txt zope.pluggableauth-1.3/CHANGES.txt --- zope.pluggableauth-1.0.3/CHANGES.txt 2010-07-09 21:12:11.000000000 +0000 +++ zope.pluggableauth-1.3/CHANGES.txt 2011-02-08 08:55:13.000000000 +0000 @@ -2,12 +2,37 @@ Changes ======= +1.3 (2011-02-08) +---------------- + +- As the camefrom information is most probably used for a redirect, require + it to be an absolute URL (see also + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30). + +1.2 (2010-12-16) +---------------- + +- SessionCredentialsPlugin has a hook (_makeCredentials) that can be overriden + in subclasses to store the credentials in the session differently. + + For example, you could use keas.kmi and encrypt the passwords of the + currently logged-in users so they don't appear in plain text in the ZODB. + +1.1 (2010-10-18) +---------------- + +* Moved concrete IAuthenticatorPlugin implementations from + zope.app.authentication to zope.pluggableauth.plugins. + + As a result projects that want to use the IAuthenticator plugins (previously + found in zope.app.authentication) do not automatically also pull in the + zope.app.* dependencies that are needed to register the ZMI views. + 1.0.3 (2010-07-09) ------------------ * Fixed dependency declaration. - 1.0.2 (2010-07-90) ------------------ @@ -17,7 +42,6 @@ won't be changed. (https://mail.zope.org/pipermail/zope-dev/2010-July/040898.html) - 1.0.1 (2010-02-11) ------------------ @@ -25,7 +49,6 @@ `principalfactories.zcml`. This avoids duplication errors in ``zope.app.authentication``. - 1.0 (2010-02-05) ---------------- diff -Nru zope.pluggableauth-1.0.3/PKG-INFO zope.pluggableauth-1.3/PKG-INFO --- zope.pluggableauth-1.0.3/PKG-INFO 2010-07-09 21:13:42.000000000 +0000 +++ zope.pluggableauth-1.3/PKG-INFO 2011-02-08 08:55:19.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: zope.pluggableauth -Version: 1.0.3 +Version: 1.3 Summary: Pluggable Authentication Utility Home-page: http://pypi.python.org/pypi/zope.pluggableauth Author: Zope Foundation and Contributors @@ -13,6 +13,64 @@ Based on zope.authentication, this package provides a flexible and pluggable authentication utility. Several common plugins are provided. + .. contents:: + + + ======= + Changes + ======= + + 1.3 (2011-02-08) + ---------------- + + - As the camefrom information is most probably used for a redirect, require + it to be an absolute URL (see also + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30). + + 1.2 (2010-12-16) + ---------------- + + - SessionCredentialsPlugin has a hook (_makeCredentials) that can be overriden + in subclasses to store the credentials in the session differently. + + For example, you could use keas.kmi and encrypt the passwords of the + currently logged-in users so they don't appear in plain text in the ZODB. + + 1.1 (2010-10-18) + ---------------- + + * Moved concrete IAuthenticatorPlugin implementations from + zope.app.authentication to zope.pluggableauth.plugins. + + As a result projects that want to use the IAuthenticator plugins (previously + found in zope.app.authentication) do not automatically also pull in the + zope.app.* dependencies that are needed to register the ZMI views. + + 1.0.3 (2010-07-09) + ------------------ + + * Fixed dependency declaration. + + 1.0.2 (2010-07-90) + ------------------ + + * Added persistent.Persistent and zope.container.contained.Contained as + bases zope.pluggableauth.plugins.session.SessionCredentialsPlugin, so + instances of zope.app.authentication.session.SessionCredentialsPlugin + won't be changed. + (https://mail.zope.org/pipermail/zope-dev/2010-July/040898.html) + + 1.0.1 (2010-02-11) + ------------------ + + * Adapters are now declared in a new ZCML file : + `principalfactories.zcml`. This avoids duplication errors in + ``zope.app.authentication``. + + 1.0 (2010-02-05) + ---------------- + + * Splitting off from zope.app.authentication ================================ Pluggable-Authentication Utility @@ -561,40 +619,6 @@ >> pau.getPrincipal('mypas_41') OddPrincipal('mypas_41', "{'int': 41}") - - ======= - Changes - ======= - - 1.0.3 (2010-07-09) - ------------------ - - * Fixed dependency declaration. - - - 1.0.2 (2010-07-90) - ------------------ - - * Added persistent.Persistent and zope.container.contained.Contained as - bases zope.pluggableauth.plugins.session.SessionCredentialsPlugin, so - instances of zope.app.authentication.session.SessionCredentialsPlugin - won't be changed. - (https://mail.zope.org/pipermail/zope-dev/2010-July/040898.html) - - - 1.0.1 (2010-02-11) - ------------------ - - * Adapters are now declared in a new ZCML file : - `principalfactories.zcml`. This avoids duplication errors in - ``zope.app.authentication``. - - - 1.0 (2010-02-05) - ---------------- - - * Splitting off from zope.app.authentication - Keywords: zope3 ztk authentication pluggable Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable diff -Nru zope.pluggableauth-1.0.3/debian/changelog zope.pluggableauth-1.3/debian/changelog --- zope.pluggableauth-1.0.3/debian/changelog 2011-12-31 08:20:32.000000000 +0000 +++ zope.pluggableauth-1.3/debian/changelog 2012-11-12 09:26:24.000000000 +0000 @@ -1,3 +1,16 @@ +zope.pluggableauth (1.3-0ubuntu1) raring; urgency=low + + * New upstream release. + * Lintian fixes: + - debian/control: + + Add Homepage field. + + Bump Standards-Version to 3.9.4. + - debian/copyright: + + Update obsolete field names. + + Update Format URI. + + -- Logan Rosen Sun, 11 Nov 2012 13:13:57 -0500 + zope.pluggableauth (1.0.3-0ubuntu5) precise; urgency=low * Rebuild to drop python2.6 dependencies. diff -Nru zope.pluggableauth-1.0.3/debian/control zope.pluggableauth-1.3/debian/control --- zope.pluggableauth-1.0.3/debian/control 2011-06-30 13:54:09.000000000 +0000 +++ zope.pluggableauth-1.3/debian/control 2012-11-12 09:26:19.000000000 +0000 @@ -1,11 +1,12 @@ Source: zope.pluggableauth Section: zope Priority: extra +Homepage: http://pypi.python.org/pypi/zope.app.authentication Maintainer: Ubuntu Developers XSBC-Original-Maintainer: Gediminas Paulauskas Build-Depends: debhelper (>= 7), python-all (>= 2.6.6-3~), python-setuptools (>= 0.6b3), python-van.pydeb (>= 1.3.0-4) -Standards-Version: 3.9.2 +Standards-Version: 3.9.4 X-Python-Version: >= 2.5 Package: python-zope.pluggableauth diff -Nru zope.pluggableauth-1.0.3/debian/copyright zope.pluggableauth-1.3/debian/copyright --- zope.pluggableauth-1.0.3/debian/copyright 2011-06-30 13:54:09.000000000 +0000 +++ zope.pluggableauth-1.3/debian/copyright 2012-11-12 09:22:58.000000000 +0000 @@ -1,6 +1,6 @@ -Format-Specification: http://dep.debian.net/deps/dep5/ -Name: zope.pluggableauth -Maintainer: Zope Foundation and Contributors +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: zope.pluggableauth +Upstream-Contact: Zope Foundation and Contributors Source: http://pypi.python.org/pypi/zope.pluggableauth Files: * diff -Nru zope.pluggableauth-1.0.3/setup.py zope.pluggableauth-1.3/setup.py --- zope.pluggableauth-1.0.3/setup.py 2010-07-09 21:12:19.000000000 +0000 +++ zope.pluggableauth-1.3/setup.py 2011-02-08 08:55:13.000000000 +0000 @@ -28,14 +28,16 @@ return open(os.path.join(os.path.dirname(__file__), *rnames)).read() setup(name='zope.pluggableauth', - version = '1.0.3', + version='1.3', author='Zope Foundation and Contributors', author_email='zope-dev@zope.org', description='Pluggable Authentication Utility', - long_description= "%s\n\n%s\n\n%s" % ( - read('README.txt'), + long_description= "\n".join(( + read('README.txt'), + '.. contents::\n\n', + read('CHANGES.txt'), read('src', 'zope', 'pluggableauth', 'README.txt'), - read('CHANGES.txt')), + )), url='http://pypi.python.org/pypi/zope.pluggableauth', license='ZPL 2.1', keywords='zope3 ztk authentication pluggable', @@ -54,12 +56,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', diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/__init__.py zope.pluggableauth-1.3/src/zope/pluggableauth/__init__.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/__init__.py 2010-07-01 20:30:58.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/__init__.py 2011-02-08 08:55:13.000000000 +0000 @@ -14,5 +14,4 @@ """Pluggable Authentication Utility """ -from zope.pluggableauth import interfaces from zope.pluggableauth.authentication import PluggableAuthentication diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/interfaces.py zope.pluggableauth-1.3/src/zope/pluggableauth/interfaces.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/interfaces.py 2010-07-01 20:30:58.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/interfaces.py 2011-02-08 08:55:13.000000000 +0000 @@ -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') diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/groupfolder.py zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/groupfolder.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/groupfolder.py 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/groupfolder.py 2011-02-08 08:55:13.000000000 +0000 @@ -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: groupfolder.py 117625 2010-10-18 09:12:52Z janwijbrand $ + +""" +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) + +@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 "" % 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) diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/groupfolder.txt zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/groupfolder.txt --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/groupfolder.txt 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/groupfolder.txt 2011-02-08 08:55:13.000000000 +0000 @@ -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) + [] + +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] + + +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] + + +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] + + +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] + + >>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1] + + >>> g1.principals = ('auth.p1', 'auth.p2') + >>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1] + + >>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1] + + +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 + + +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). diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/groupfolder.zcml zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/groupfolder.zcml --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/groupfolder.zcml 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/groupfolder.zcml 2011-02-08 08:55:13.000000000 +0000 @@ -0,0 +1,21 @@ + + + + + + diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/idpicker.py zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/idpicker.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/idpicker.py 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/idpicker.py 2011-02-08 08:55:13.000000000 +0000 @@ -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 diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/principalfolder.py zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/principalfolder.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/principalfolder.py 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/principalfolder.py 2011-02-08 08:55:13.000000000 +0000 @@ -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: principalfolder.py 117625 2010-10-18 09:12:52Z janwijbrand $ +""" +__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__ diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/principalfolder.txt zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/principalfolder.txt --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/principalfolder.txt 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/principalfolder.txt 2011-02-08 08:55:13.000000000 +0000 @@ -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'}) diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/principalfolder.zcml zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/principalfolder.zcml --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/principalfolder.zcml 1970-01-01 00:00:00.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/principalfolder.zcml 2011-02-08 08:55:13.000000000 +0000 @@ -0,0 +1,10 @@ + + + + + + diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/session.py zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/session.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/plugins/session.py 2010-07-09 20:35:40.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/plugins/session.py 2011-02-08 08:55:13.000000000 +0000 @@ -213,7 +213,7 @@ credentials = None if login and password: - credentials = SessionCredentials(login, password) + credentials = self._makeCredentials(login, password) elif not sessionData: return None sessionData = session[ @@ -227,6 +227,15 @@ return {'login': credentials.getLogin(), 'password': credentials.getPassword()} + def _makeCredentials(self, login, password): + """Create an ISessionCredentials. + + You can override this if you desire a different implementation, e.g. + one that encrypts the password, so it's not stored in plain text in + the ZODB. + """ + return SessionCredentials(login, password) + def challenge(self, request): """Challenges by redirecting to a login form. @@ -252,7 +261,7 @@ >>> request.response.getStatus() 302 >>> request.response.getHeader('location') - 'http://127.0.0.1/@@loginForm.html?camefrom=%2F' + 'http://127.0.0.1/@@loginForm.html?camefrom=http%3A%2F%2F127.0.0.1' The plugin redirects to the page defined by the loginpagename attribute: @@ -261,7 +270,7 @@ >>> plugin.challenge(request) True >>> request.response.getHeader('location') - 'http://127.0.0.1/@@mylogin.html?camefrom=%2F' + 'http://127.0.0.1/@@mylogin.html?camefrom=http%3A%2F%2F127.0.0.1' It also provides the request URL as a 'camefrom' GET style parameter. To illustrate, we'll pretend we've traversed a couple names: @@ -283,11 +292,26 @@ We see the 'camefrom' points to the requested URL: - >>> request.response.getHeader('location') # doctest: +ELLIPSIS - '.../@@mylogin.html?camefrom=%2Ffoo%2Fbar%2Ffolder%2Fpage+1.html%3Fq%3Dvalue' + >>> request.response.getHeader('location') + 'http://127.0.0.1/@@mylogin.html?camefrom=http%3A%2F%2F127.0.0.1%2Ffoo%2Fbar%2Ffolder%2Fpage+1.html%3Fq%3Dvalue' This can be used by the login form to redirect the user back to the originating URL upon successful authentication. + + Now that the 'camefrom' is an absolute URL, quickly demonstrate that + 'camefrom' information that inadvertently points to a different host, + will by default not be trusted in a redirect: + + >>> camefrom = request.response.getHeader('location') + >>> request.response.redirect(camefrom) + 'http://127.0.0.1/@@mylogin.html?camefrom=http%3A%2F%2F127.0.0.1%2Ffoo%2Fbar%2Ffolder%2Fpage+1.html%3Fq%3Dvalue' + >>> suspicious_camefrom = 'http://example.com/foobar' + >>> request.response.redirect(suspicious_camefrom) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Untrusted redirect to host 'example.com:80' not allowed. + + """ if not IHTTPRequest.providedBy(request): return False @@ -299,7 +323,7 @@ # Better to add the query string, if present query = request.get('QUERY_STRING') - camefrom = '/'.join([request.getURL(path_only=True)] + stack) + camefrom = '/'.join([request.getURL()] + stack) if query: camefrom = camefrom + '?' + query url = '%s/@@%s?%s' % (absoluteURL(site, request), diff -Nru zope.pluggableauth-1.0.3/src/zope/pluggableauth/tests.py zope.pluggableauth-1.3/src/zope/pluggableauth/tests.py --- zope.pluggableauth-1.0.3/src/zope/pluggableauth/tests.py 2010-07-01 20:30:58.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope/pluggableauth/tests.py 2011-02-08 08:55:13.000000000 +0000 @@ -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 Interface from zope.interface import implements +from zope.interface import Interface 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 diff -Nru zope.pluggableauth-1.0.3/src/zope.pluggableauth.egg-info/PKG-INFO zope.pluggableauth-1.3/src/zope.pluggableauth.egg-info/PKG-INFO --- zope.pluggableauth-1.0.3/src/zope.pluggableauth.egg-info/PKG-INFO 2010-07-09 21:13:42.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope.pluggableauth.egg-info/PKG-INFO 2011-02-08 08:55:16.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.0 Name: zope.pluggableauth -Version: 1.0.3 +Version: 1.3 Summary: Pluggable Authentication Utility Home-page: http://pypi.python.org/pypi/zope.pluggableauth Author: Zope Foundation and Contributors @@ -13,6 +13,64 @@ Based on zope.authentication, this package provides a flexible and pluggable authentication utility. Several common plugins are provided. + .. contents:: + + + ======= + Changes + ======= + + 1.3 (2011-02-08) + ---------------- + + - As the camefrom information is most probably used for a redirect, require + it to be an absolute URL (see also + http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.30). + + 1.2 (2010-12-16) + ---------------- + + - SessionCredentialsPlugin has a hook (_makeCredentials) that can be overriden + in subclasses to store the credentials in the session differently. + + For example, you could use keas.kmi and encrypt the passwords of the + currently logged-in users so they don't appear in plain text in the ZODB. + + 1.1 (2010-10-18) + ---------------- + + * Moved concrete IAuthenticatorPlugin implementations from + zope.app.authentication to zope.pluggableauth.plugins. + + As a result projects that want to use the IAuthenticator plugins (previously + found in zope.app.authentication) do not automatically also pull in the + zope.app.* dependencies that are needed to register the ZMI views. + + 1.0.3 (2010-07-09) + ------------------ + + * Fixed dependency declaration. + + 1.0.2 (2010-07-90) + ------------------ + + * Added persistent.Persistent and zope.container.contained.Contained as + bases zope.pluggableauth.plugins.session.SessionCredentialsPlugin, so + instances of zope.app.authentication.session.SessionCredentialsPlugin + won't be changed. + (https://mail.zope.org/pipermail/zope-dev/2010-July/040898.html) + + 1.0.1 (2010-02-11) + ------------------ + + * Adapters are now declared in a new ZCML file : + `principalfactories.zcml`. This avoids duplication errors in + ``zope.app.authentication``. + + 1.0 (2010-02-05) + ---------------- + + * Splitting off from zope.app.authentication ================================ Pluggable-Authentication Utility @@ -561,40 +619,6 @@ >> pau.getPrincipal('mypas_41') OddPrincipal('mypas_41', "{'int': 41}") - - ======= - Changes - ======= - - 1.0.3 (2010-07-09) - ------------------ - - * Fixed dependency declaration. - - - 1.0.2 (2010-07-90) - ------------------ - - * Added persistent.Persistent and zope.container.contained.Contained as - bases zope.pluggableauth.plugins.session.SessionCredentialsPlugin, so - instances of zope.app.authentication.session.SessionCredentialsPlugin - won't be changed. - (https://mail.zope.org/pipermail/zope-dev/2010-July/040898.html) - - - 1.0.1 (2010-02-11) - ------------------ - - * Adapters are now declared in a new ZCML file : - `principalfactories.zcml`. This avoids duplication errors in - ``zope.app.authentication``. - - - 1.0 (2010-02-05) - ---------------- - - * Splitting off from zope.app.authentication - Keywords: zope3 ztk authentication pluggable Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable diff -Nru zope.pluggableauth-1.0.3/src/zope.pluggableauth.egg-info/SOURCES.txt zope.pluggableauth-1.3/src/zope.pluggableauth.egg-info/SOURCES.txt --- zope.pluggableauth-1.0.3/src/zope.pluggableauth.egg-info/SOURCES.txt 2010-07-09 21:13:42.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope.pluggableauth.egg-info/SOURCES.txt 2011-02-08 08:55:16.000000000 +0000 @@ -27,7 +27,14 @@ src/zope/pluggableauth/plugins/ftpplugins.zcml src/zope/pluggableauth/plugins/generic.py src/zope/pluggableauth/plugins/generic.zcml +src/zope/pluggableauth/plugins/groupfolder.py +src/zope/pluggableauth/plugins/groupfolder.txt +src/zope/pluggableauth/plugins/groupfolder.zcml src/zope/pluggableauth/plugins/httpplugins.py src/zope/pluggableauth/plugins/httpplugins.zcml +src/zope/pluggableauth/plugins/idpicker.py +src/zope/pluggableauth/plugins/principalfolder.py +src/zope/pluggableauth/plugins/principalfolder.txt +src/zope/pluggableauth/plugins/principalfolder.zcml src/zope/pluggableauth/plugins/session.py src/zope/pluggableauth/plugins/session.zcml \ No newline at end of file diff -Nru zope.pluggableauth-1.0.3/src/zope.pluggableauth.egg-info/requires.txt zope.pluggableauth-1.3/src/zope.pluggableauth.egg-info/requires.txt --- zope.pluggableauth-1.0.3/src/zope.pluggableauth.egg-info/requires.txt 2010-07-09 21:13:42.000000000 +0000 +++ zope.pluggableauth-1.3/src/zope.pluggableauth.egg-info/requires.txt 2011-02-08 08:55:16.000000000 +0000 @@ -6,6 +6,7 @@ zope.event zope.i18nmessageid zope.interface +zope.password >= 3.5.1 zope.publisher>=3.12 zope.schema zope.security