[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