[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/authentication/ Fixed a bug in group handling. Authentication-utility prefixes

Jim Fulton jim at zope.com
Mon Jun 13 18:51:26 EDT 2005


Log message for revision 30787:
  Fixed a bug in group handling.  Authentication-utility prefixes
  weren't handled properly.  To fix this, I had to change the factory
  and creation event APIs to make the authentication utility available
  to event subscribers.
  
  I also got rid of some backward compatibility code that was slated to
  disappear in 3.1.
  

Changed:
  U   Zope3/trunk/src/zope/app/authentication/README.txt
  U   Zope3/trunk/src/zope/app/authentication/__init__.py
  U   Zope3/trunk/src/zope/app/authentication/authentication.py
  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/interfaces.py
  U   Zope3/trunk/src/zope/app/authentication/principalfolder.py

-=-
Modified: Zope3/trunk/src/zope/app/authentication/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/README.txt	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/README.txt	2005-06-13 22:51:26 UTC (rev 30787)
@@ -40,6 +40,7 @@
 
 Simple Credentials Plugin
 -------------------------
+
 To illustrate, we'll create a simple credentials plugin:
 
   >>> from zope import interface
@@ -64,6 +65,7 @@
 
 Simple Authenticator Plugin
 ---------------------------
+
 Next we'll create a simple authenticator plugin. For our plugin, we'll need
 an implementation of IPrincipalInfo:
 
@@ -111,6 +113,7 @@
 
 Configuring a PAU
 -----------------
+
 Finally, we'll create the PAU itself:
 
   >>> from zope.app import authentication
@@ -123,6 +126,7 @@
 
 Using the PAU to Authenticate
 -----------------------------
+
 We can now use the PAU to authenticate a sample request:
 
   >>> from zope.publisher.browser import TestRequest
@@ -146,6 +150,7 @@
 
 Authenticated Principal Creates Events
 --------------------------------------
+
 We can verify that the appropriate event was published:
 
   >>> [event] = getEvents(interfaces.IAuthenticatedPrincipalCreated)
@@ -157,7 +162,7 @@
   True
 
 Normally, we provide subscribers to these events that add additional
-information to the principal. For examples, we'll add one that sets
+information to the principal. For example, we'll add one that sets
 the title:
 
   >>> def add_info(event):
@@ -172,6 +177,7 @@
 
 Multiple Authenticator Plugins
 ------------------------------
+
 The PAU works with multiple authenticator plugins. It uses each plugin, in the
 order specified in the PAU's authenticatorPlugins attribute, to authenticate
 a set of credentials.
@@ -222,6 +228,7 @@
 
 Multiple Credentials Plugins
 ----------------------------
+
 As with with authenticators, we can specify multiple credentials plugins. To
 illustrate, we'll create a credentials plugin that extracts credentials from
 a request form:
@@ -394,6 +401,7 @@
 
 Found Principal Creates Events
 ------------------------------
+
 As evident in the authenticator's 'createFoundPrincipal' method (see above),
 a FoundPrincipalCreatedEvent is published when the authenticator finds a
 principal on behalf of PAU's 'getPrincipal':
@@ -420,6 +428,7 @@
 
 Multiple Authenticator Plugins
 ------------------------------
+
 As with the other operations we've seen, the PAU uses multiple plugins to
 find a principal. If the first authenticator plugin can't find the requested
 principal, the next plugin is used, and so on.
@@ -551,6 +560,7 @@
 
 Challenge Protocols
 -------------------
+
 Sometimes, we want multiple challengers to work together. For example, the
 HTTP specification allows multiple challenges to be issued in a response. A
 challenge plugin can provide a `challengeProtocol` attribute that effectively

Modified: Zope3/trunk/src/zope/app/authentication/__init__.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/__init__.py	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/__init__.py	2005-06-13 22:51:26 UTC (rev 30787)
@@ -18,4 +18,3 @@
 
 import interfaces
 from zope.app.authentication.authentication import PluggableAuthentication
-from zope.app.authentication.authentication import LocalPluggableAuthentication

Modified: Zope3/trunk/src/zope/app/authentication/authentication.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/authentication.py	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/authentication.py	2005-06-13 22:51:26 UTC (rev 30787)
@@ -27,7 +27,6 @@
 
 from zope.app.authentication import interfaces
 
-
 class PluggableAuthentication(SiteManagementFolder):
 
     zope.interface.implements(
@@ -60,7 +59,7 @@
                 if info is None:
                     continue
                 principal = component.getMultiAdapter((info, request),
-                    interfaces.IAuthenticatedPrincipalFactory)()
+                    interfaces.IAuthenticatedPrincipalFactory)(self)
                 principal.id = self.prefix + info.id
                 return principal
         return None
@@ -78,7 +77,7 @@
             info = authplugin.principalInfo(id)
             if info is None:
                 continue
-            principal = interfaces.IFoundPrincipalFactory(info)()
+            principal = interfaces.IFoundPrincipalFactory(info)(self)
             principal.id = self.prefix + info.id
             return principal
         next = queryNextUtility(self, IAuthentication)
@@ -88,8 +87,9 @@
 
     def getQueriables(self):
         for name in self.authenticatorPlugins:
-            authplugin = component.queryUtility(interfaces.IAuthenticatorPlugin,
-                                                name, context=self)
+            authplugin = component.queryUtility(
+                interfaces.IAuthenticatorPlugin,
+                name, context=self)
             if authplugin is None:
                 continue
             queriable = interfaces.IQueriableAuthenticator(authplugin, None)
@@ -141,26 +141,3 @@
             next = queryNextUtility(self, IAuthentication)
             if next is not None:
                 next.logout(request)
-
-    # BBB gone in 3.1
-    def getPrincipals(self, name):
-        return ()
-
-    # BBB gone in 3.1
-    def __len__(self):
-        return hasattr(self, '_SampleContainer__data') and \
-            len(self._SampleContainer__data) or 0
-
-    # BBB gone in 3.1
-    def items(self):
-        return hasattr(self, '_SampleContainer__data') and \
-            self._SampleContainer__data.items() or []
-
-    # BBB gone in 3.1
-    def __iter__(self):
-        return hasattr(self, '_SampleContainer__data') and \
-            iter(self._SampleContainer__data) or iter([])
-
-
-# BBB, gone in 3.1
-LocalPluggableAuthentication = PluggableAuthentication

Modified: Zope3/trunk/src/zope/app/authentication/groupfolder.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/groupfolder.py	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/groupfolder.py	2005-06-13 22:51:26 UTC (rev 30787)
@@ -215,14 +215,14 @@
     """A user has a group id for a group that can't be found
     """
 
-def nocycles(principal_id, seen, getPrincipal):
-    if principal_id in seen:
-        raise GroupCycle(principal_id, seen)
-    seen.append(principal_id)
-    principal = getPrincipal(principal_id)
-    for group_id in principal.groups:
-        nocycles(group_id, seen, getPrincipal)
-    seen.pop()
+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):
 
@@ -236,28 +236,36 @@
         self.title = title
         self.description = description
 
-    def setPrincipals(self, prinlist):
+    def setPrincipals(self, prinlist, check=True):
         parent = self.__parent__
+        old = self._principals
+        self._principals = tuple(prinlist)
+
         if parent is not None:
-            old = set(self._principals)
+            oldset = set(old)
             new = set(prinlist)
             group_id = parent._groupid(self)
 
-            for principal_id in old - new:
+            for principal_id in oldset - new:
                 try:
                     parent._removePrincipalFromGroup(principal_id, group_id)
                 except AttributeError:
                     pass
 
-            for principal_id in new - old:
+            for principal_id in new - oldset:
                 try:
                     parent._addPrincipalToGroup(principal_id, group_id)
                 except AttributeError:
                     pass
 
-            nocycles(group_id, [], zapi.principals().getPrincipal)
+            if check:
+                try:
+                    nocycles(new, [], zapi.principals().getPrincipal)
+                except GroupCycle:
+                    # abort
+                    self.setPrincipals(old, False)
+                    raise
 
-        self._principals = tuple(prinlist)
 
     principals = property(lambda self: self._principals, setPrincipals)
 
@@ -284,14 +292,18 @@
     if not IGroupAwarePrincipal.providedBy(principal):
         return
 
+    authentication = event.authentication
+
     plugins = zapi.getUtilitiesFor(interfaces.IAuthenticatorPlugin)
     for name, plugin in plugins:
         if not IGroupFolder.providedBy(plugin):
             continue
         groupfolder = plugin
         principal.groups.extend(
-            groupfolder.getGroupsForPrincipal(principal.id),)
+            [authentication.prefix + id
+             for id in groupfolder.getGroupsForPrincipal(principal.id)
+             ])
         id = principal.id
-        prefix = groupfolder.prefix
+        prefix = authentication.prefix + groupfolder.prefix
         if id.startswith(prefix) and id[len(prefix):] in groupfolder:
             alsoProvides(principal, IGroup)

Modified: Zope3/trunk/src/zope/app/authentication/groupfolder.txt
===================================================================
--- Zope3/trunk/src/zope/app/authentication/groupfolder.txt	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/groupfolder.txt	2005-06-13 22:51:26 UTC (rev 30787)
@@ -31,6 +31,7 @@
 
   >>> from zope import interface
   >>> from zope.app.security.interfaces import IAuthentication
+  >>> from zope.app.security.interfaces import PrincipalLookupError
   >>> from zope.security.interfaces import IGroupAwarePrincipal
   >>> from zope.app.authentication.groupfolder import setGroupsForPrincipal
 
@@ -41,7 +42,8 @@
   ...         self.groups = []
 
   >>> class PrincipalCreatedEvent:
-  ...     def __init__(self, principal):
+  ...     def __init__(self, authentication, principal):
+  ...         self.authentication = authentication
   ...         self.principal = principal
 
   >>> from zope.app.authentication import principalfolder
@@ -50,7 +52,8 @@
   ...
   ...     interface.implements(IAuthentication)
   ...
-  ...     def __init__(self, groups):
+  ...     def __init__(self, groups, prefix='auth.'):
+  ...         self.prefix = prefix
   ...         self.principals = {
   ...            'p1': principalfolder.PrincipalInfo('p1', '', '', ''),
   ...            'p2': principalfolder.PrincipalInfo('p2', '', '', ''),
@@ -58,13 +61,17 @@
   ...         self.groups = 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:
-  ...                return None
-  ...         principal = Principal(info.id, info.title, info.description)
-  ...         setGroupsForPrincipal(PrincipalCreatedEvent(principal))
+  ...                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
@@ -85,13 +92,13 @@
 
 Now we can set the principals on the group:
 
-  >>> g1.principals = ['p1', 'p2']
+  >>> g1.principals = ['auth.p1', 'auth.p2']
   >>> g1.principals
-  ('p1', 'p2')
+  ('auth.p1', 'auth.p2')
 
 This allows us to look up groups for the principals:
 
-  >>> groups.getGroupsForPrincipal('p1')
+  >>> groups.getGroupsForPrincipal('auth.p1')
   (u'group.g1',)
 
 Note that the group id is a concatenation of the group-folder prefix
@@ -104,20 +111,20 @@
 then the groups folder loses the group information for that group's
 principals:
 
-  >>> groups.getGroupsForPrincipal('p1')
+  >>> groups.getGroupsForPrincipal('auth.p1')
   ()
 
 but the principal information on the group is unchanged:
 
   >>> g1.principals
-  ('p1', 'p2')
+  ('auth.p1', 'auth.p2')
 
 Adding the group sets the folder principal information.  Let's use a
 different group name:
 
   >>> groups['G1'] = g1
 
-  >>> groups.getGroupsForPrincipal('p1')
+  >>> groups.getGroupsForPrincipal('auth.p1')
   (u'group.G1',)
 
 Here we see that the new name is reflected in the group information.
@@ -126,17 +133,19 @@
 
   >>> g2 = zope.app.authentication.groupfolder.GroupInformation("Group Two")
   >>> groups['G2'] = g2
-  >>> g2.principals = ['group.G1']
+  >>> g2.principals = ['auth.group.G1']
 
-  >>> groups.getGroupsForPrincipal('group.G1')
+  >>> groups.getGroupsForPrincipal('auth.group.G1')
   (u'group.G2',)
 
 Groups cannot contain cycles:
 
-  >>> g1.principals = ('p1', 'p2', 'group.G2')
+  >>> g1.principals = ('auth.p1', 'auth.p2', 'auth.group.G2')
+  ... # doctest: +NORMALIZE_WHITESPACE
   Traceback (most recent call last):
   ...
-  GroupCycle: (u'group.G1', [u'group.G1', u'group.G2'])
+  GroupCycle: (u'auth.group.G1', 
+               ['auth.p2', u'auth.group.G1', u'auth.group.G2'])
 
 They need not be hierarchical:
 
@@ -145,17 +154,17 @@
 
   >>> gb = zope.app.authentication.groupfolder.GroupInformation("Group B")
   >>> groups['GB'] = gb
-  >>> gb.principals = ['group.GA']
+  >>> gb.principals = ['auth.group.GA']
 
   >>> gc = zope.app.authentication.groupfolder.GroupInformation("Group C")
   >>> groups['GC'] = gc
-  >>> gc.principals = ['group.GA']
+  >>> gc.principals = ['auth.group.GA']
 
   >>> gd = zope.app.authentication.groupfolder.GroupInformation("Group D")
   >>> groups['GD'] = gd
-  >>> gd.principals = ['group.GA', 'group.GB']
+  >>> gd.principals = ['auth.group.GA', 'auth.group.GB']
 
-  >>> ga.principals = ['p1']
+  >>> ga.principals = ['auth.p1']
 
 Group folders provide a very simple search interface.  They perform
 simple string searches on group titles and descriptions.
@@ -184,19 +193,19 @@
 principal-creation events.  It adds any group-folder-defined groups to
 users in those groups:
 
-  >>> principal = principals.getPrincipal('p1')
+  >>> principal = principals.getPrincipal('auth.p1')
 
   >>> principal.groups
-  [u'group.G1', u'group.GA']
+  [u'auth.group.G1', u'auth.group.GA']
 
 Of course, this applies to groups too:
 
-  >>> principal = principals.getPrincipal('group.G1')
+  >>> principal = principals.getPrincipal('auth.group.G1')
   >>> principal.id
-  'group.G1'
+  'auth.group.G1'
 
   >>> principal.groups
-  [u'group.G2']
+  [u'auth.group.G2']
 
 In addition to setting principal groups, the `setGroupsForPrincipal`
 function also declares the `IGroup` interface on groups:
@@ -205,7 +214,7 @@
   ['IGroup', 'IGroupAwarePrincipal']
 
   >>> [iface.__name__
-  ...  for iface in interface.providedBy(principals.getPrincipal('p1'))]
+  ...  for iface in interface.providedBy(principals.getPrincipal('auth.p1'))]
   ['IGroupAwarePrincipal']
 
 Special groups
@@ -229,7 +238,7 @@
 because the groups haven't been defined:
 
   >>> prin = GroupAwarePrincipal('x')
-  >>> event = interfaces.FoundPrincipalCreated(prin, {})
+  >>> event = interfaces.FoundPrincipalCreated(42, prin, {})
   >>> zope.app.authentication.groupfolder.specialGroups(event)
   >>> prin.groups
   []
@@ -282,7 +291,35 @@
   ...     interface.implements(zope.security.interfaces.IPrincipal)
   ...     id = title = description = ''
 
-  >>> event = interfaces.FoundPrincipalCreated(SolitaryPrincipal(), {})
+  >>> event = interfaces.FoundPrincipalCreated(42, SolitaryPrincipal(), {})
   >>> zope.app.authentication.groupfolder.specialGroups(event)
   >>> prin.groups
   []
+
+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 cntaining 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).

Modified: Zope3/trunk/src/zope/app/authentication/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/interfaces.py	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/interfaces.py	2005-06-13 22:51:26 UTC (rev 30787)
@@ -127,10 +127,14 @@
 class IPrincipalFactory(zope.interface.Interface):
     """A principal factory."""
 
-    def __call__():
-        """Creates a principal."""
+    def __call__(authentication):
+        """Creates a principal.
 
+        The authentication utility that called the factory is passed
+        and should be included in the principal-created event.
+        """
 
+
 class IFoundPrincipalFactory(IPrincipalFactory):
     """A found principal factory."""
 
@@ -144,6 +148,9 @@
 
     principal = zope.interface.Attribute("The principal that was created")
 
+    authentication = zope.interface.Attribute(
+        "The authentication utility that created the principal")
+
     info = zope.interface.Attribute("An object providing IPrincipalInfo.")
 
 
@@ -158,7 +165,8 @@
 
     zope.interface.implements(IAuthenticatedPrincipalCreated)
 
-    def __init__(self, principal, info, request):
+    def __init__(self, authentication, principal, info, request):
+        self.authentication = authentication
         self.principal = principal
         self.info = info
         self.request = request
@@ -172,7 +180,8 @@
 
     zope.interface.implements(IFoundPrincipalCreated)
 
-    def __init__(self, principal, info):
+    def __init__(self, authentication, principal, info):
+        self.authentication = authentication
         self.principal = principal
         self.info = info
 

Modified: Zope3/trunk/src/zope/app/authentication/principalfolder.py
===================================================================
--- Zope3/trunk/src/zope/app/authentication/principalfolder.py	2005-06-13 20:55:05 UTC (rev 30786)
+++ Zope3/trunk/src/zope/app/authentication/principalfolder.py	2005-06-13 22:51:26 UTC (rev 30787)
@@ -291,7 +291,7 @@
       >>> from zope.publisher.base import TestRequest
       >>> request = TestRequest('/')
       >>> factory = AuthenticatedPrincipalFactory(info, request)
-      >>> principal = factory()
+      >>> principal = factory(42)
 
     The factory uses the info to create a principal with the same ID, title,
     and description:
@@ -307,8 +307,8 @@
 
       >>> from zope.app.event.tests.placelesssetup import getEvents
       >>> [event] = getEvents(interfaces.IAuthenticatedPrincipalCreated)
-      >>> event.principal is principal
-      True
+      >>> event.principal is principal, event.authentication == 42
+      (True, True)
       >>> event.info
       PrincipalInfo('users.mary')
       >>> event.request is request
@@ -328,11 +328,11 @@
         self.info = info
         self.request = request
 
-    def __call__(self):
+    def __call__(self, authentication):
         principal = Principal(self.info.id, self.info.title,
                               self.info.description)
         notify(interfaces.AuthenticatedPrincipalCreated(
-            principal, self.info, self.request))
+            authentication, principal, self.info, self.request))
         return principal
 
 
@@ -346,7 +346,7 @@
 
       >>> info = PrincipalInfo('users.sam', 'sam', 'Sam', 'A site user.')
       >>> factory = FoundPrincipalFactory(info)
-      >>> principal = factory()
+      >>> principal = factory(42)
 
     The factory uses the info to create a principal with the same ID, title,
     and description:
@@ -362,8 +362,8 @@
 
       >>> from zope.app.event.tests.placelesssetup import getEvents
       >>> [event] = getEvents(interfaces.IFoundPrincipalCreated)
-      >>> event.principal is principal
-      True
+      >>> event.principal is principal, event.authentication == 42
+      (True, True)
       >>> event.info
       PrincipalInfo('users.sam')
 
@@ -380,8 +380,9 @@
     def __init__(self, info):
         self.info = info
 
-    def __call__(self):
+    def __call__(self, authentication):
         principal = Principal(self.info.id, self.info.title,
                               self.info.description)
-        notify(interfaces.FoundPrincipalCreated(principal, self.info))
+        notify(interfaces.FoundPrincipalCreated(authentication,
+                                                principal, self.info))
         return principal



More information about the Zope3-Checkins mailing list