[Zope3-checkins] SVN: Zope3/trunk/src/zope/ Commit changes to implement the following proposals:

Gary Poster gary at zope.com
Thu Jan 19 23:19:09 EST 2006


Log message for revision 41374:
  Commit changes to implement the following proposals:
  http://www.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/AllowContainedNonUtilityPluggableAuthenticationUtilityPlugins
  http://www.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/FireEventsWhenPrincipalsAreAddedToAndRemovedFromGroupFolders
  http://www.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/DefineInterfaceForGettingFullClosureOfAPrincipalsGroupsImplementInPAU
  http://www.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/DefineInterfaceForGettingAGroupsMembersImplementInPAU
  
  Includes a new zope.app generation to evolve legacy databases.
  

Changed:
  U   Zope3/trunk/src/zope/app/authentication/authentication.py
  U   Zope3/trunk/src/zope/app/authentication/browser/group_searching_with_empty_string.txt
  U   Zope3/trunk/src/zope/app/authentication/browser/groupfolder.txt
  U   Zope3/trunk/src/zope/app/authentication/browser/pau_prefix_and_searching.txt
  U   Zope3/trunk/src/zope/app/authentication/browser/principalfolder.txt
  U   Zope3/trunk/src/zope/app/authentication/browser/special-groups.txt
  U   Zope3/trunk/src/zope/app/authentication/configure.zcml
  U   Zope3/trunk/src/zope/app/authentication/groupfolder.py
  U   Zope3/trunk/src/zope/app/authentication/groupfolder.txt
  U   Zope3/trunk/src/zope/app/authentication/groupfolder.zcml
  U   Zope3/trunk/src/zope/app/authentication/interfaces.py
  U   Zope3/trunk/src/zope/app/authentication/principalfolder.py
  U   Zope3/trunk/src/zope/app/authentication/tests.py
  A   Zope3/trunk/src/zope/app/authentication/vocabulary.py
  A   Zope3/trunk/src/zope/app/authentication/vocabulary.txt
  U   Zope3/trunk/src/zope/app/zopeappgenerations/__init__.py
  A   Zope3/trunk/src/zope/app/zopeappgenerations/evolve3.py
  U   Zope3/trunk/src/zope/security/interfaces.py

-=-
Modified: Zope3/trunk/src/zope/app/authentication/authentication.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/authentication.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/authentication.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -41,16 +41,25 @@
         super(PluggableAuthentication, self).__init__()
         self.prefix = prefix
 
+    def _plugins(self, names, interface):
+        for name in names:
+            plugin = self.get(name)
+            if not interface.providedBy(plugin):
+                plugin = component.queryUtility(interface, name, context=self)
+            if plugin is not None:
+                yield name, plugin
+
+    def getAuthenticatorPlugins(self):
+        return self._plugins(
+            self.authenticatorPlugins, interfaces.IAuthenticatorPlugin)
+
+    def getCredentialsPlugins(self):
+        return self._plugins(
+            self.credentialsPlugins, interfaces.ICredentialsPlugin)
+
     def authenticate(self, request):
-        authenticatorPlugins = [
-            component.queryUtility(interfaces.IAuthenticatorPlugin,
-                                  name, context=self)
-            for name in self.authenticatorPlugins]
-        for name in self.credentialsPlugins:
-            credplugin = component.queryUtility(
-                interfaces.ICredentialsPlugin, name, context=self)
-            if credplugin is None:
-                continue
+        authenticatorPlugins = [p for n, p in self.getAuthenticatorPlugins()]
+        for name, credplugin in self.getCredentialsPlugins():
             credentials = credplugin.extractCredentials(request)
             for authplugin in authenticatorPlugins:
                 if authplugin is None:
@@ -73,11 +82,7 @@
                 raise PrincipalLookupError(id)
             return next.getPrincipal(id)
         id = id[len(self.prefix):]
-        for name in self.authenticatorPlugins:
-            authplugin = component.queryUtility(
-                interfaces.IAuthenticatorPlugin, name, context=self)
-            if authplugin is None:
-                continue
+        for name, authplugin in self.getAuthenticatorPlugins():
             info = authplugin.principalInfo(id)
             if info is None:
                 continue
@@ -92,11 +97,7 @@
         raise PrincipalLookupError(id)
 
     def getQueriables(self):
-        for name in self.authenticatorPlugins:
-            authplugin = component.queryUtility(
-                interfaces.IAuthenticatorPlugin, name, context=self)
-            if authplugin is None:
-                continue
+        for name, authplugin in self.getAuthenticatorPlugins():
             queriable = component.queryMultiAdapter((authplugin, self),
                 interfaces.IQueriableAuthenticator)
             if queriable is not None:
@@ -108,11 +109,7 @@
     def unauthorized(self, id, request):
         challengeProtocol = None
 
-        for name in self.credentialsPlugins:
-            credplugin = component.queryUtility(interfaces.ICredentialsPlugin,
-                                                name)
-            if credplugin is None:
-                continue
+        for name, credplugin in self.getCredentialsPlugins():
             protocol = getattr(credplugin, 'challengeProtocol', None)
             if challengeProtocol is None or protocol == challengeProtocol:
                 if credplugin.challenge(request):
@@ -129,11 +126,7 @@
     def logout(self, request):
         challengeProtocol = None
 
-        for name in self.credentialsPlugins:
-            credplugin = component.queryUtility(interfaces.ICredentialsPlugin,
-                                                name)
-            if credplugin is None:
-                continue
+        for name, credplugin in self.getCredentialsPlugins():
             protocol = getattr(credplugin, 'challengeProtocol', None)
             if challengeProtocol is None or protocol == challengeProtocol:
                 if credplugin.logout(request):

Modified: Zope3/trunk/src/zope/app/authentication/browser/group_searching_with_empty_string.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/browser/group_searching_with_empty_string.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/browser/group_searching_with_empty_string.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -77,45 +77,6 @@
   ...
 
 
-Register group folder pulgin.
-
-  >>> print http(r"""
-  ... POST /++etc++site/default/PAU/groups/addRegistration.html HTTP/1.1
-  ... Authorization: Basic bWdyOm1ncnB3
-  ... Content-Length: 807
-  ... Content-Type: multipart/form-data; boundary=---------------------------6689874747253728091673221069
-  ... Referer: http://localhost:8081/++etc++site/default/PAU/groups/addRegistration.html
-  ... 
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.name"
-  ... 
-  ... groups
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.status"
-  ... 
-  ... Active
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.status-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.permission"
-  ... 
-  ... 
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.permission-empty-marker"
-  ... 
-  ... 1
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
-  ... 
-  ... Add
-  ... -----------------------------6689874747253728091673221069--
-  ... """)
-  HTTP/1.1 303 See Other
-  ...
-
-
 And add some groups:
 
 
@@ -191,7 +152,7 @@
   ... -----------------------------1786480431902757372789659730
   ... Content-Disposition: form-data; name="field.credentialsPlugins.to"
   ... 
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------1786480431902757372789659730
   ... Content-Disposition: form-data; name="field.credentialsPlugins-empty-marker"
   ... 
@@ -199,7 +160,7 @@
   ... -----------------------------1786480431902757372789659730
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ... 
-  ... groups
+  ... Z3JvdXBz
   ... -----------------------------1786480431902757372789659730
   ... Content-Disposition: form-data; name="field.authenticatorPlugins-empty-marker"
   ... 
@@ -211,11 +172,11 @@
   ... -----------------------------1786480431902757372789659730
   ... Content-Disposition: form-data; name="field.credentialsPlugins"
   ... 
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------1786480431902757372789659730
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ... 
-  ... groups
+  ... Z3JvdXBz
   ... -----------------------------1786480431902757372789659730--
   ... """)
   HTTP/1.1 200 Ok

Modified: Zope3/trunk/src/zope/app/authentication/browser/groupfolder.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/browser/groupfolder.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/browser/groupfolder.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -463,45 +463,6 @@
   ...
 
 
-Register group folder pulgin.
-
-  >>> print http(r"""
-  ... POST /++etc++site/default/PAU/groups/addRegistration.html HTTP/1.1
-  ... Authorization: Basic bWdyOm1ncnB3
-  ... Content-Length: 807
-  ... Content-Type: multipart/form-data; boundary=---------------------------6689874747253728091673221069
-  ... Referer: http://localhost:8081/++etc++site/default/PAU/groups/addRegistration.html
-  ...
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.name"
-  ...
-  ... groups
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.status"
-  ...
-  ... Active
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.status-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.permission"
-  ...
-  ...
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="field.permission-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------6689874747253728091673221069
-  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
-  ...
-  ... Add
-  ... -----------------------------6689874747253728091673221069--
-  ... """)
-  HTTP/1.1 303 See Other
-  ...
-
-
 Next we'll select the credentials and authenticators for the PAU:
 
   >>> print http(r"""
@@ -514,7 +475,7 @@
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.credentialsPlugins.to"
   ...
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.credentialsPlugins-empty-marker"
   ...
@@ -522,11 +483,11 @@
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ...
-  ... users
+  ... dXNlcnM=
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ...
-  ... groups
+  ... Z3JvdXBz
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.authenticatorPlugins-empty-marker"
   ...
@@ -538,15 +499,15 @@
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.credentialsPlugins"
   ...
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ...
-  ... users
+  ... dXNlcnM=
   ... -----------------------------2026736768606413562109112352
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ...
-  ... groups
+  ... Z3JvdXBz
   ... -----------------------------2026736768606413562109112352--
   ... """)
   HTTP/1.1 200 Ok

Modified: Zope3/trunk/src/zope/app/authentication/browser/pau_prefix_and_searching.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/browser/pau_prefix_and_searching.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/browser/pau_prefix_and_searching.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -187,41 +187,6 @@
   HTTP/1.1 303 See Other
   ...
 
-  >>> print http(r"""
-  ... POST /++etc++site/default/PAU1/Groups/addRegistration.html HTTP/1.1
-  ... Authorization: Basic bWdyOm1ncnB3
-  ... Content-Length: 709
-  ... Content-Type: multipart/form-data; boundary=---------------------------27244279644818
-  ...
-  ... -----------------------------27244279644818
-  ... Content-Disposition: form-data; name="field.name"
-  ...
-  ... Groups
-  ... -----------------------------27244279644818
-  ... Content-Disposition: form-data; name="field.status"
-  ...
-  ... Active
-  ... -----------------------------27244279644818
-  ... Content-Disposition: form-data; name="field.status-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------27244279644818
-  ... Content-Disposition: form-data; name="field.permission"
-  ...
-  ...
-  ... -----------------------------27244279644818
-  ... Content-Disposition: form-data; name="field.permission-empty-marker"
-  ...
-  ... 1
-  ... -----------------------------27244279644818
-  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
-  ...
-  ... Add
-  ... -----------------------------27244279644818--
-  ... """)
-  HTTP/1.1 303 See Other
-  ...
-
 and add a group to search for:
 
   >>> print http(r"""
@@ -270,11 +235,11 @@
   ... -----------------------------610310492754
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ...
-  ... Groups
+  ... R3JvdXBz
   ... -----------------------------610310492754
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ...
-  ... Users
+  ... VXNlcnM=
   ... -----------------------------610310492754
   ... Content-Disposition: form-data; name="field.authenticatorPlugins-empty-marker"
   ...
@@ -286,11 +251,11 @@
   ... -----------------------------610310492754
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ...
-  ... Groups
+  ... R3JvdXBz
   ... -----------------------------610310492754
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ...
-  ... Users
+  ... VXNlcnM=
   ... -----------------------------610310492754--
   ... """)
   HTTP/1.1 200 Ok

Modified: Zope3/trunk/src/zope/app/authentication/browser/principalfolder.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/browser/principalfolder.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/browser/principalfolder.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -224,7 +224,7 @@
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.credentialsPlugins.to"
   ... 
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.credentialsPlugins-empty-marker"
   ... 
@@ -232,7 +232,7 @@
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ... 
-  ... users
+  ... dXNlcnM=
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.authenticatorPlugins-empty-marker"
   ... 
@@ -244,11 +244,11 @@
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.credentialsPlugins"
   ... 
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ... 
-  ... users
+  ... dXNlcnM=
   ... -----------------------------6519411471194050603270010787--
   ... """)
   HTTP/1.1 200 Ok

Modified: Zope3/trunk/src/zope/app/authentication/browser/special-groups.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/browser/special-groups.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/browser/special-groups.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -187,7 +187,7 @@
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.credentialsPlugins.to"
   ... 
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.credentialsPlugins-empty-marker"
   ... 
@@ -195,7 +195,7 @@
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.authenticatorPlugins.to"
   ... 
-  ... users
+  ... dXNlcnM=
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.authenticatorPlugins-empty-marker"
   ... 
@@ -207,11 +207,11 @@
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.credentialsPlugins"
   ... 
-  ... Session Credentials
+  ... U2Vzc2lvbiBDcmVkZW50aWFscw==
   ... -----------------------------6519411471194050603270010787
   ... Content-Disposition: form-data; name="field.authenticatorPlugins"
   ... 
-  ... users
+  ... dXNlcnM=
   ... -----------------------------6519411471194050603270010787--
   ... """)
   HTTP/1.1 200 Ok

Modified: Zope3/trunk/src/zope/app/authentication/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/authentication/configure.zcml	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/configure.zcml	2006-01-20 04:19:08 UTC (rev 41374)
@@ -22,22 +22,14 @@
     provides=".interfaces.IQueriableAuthenticator"
     factory=".authentication.QuerySchemaSearchAdapter" />
 
-  <!-- This explicit declaration is needed indirectly by vocabulary to make
-    the interface available as an IInterface utility. This is bogus...the
-    vocabulary directive should make sure this registration happens. -->
-  <interface interface=".interfaces.IAuthenticatorPlugin" />
   <vocabulary
     name="CredentialsPlugins"
-    factory="zope.app.component.vocabulary.UtilityVocabulary"
-    interface="zope.app.authentication.interfaces.ICredentialsPlugin"
-    nameOnly="True"
+    factory=".vocabulary.credentialsPlugins"
    />
 
   <vocabulary
     name="AuthenticatorPlugins"
-    factory="zope.app.component.vocabulary.UtilityVocabulary"
-    interface="zope.app.authentication.interfaces.IAuthenticatorPlugin"
-    nameOnly="True"
+    factory=".vocabulary.authenticatorPlugins"
    />
 
   <utility

Modified: Zope3/trunk/src/zope/app/authentication/groupfolder.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/groupfolder.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/groupfolder.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -16,18 +16,13 @@
 $Id$
 
 """
-
-try:
-    set
-except NameError:
-    from sets import Set as set
-
 import BTrees.OOBTree
 import persistent
 
 from zope import interface, event, schema, component
 from zope.interface import alsoProvides
-from zope.security.interfaces import IGroup, IGroupAwarePrincipal
+from zope.security.interfaces import (
+    IGroup, IGroupAwarePrincipal, IMemberAwareGroup)
 
 from zope.app import zapi
 from zope.app.container.btree import BTreeContainer
@@ -81,7 +76,6 @@
 
     zope.app.container.constraints.containers(IGroupFolder)
 
-
 class IGroupSearchCriteria(interface.Interface):
 
     search = schema.TextLine(
@@ -90,13 +84,25 @@
         missing_value=u'',
         )
 
+class IGroupPrincipalInfo(interfaces.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:
 
-      >>> info = GroupInfo('groups.managers', 'Managers', 'Taskmasters')
+      >>> 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
@@ -105,15 +111,35 @@
       'Managers'
       >>> info.description
       'Taskmasters'
+      >>> info.members
+      ('joe', 'jane')
+      >>> info.members = ('joe', 'jane', 'jaime')
+      >>> info.members
+      ('joe', 'jane', 'jaime')
 
     """
-    interface.implements(interfaces.IPrincipalInfo)
+    interface.implements(IGroupPrincipalInfo)
 
-    def __init__(self, id, title, description):
+    def __init__(self, id, information):
         self.id = id
-        self.title = title
-        self.description = description
+        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
 
@@ -136,35 +162,43 @@
     def __setitem__(self, name, value):
         BTreeContainer.__setitem__(self, name, value)
         group_id = self._groupid(value)
-        for principal_id in value.principals:
-            self._addPrincipalToGroup(principal_id, group_id)
+        self._addPrincipalsToGroup(value.principals, group_id)
+        if value.principals:
+            event.notify(
+                interfaces.PrincipalsAddedToGroup(
+                    value.principals, self.__parent__.prefix + group_id))
         group = principalfolder.Principal(self.prefix + name)
         event.notify(interfaces.GroupAdded(group))
 
     def __delitem__(self, name):
         value = self[name]
         group_id = self._groupid(value)
-        for principal_id in value.principals:
-            self._removePrincipalFromGroup(principal_id, group_id)
+        self._removePrincipalsFromGroup(value.principals, group_id)
+        if value.principals:
+            event.notify(
+                interfaces.PrincipalsRemovedFromGroup(
+                    value.principals, self.__parent__.prefix + group_id))
         BTreeContainer.__delitem__(self, name)
 
     def _groupid(self, group):
         return self.prefix+group.__name__
 
-    def _addPrincipalToGroup(self, principal_id, group_id):
-        self.__inverseMapping[principal_id] = (
-            self.__inverseMapping.get(principal_id, ())
-            + (group_id,))
+    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 _removePrincipalFromGroup(self, principal_id, group_id):
-        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 _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"""
@@ -200,7 +234,7 @@
             info = self.get(id)
             if info is not None:
                 return GroupInfo(
-                    self.prefix+id, info.title, info.description)
+                    self.prefix+id, info)
 
 class GroupCycle(Exception):
     """There is a cyclic relationship among groups
@@ -236,6 +270,7 @@
         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)
@@ -244,19 +279,18 @@
             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
 
-            for principal_id in oldset - new:
-                try:
-                    parent._removePrincipalFromGroup(principal_id, group_id)
-                except AttributeError:
-                    pass
+            try:
+                parent._addPrincipalsToGroup(added, group_id)
+            except AttributeError:
+                added = None
 
-            for principal_id in new - oldset:
-                try:
-                    parent._addPrincipalToGroup(principal_id, group_id)
-                except AttributeError:
-                    pass
-
             if check:
                 try:
                     nocycles(new, [], zapi.principals().getPrincipal)
@@ -264,8 +298,16 @@
                     # abort
                     self.setPrincipals(old, False)
                     raise
+            # now that we've gotten past the checks, fire the events.
+            if removed:
+                event.notify(
+                    interfaces.PrincipalsRemovedFromGroup(
+                        removed, self.__parent__.__parent__.prefix + group_id))
+            if added:
+                event.notify(
+                    interfaces.PrincipalsAddedToGroup(
+                        added, self.__parent__.__parent__.prefix + group_id))
 
-
     principals = property(lambda self: self._principals, setPrincipals)
 
 
@@ -293,8 +335,7 @@
 
     authentication = event.authentication
 
-    plugins = zapi.getUtilitiesFor(interfaces.IAuthenticatorPlugin)
-    for name, plugin in plugins:
+    for name, plugin in authentication.getAuthenticatorPlugins():
         if not IGroupFolder.providedBy(plugin):
             continue
         groupfolder = plugin
@@ -306,3 +347,16 @@
         prefix = authentication.prefix + groupfolder.prefix
         if id.startswith(prefix) and id[len(prefix):] in groupfolder:
             alsoProvides(principal, IGroup)
+
+ at component.adapter(interfaces.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)

Modified: Zope3/trunk/src/zope/app/authentication/groupfolder.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/groupfolder.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/groupfolder.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -2,7 +2,8 @@
 Group Folders
 =============
 
-Group folders provide support for groups information stored in the ZODB.
+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.
 
@@ -57,9 +58,15 @@
   ...         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)
@@ -96,8 +103,13 @@
   >>> g1.principals
   ('auth.p1', 'auth.p2')
 
-This allows us to look up groups for the principals:
+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',)
 
@@ -119,6 +131,12 @@
   >>> 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:
 
@@ -129,6 +147,29 @@
 
 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.app.authentication.groupfolder.GroupInformation("Group Two")
@@ -138,6 +179,10 @@
   >>> 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')
@@ -147,6 +192,11 @@
   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.app.authentication.groupfolder.GroupInformation("Group A")
@@ -296,6 +346,58 @@
   >>> 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.app.authentication.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.app.authentication.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.app.authentication.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
 ==========
 
@@ -313,7 +415,7 @@
 
 o It is impossible to assign users from lower authentication
   utilities because they can't be seen when managing the group,
-  from the site cntaining 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)

Modified: Zope3/trunk/src/zope/app/authentication/groupfolder.zcml
===================================================================
--- Zope3/trunk/src/zope/app/authentication/groupfolder.zcml	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/groupfolder.zcml	2006-01-20 04:19:08 UTC (rev 41374)
@@ -17,14 +17,14 @@
           />
   </content>
 
-  <localUtility class=".groupfolder.GroupFolder">
+  <content class=".groupfolder.GroupFolder">
     <implements
         interface=".groupfolder.IGroupFolder" />
     <require
         permission="zope.ManageServices"
         interface="zope.app.container.interfaces.IContainer
                  zope.app.container.interfaces.INameChooser" />
-  </localUtility>
+  </content>
 
   <adapter
       provides="zope.app.container.interfaces.INameChooser"
@@ -42,6 +42,8 @@
       handler=".groupfolder.setGroupsForPrincipal"
       />
 
+  <subscriber handler=".groupfolder.setMemberSubscriber" />
+
   <include package=".browser" file="groupfolder.zcml" />
 
   <!-- Registering documentation with API doc -->

Modified: Zope3/trunk/src/zope/app/authentication/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/interfaces.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/interfaces.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -19,6 +19,7 @@
 
 import zope.interface
 import zope.schema
+import zope.security.interfaces
 from zope.app.i18n import ZopeMessageFactory as _
 from zope.app.security.interfaces import ILogout
 from zope.app.container.constraints import contains, containers
@@ -44,16 +45,38 @@
 
     credentialsPlugins = zope.schema.List(
         title=_('Credentials Plugins'),
+        description=_("""Used for extracting credentials.
+        Names may be of ids of non-utility ICredentialsPlugins contained in
+        the IPluggableAuthentication, or names of registered
+        ICredentialsPlugins utilities.  Contained non-utility ids mask 
+        utility names."""),
         value_type=zope.schema.Choice(vocabulary='CredentialsPlugins'),
         default=[],
         )
 
     authenticatorPlugins = zope.schema.List(
         title=_('Authenticator Plugins'),
+        description=_("""Used for converting credentials to principals.
+        Names may be of ids of non-utility IAuthenticatorPlugins contained in
+        the IPluggableAuthentication, or names of registered
+        IAuthenticatorPlugins utilities.  Contained non-utility ids mask 
+        utility names."""),
         value_type=zope.schema.Choice(vocabulary='AuthenticatorPlugins'),
         default=[],
         )
 
+    def getCredentialsPlugins():
+        """Return iterable of (plugin name, actual credentials plugin) pairs.
+        Looks up names in credentialsPlugins as contained ids of non-utility
+        ICredentialsPlugins first, then as registered ICredentialsPlugin
+        utilities.  Names that do not resolve are ignored."""
+
+    def getAuthenticatorPlugins():
+        """Return iterable of (plugin name, actual authenticator plugin) pairs.
+        Looks up names in authenticatorPlugins as contained ids of non-utility
+        IAuthenticatorPlugins first, then as registered IAuthenticatorPlugin
+        utilities.  Names that do not resolve are ignored."""
+
     prefix = zope.schema.TextLine(
         title=_('Prefix'),
         default=u'',
@@ -62,7 +85,7 @@
         )
 
     def logout(request):
-        """Performs a logout by delegating to its authentictor plugins."""
+        """Performs a logout by delegating to its authenticator plugins."""
 
 
 class ICredentialsPlugin(IPlugin):
@@ -154,7 +177,22 @@
         IPluggableAuthentication.getPrincipal.
         """)
 
+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 IPrincipalFactory(zope.interface.Interface):
     """A principal factory."""
 
@@ -281,3 +319,31 @@
 
     def __repr__(self):
         return "<GroupAdded %r>" % self.group.id
+
+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')
+
+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):
+    zope.interface.implements(IPrincipalsAddedToGroup)
+
+class PrincipalsRemovedFromGroup(AbstractMembersChanged):
+    zope.interface.implements(IPrincipalsRemovedFromGroup)

Modified: Zope3/trunk/src/zope/app/authentication/principalfolder.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/principalfolder.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/principalfolder.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -23,7 +23,6 @@
 from zope.event import notify
 from zope.schema import Text, TextLine, Password, Choice
 from zope.publisher.interfaces import IRequest
-from zope.security.interfaces import IGroupAwarePrincipal
 
 from zope.app import zapi
 from zope.app.container.interfaces import DuplicateIDError
@@ -31,6 +30,7 @@
 from zope.app.container.constraints import contains, containers
 from zope.app.container.btree import BTreeContainer
 from zope.app.i18n import ZopeMessageFactory as _
+from zope.app.security.interfaces import IAuthentication
 
 from zope.app.authentication import interfaces
 
@@ -305,7 +305,6 @@
                     n += 1
                     yield self.prefix + value.__name__
 
-
 class Principal(object):
     """A group-aware implementation of zope.security.interfaces.IPrincipal.
 
@@ -329,8 +328,77 @@
       >>> p.description
       'A site member.'
 
+    The `groups` is a simple list, filled in by plugins.
+
+      >>> p.groups
+      []
+
+    The `allGroups` attribute is a readonly iterable of the full closure of the
+    groups in the `groups` attribute--that is, if the principal is a direct
+    member of the 'Administrators' group, and the 'Administrators' group is
+    a member of the 'Reviewers' group, then p.groups would be 
+    ['Administrators'] and list(p.allGroups) would be
+    ['Administrators', 'Reviewers'].
+
+    To illustrate this, we'll need to set up a dummy authentication utility,
+    and a few principals.  Our main principal will also gain some groups, as if
+    plugins had added the groups to the list.  This is all setup--skip to the
+    next block to actually see `allGroups` in action.
+    
+      >>> p.groups.extend(
+      ...     ['content_administrators', 'zope_3_project',
+      ...      'list_administrators', 'zpug'])
+      >>> editor = Principal('editors', 'Content Editors')
+      >>> creator = Principal('creators', 'Content Creators')
+      >>> reviewer = Principal('reviewers', 'Content Reviewers')
+      >>> reviewer.groups.extend(['editors', 'creators'])
+      >>> usermanager = Principal('user_managers', 'User Managers')
+      >>> contentAdmin = Principal(
+      ...     'content_administrators', 'Content Administrators')
+      >>> contentAdmin.groups.extend(['reviewers', 'user_managers'])
+      >>> zope3Dev = Principal('zope_3_project', 'Zope 3 Developer')
+      >>> zope3ListAdmin = Principal(
+      ...     'zope_3_list_admin', 'Zope 3 List Administrators')
+      >>> zope3ListAdmin.groups.append('zope_3_project') # duplicate, but
+      ... # should only appear in allGroups once
+      >>> listAdmin = Principal('list_administrators', 'List Administrators')
+      >>> listAdmin.groups.append('zope_3_list_admin')
+      >>> zpugMember = Principal('zpug', 'ZPUG Member')
+      >>> martians = Principal('martians', 'Martians') # not in p's allGroups
+      >>> group_data = dict((p.id, p) for p in (
+      ...     editor, creator, reviewer, usermanager, contentAdmin,
+      ...     zope3Dev, zope3ListAdmin, listAdmin, zpugMember, martians))
+      >>> class DemoAuth(object):
+      ...     interface.implements(IAuthentication)
+      ...     def getPrincipal(self, id):
+      ...         return group_data[id]
+      ...
+      >>> demoAuth = DemoAuth()
+      >>> component.provideUtility(demoAuth)
+
+    Now, we have a user with the following groups (lowest level are p's direct
+    groups, and lines show membership):
+
+      editors  creators
+         \------/
+             |                                     zope_3_project (duplicate)
+          reviewers  user_managers                          |
+               \---------/                           zope_3_list_admin
+                    |                                       |
+          content_administrators   zope_3_project   list_administrators   zpug
+
+    The allGroups value includes all of the shown groups, and with
+    'zope_3_project' only appearing once.
+
+      >>> p.groups # doctest: +NORMALIZE_WHITESPACE
+      ['content_administrators', 'zope_3_project', 'list_administrators',
+       'zpug']
+      >>> list(p.allGroups) # doctest: +NORMALIZE_WHITESPACE
+      ['content_administrators', 'reviewers', 'editors', 'creators',
+       'user_managers', 'zope_3_project', 'list_administrators',
+       'zope_3_list_admin', 'zpug']
     """
-    interface.implements(IGroupAwarePrincipal)
+    interface.implements(interfaces.IPrincipal)
 
     def __init__(self, id, title=u'', description=u''):
         self.id = id
@@ -341,6 +409,23 @@
     def __repr__(self):
         return 'Principal(%r)' % self.id
 
+    @property
+    def allGroups(self):
+        if self.groups:
+            seen = set()
+            principals = component.getUtility(IAuthentication)
+            stack = [iter(self.groups)]
+            while stack:
+                try:
+                    group_id = stack[-1].next()
+                except StopIteration:
+                    stack.pop()
+                else:
+                    if group_id not in seen:
+                        yield group_id
+                        seen.add(group_id)
+                        group = principals.getPrincipal(group_id)
+                        stack.append(iter(group.groups))
 
 class AuthenticatedPrincipalFactory(object):
     """Creates 'authenticated' principals.

Modified: Zope3/trunk/src/zope/app/authentication/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/tests.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/tests.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -102,6 +102,7 @@
         doctest.DocTestSuite('zope.app.authentication.generic'),
         doctest.DocTestSuite('zope.app.authentication.httpplugins'),
         doctest.DocTestSuite('zope.app.authentication.ftpplugins'),
+        doctest.DocTestSuite('zope.app.authentication.groupfolder'),
         doctest.DocFileSuite('principalfolder.txt',
                              setUp=placelesssetup.setUp,
                              tearDown=placelesssetup.tearDown),
@@ -125,6 +126,10 @@
                              setUp=placelesssetup.setUp,
                              tearDown=placelesssetup.tearDown,
                              ),
+        doctest.DocFileSuite('vocabulary.txt',
+                             setUp=placelesssetup.setUp,
+                             tearDown=placelesssetup.tearDown,
+                             ),
         unittest.makeSuite(NonHTTPSessionTestCase),
         ))
 

Added: Zope3/trunk/src/zope/app/authentication/vocabulary.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/vocabulary.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/vocabulary.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -0,0 +1,94 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation 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.
+#
+##############################################################################
+"""Plugin Vocabulary.
+
+This vocabulary provides terms for authentication utility plugins.
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from zope import interface, component, i18n
+from zope.interface.interfaces import IInterface
+from zope.schema import vocabulary
+
+import zope.app.dublincore.interfaces
+from zope.app.component.interfaces import ILocalUtility
+from zope.app.i18n import ZopeMessageFactory as _
+
+from zope.app.authentication import interfaces
+
+UTILITY_TITLE = _(
+    'zope.app.authentication.vocabulary-utility-plugin-title',
+    '${name} (a utility)')
+CONTAINED_TITLE = _(
+    'zope.app.authentication.vocabulary-contained-plugin-title',
+    '${name} (in contents)')
+MISSING_TITLE = _(
+    'zope.app.authentication.vocabulary-missing-plugin-title',
+    '${name} (not found; deselecting will remove)')
+
+def _pluginVocabulary(context, interface, attr_name):
+    """Vocabulary that provides names of plugins of a specified interface.
+
+    Given an interface, the options should include the unique names of all of
+    the plugins that provide the specified interface for the current context--
+    which is expected to be a pluggable authentication utility, hereafter
+    referred to as a PAU).
+
+    These plugins may be objects contained within the PAU that do not provide
+    zope.app.component.interfaces.ILocalUtility ("contained plugins"), or may
+    be utilities registered for the specified interface, found in the context
+    of the PAU ("utility plugins").  Contained plugins mask utility plugins of
+    the same name.
+
+    The vocabulary also includes the current values of the PAU even if they do
+    not correspond to a contained or utility plugin.
+    """
+    terms = {}
+    isPAU = interfaces.IPluggableAuthentication.providedBy(context)
+    if isPAU:
+        for k, v in context.items():
+            if interface.providedBy(v) and not ILocalUtility.providedBy(v):
+                dc = zope.app.dublincore.interfaces.IDCDescriptiveProperties(
+                    v, None)
+                if dc is not None and dc.title:
+                    title = dc.title
+                else:
+                    title = k
+                terms[k] = vocabulary.SimpleTerm(
+                    k, k.encode('base64').strip(), i18n.Message(
+                        CONTAINED_TITLE, mapping={'name': title}))
+    utils = component.getUtilitiesFor(interface, context)
+    for nm, util in utils:
+        if nm not in terms:
+            terms[nm] = vocabulary.SimpleTerm(
+                nm, nm.encode('base64').strip(), i18n.Message(
+                    UTILITY_TITLE, mapping={'name': nm}))
+    if isPAU:
+        for nm in set(getattr(context, attr_name)):
+            if nm not in terms:
+                terms[nm] = vocabulary.SimpleTerm(
+                    nm, nm.encode('base64').strip(), i18n.Message(
+                        MISSING_TITLE, mapping={'name': nm}))
+    return vocabulary.SimpleVocabulary(
+        [term for nm, term in sorted(terms.items())])
+
+def authenticatorPlugins(context):
+    return _pluginVocabulary(
+        context, interfaces.IAuthenticatorPlugin, 'authenticatorPlugins')
+
+def credentialsPlugins(context):
+    return _pluginVocabulary(
+        context, interfaces.ICredentialsPlugin, 'credentialsPlugins')

Added: Zope3/trunk/src/zope/app/authentication/vocabulary.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/vocabulary.txt	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/authentication/vocabulary.txt	2006-01-20 04:19:08 UTC (rev 41374)
@@ -0,0 +1,206 @@
+The vocabulary module provides vocabularies for the authenticatorPlugins and
+the credentialsPlugins.
+
+The options should include the unique names of all of the plugins that provide
+the appropriate interface (interfaces.ICredentialsPlugin or
+interfaces.IAuthentiatorPlugin, respectively) for the current context-- which
+is expected to be a pluggable authentication utility, hereafter referred to as
+a PAU.
+
+These names may be for objects contained within the PAU that do not provide
+zope.app.component.interfaces.ILocalUtility ("contained plugins"), or may
+be utilities registered for the specified interface, found in the context
+of the PAU ("utility plugins").  Contained plugins mask utility plugins of
+the same name.  They also may be names currently selected in the PAU that do
+not actually have a corresponding plugin at this time.
+
+Here is a short example of how the vocabulary should work.  Let's say we're
+working with authentication plugins.  We'll create some faux
+authentication plugins, and register some of them as utilities and put
+others in a faux PAU.  Two of the contained plugins will provide
+zope.app.component.interfaces.ILocalUtility, and so should not be included in
+the vocabulary.
+
+    >>> from zope.app.authentication import interfaces
+    >>> from zope import interface, component
+    >>> from zope.app.component.interfaces import ILocalUtility
+    >>> class DemoPlugin(object):
+    ...     interface.implements(interfaces.IAuthenticatorPlugin)
+    ...     def __init__(self, name):
+    ...         self.name = name
+    ...
+    >>> utility_plugins = dict(
+    ...     (i, DemoPlugin(u'Plugin %d' % i)) for i in range(4))
+    >>> contained_plugins = dict(
+    ...     (i, DemoPlugin(u'Plugin %d' % i)) for i in range(1, 5))
+    >>> interface.directlyProvides(contained_plugins[2], ILocalUtility)
+    >>> interface.directlyProvides(contained_plugins[3], ILocalUtility)
+    >>> sorted(utility_plugins.keys())
+    [0, 1, 2, 3]
+    >>> for p in utility_plugins.values():
+    ...     component.provideUtility(p, name=p.name)
+    ...
+    >>> sorted(contained_plugins.keys()) # 1 will mask utility plugin 1
+    [1, 2, 3, 4]
+    >>> class DemoAuth(dict):
+    ...     interface.implements(interfaces.IPluggableAuthentication)
+    ...     def __init__(self, *args, **kwargs):
+    ...         super(DemoAuth, self).__init__(*args, **kwargs)
+    ...         self.authenticatorPlugins = (u'Plugin 3', u'Plugin X')
+    ...         self.credentialsPlugins = (u'Plugin 4', u'Plugin X')
+    ...
+    >>> auth = DemoAuth((p.name, p) for p in contained_plugins.values())
+    
+    >>> @component.adapter(interface.Interface)
+    ... @interface.implementer(component.ISiteManager)
+    ... def getSiteManager(context):
+    ...     return component.getGlobalSiteManager()
+    ...
+    >>> component.provideAdapter(getSiteManager)
+
+We are now ready to create a vocabulary that we can use.  The context is
+our faux authentication utility, `auth`.
+
+    >>> from zope.app.authentication import vocabulary
+    >>> vocab = vocabulary.authenticatorPlugins(auth)
+
+Iterating over the vocabulary results in all of the terms, in a relatively
+arbitrary order of their names.  (This vocabulary should typically use a
+widget that sorts values on the basis of localized collation order of the
+term titles.)
+
+    >>> [term.value for term in vocab] # doctest: +NORMALIZE_WHITESPACE
+    [u'Plugin 0', u'Plugin 1', u'Plugin 2', u'Plugin 3', u'Plugin 4',
+     u'Plugin X']
+
+Similarly, we can use `in` to test for the presence of values in the
+vocabulary.
+
+    >>> ['Plugin %s' % i in vocab for i in range(-1, 6)]
+    [False, True, True, True, True, True, False]
+    >>> 'Plugin X' in vocab
+    True
+
+The length reports the expected value.
+
+    >>> len(vocab)
+    6
+
+One can get a term for a given value using `getTerm()`; its token, in
+turn, should also return the same effective term from `getTermByToken`.
+
+    >>> values = ['Plugin 0', 'Plugin 1', 'Plugin 2', 'Plugin 3', 'Plugin 4',
+    ...           'Plugin X']
+    >>> for val in values:
+    ...     term = vocab.getTerm(val)
+    ...     assert term.value == val
+    ...     term2 = vocab.getTermByToken(term.token)
+    ...     assert term2.token == term.token
+    ...     assert term2.value == val
+    ...
+
+The terms have titles, which are message ids that show the plugin title or id
+and whether the plugin is a utility or just contained in the auth utility.
+We'll give one of the plugins a dublin core title just to show the
+functionality.
+
+    >>> import zope.app.dublincore.interfaces
+    >>> class ISpecial(interface.Interface):
+    ...     pass
+    ...
+    >>> interface.directlyProvides(contained_plugins[1], ISpecial)
+    >>> class DemoDCAdapter(object):
+    ...     interface.implements(
+    ...         zope.app.dublincore.interfaces.IDCDescriptiveProperties)
+    ...     component.adapts(ISpecial)
+    ...     def __init__(self, context):
+    ...         pass
+    ...     title = u'Special Title'
+    ...
+    >>> component.provideAdapter(DemoDCAdapter)
+
+We need to regenerate the vocabulary, since it calculates all of its data at
+once.
+
+    >>> vocab = vocabulary.authenticatorPlugins(auth)
+
+Now we'll check the titles.  We'll have to translate them to see what we
+expect.
+
+    >>> from zope import i18n
+    >>> import pprint
+    >>> pprint.pprint([i18n.translate(term.title) for term in vocab])
+    [u'Plugin 0 (a utility)',
+     u'Special Title (in contents)',
+     u'Plugin 2 (a utility)',
+     u'Plugin 3 (a utility)',
+     u'Plugin 4 (in contents)',
+     u'Plugin X (not found; deselecting will remove)']
+
+credentialsPlugins
+-----
+
+For completeness, we'll do the same review of the credentialsPlugins.
+
+    >>> class DemoPlugin(object):
+    ...     interface.implements(interfaces.ICredentialsPlugin)
+    ...     def __init__(self, name):
+    ...         self.name = name
+    ...
+    >>> utility_plugins = dict(
+    ...     (i, DemoPlugin(u'Plugin %d' % i)) for i in range(4))
+    >>> contained_plugins = dict(
+    ...     (i, DemoPlugin(u'Plugin %d' % i)) for i in range(1, 5))
+    >>> interface.directlyProvides(contained_plugins[2], ILocalUtility)
+    >>> interface.directlyProvides(contained_plugins[3], ILocalUtility)
+    >>> for p in utility_plugins.values():
+    ...     component.provideUtility(p, name=p.name)
+    ...
+    >>> auth = DemoAuth((p.name, p) for p in contained_plugins.values())
+    >>> vocab = vocabulary.credentialsPlugins(auth)
+
+Iterating over the vocabulary results in all of the terms, in a relatively
+arbitrary order of their names.  (This vocabulary should typically use a
+widget that sorts values on the basis of localized collation order of the term
+titles.) Similarly, we can use `in` to test for the presence of values in the
+vocabulary. The length reports the expected value.
+
+    >>> [term.value for term in vocab] # doctest: +NORMALIZE_WHITESPACE
+    [u'Plugin 0', u'Plugin 1', u'Plugin 2', u'Plugin 3', u'Plugin 4',
+     u'Plugin X']
+    >>> ['Plugin %s' % i in vocab for i in range(-1, 6)]
+    [False, True, True, True, True, True, False]
+    >>> 'Plugin X' in vocab
+    True
+    >>> len(vocab)
+    6
+
+One can get a term for a given value using `getTerm()`; its token, in
+turn, should also return the same effective term from `getTermByToken`.
+
+    >>> values = ['Plugin 0', 'Plugin 1', 'Plugin 2', 'Plugin 3', 'Plugin 4',
+    ...           'Plugin X']
+    >>> for val in values:
+    ...     term = vocab.getTerm(val)
+    ...     assert term.value == val
+    ...     term2 = vocab.getTermByToken(term.token)
+    ...     assert term2.token == term.token
+    ...     assert term2.value == val
+    ...
+
+The terms have titles, which are message ids that show the plugin title or id
+and whether the plugin is a utility or just contained in the auth utility.
+We'll give one of the plugins a dublin core title just to show the
+functionality. We need to regenerate the vocabulary, since it calculates all
+of its data at once. Then we'll check the titles.  We'll have to translate
+them to see what we expect.
+
+    >>> interface.directlyProvides(contained_plugins[1], ISpecial)
+    >>> vocab = vocabulary.credentialsPlugins(auth)
+    >>> pprint.pprint([i18n.translate(term.title) for term in vocab])
+    [u'Plugin 0 (a utility)',
+     u'Special Title (in contents)',
+     u'Plugin 2 (a utility)',
+     u'Plugin 3 (a utility)',
+     u'Plugin 4 (in contents)',
+     u'Plugin X (not found; deselecting will remove)']


Property changes on: Zope3/trunk/src/zope/app/authentication/vocabulary.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/zopeappgenerations/__init__.py
===================================================================
--- Zope3/trunk/src/zope/app/zopeappgenerations/__init__.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/zopeappgenerations/__init__.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -24,7 +24,7 @@
 
 ZopeAppSchemaManager = SchemaManager(
     minimum_generation=0,
-    generation=2,
+    generation=3,
     package_name=key)
 
 

Added: Zope3/trunk/src/zope/app/zopeappgenerations/evolve3.py
===================================================================
--- Zope3/trunk/src/zope/app/zopeappgenerations/evolve3.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/app/zopeappgenerations/evolve3.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -0,0 +1,95 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation 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.
+#
+##############################################################################
+"""Evolve existing PAU group folders.
+
+They should be used as contained plugins rather than registered plugins.
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from zope import component
+
+from zope.app.component.interfaces import ISite
+from zope.app.zopeappgenerations import getRootFolder
+
+from zope.app.generations.utility import findObjectsProviding
+
+from zope.app.component import registration
+import zope.app.authentication.interfaces
+from zope.app.authentication import groupfolder
+from zope.app.copypastemove.interfaces import IObjectMover
+
+generation = 3
+
+def evolve(context):
+    """Evolve existing PAUs and group folders.
+
+    - Group folders should no longer be registered.
+
+    - PAUs that use group folders should use their contents name, not their
+    (formerly) registered name.
+
+    Group folders used by multiple PAUs were not supported, and are not
+    supported with this evolution.
+    """
+    root = getRootFolder(context)
+
+    for site in findObjectsProviding(root, ISite):
+        sm = site.getSiteManager()
+        for pau in findObjectsProviding(
+            sm, zope.app.authentication.interfaces.IPluggableAuthentication):
+            for nm, util in component.getUtilitiesFor(
+                zope.app.authentication.interfaces.IAuthenticatorPlugin,
+                context=pau):
+                if groupfolder.IGroupFolder.providedBy(util):
+                    if util.__parent__ is not pau:
+                        raise RuntimeError(
+                            "I don't know how to migrate your database: "
+                            "each group folder should only be within the "
+                            "Pluggable Authentication utility that uses it")
+                    # we need to remove this registration
+                    regs = registration.Registered(util).registrations()
+                    if len(regs) != 1:
+                        raise RuntimeError(
+                            "I don't know how to migrate your database: "
+                            "you should only have registered your group "
+                            "folder as an IAuthenticatorPlugin, but it looks "
+                            "like it's registered for something additional "
+                            "that I don't expect")
+                    r = regs[0]
+                    r.getRegistry().unregister(r)
+                    if r.name in pau.authenticatorPlugins:
+                        if util.__name__ != r.name: # else no-op
+                            plugins = list(pau.authenticatorPlugins)
+                            if util.__name__ in pau.authenticatorPlugins:
+                                # argh! another active plugin's name is
+                                # the same as this group folder's
+                                # __name__.  That means we need to choose
+                                # a new name that is also not in
+                                # authenticatorPlugins and not in
+                                # pau.keys()...
+                                ct = 0
+                                nm = '%s_%d' % (util.__name__, ct)
+                                while (nm in pau.authenticatorPlugins and
+                                       nm in pau):
+                                    ct += 1
+                                    nm = '%s_%d' % (util.__name__, ct)
+                                IObjectMover(util).moveTo(pau, nm)
+                            plugins[plugins.index(r.name)] = util.__name__
+                            pau.authenticatorPlugins = tuple(plugins)
+            for k, r in pau.registrationManager.items():
+                if groupfolder.IGroupFolder.providedBy(r.component):
+                    del pau.registrationManager[k]
+

Modified: Zope3/trunk/src/zope/security/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/security/interfaces.py	2006-01-20 04:14:38 UTC (rev 41373)
+++ Zope3/trunk/src/zope/security/interfaces.py	2006-01-20 04:19:08 UTC (rev 41374)
@@ -277,16 +277,30 @@
     Extends IPrincipal to contain group information.
     """
     
-    groups = List(
-        title=_("Groups"),
-        description=_("List of ids of groups the principal belongs to"),
-        value_type=TextLine(),
-        required=False)
+    groups = Attribute(
+        'An iterable of groups to which the principal directly belongs')
 
+class IGroupClosureAwarePrincipal(IGroupAwarePrincipal):
+
+    allGroups = Attribute(
+        "An iterable of the full closure of the principal's groups.")
+
 class IGroup(IPrincipal):
     """Group of principals
     """
-                
+
+class IMemberGetterGroup(IGroup):
+    """a group that can get its members"""
+
+    def getMembers():
+        """return an iterable of the members of the group"""
+
+class IMemberAwareGroup(IMemberGetterGroup):
+    """a group that can both set and get its members."""
+
+    def setMembers(value):
+        """set members of group to the principal ids in the iterable value"""
+
 class IPermission(Interface):
     """A permission object."""
 



More information about the Zope3-Checkins mailing list