[Checkins] SVN: Products.PluggableAuthService/trunk/ Merged branch maurits-login-transform.

Maurits van Rees cvs-admin at zope.org
Tue Jan 22 10:36:27 UTC 2013


Log message for revision 129077:
  Merged branch maurits-login-transform.

Changed:
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/PluggableAuthService.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/authservice.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/plugins.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/BasePlugin.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/ZODBUserManager.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_PluggableAuthService.py
  U   Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_exportimport.py
  U   Products.PluggableAuthService/trunk/buildout.cfg

-=-
Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/PluggableAuthService.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/PluggableAuthService.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/PluggableAuthService.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -181,6 +181,18 @@
 
     maxlistusers = -1   # Don't allow local role form to try to list us!
 
+    # Method for transforming a login name.  This needs to be the name
+    # of a method on this plugin.  See the applyTransform method.
+    login_transform = ''
+
+    _properties = (
+        dict(id='title', type='string', mode='w',
+             label='Title'),
+        dict(id='login_transform', type='string', mode='w',
+             label='Transform to apply to login name'),
+             )
+
+
     def getId( self ):
 
         return self._id
@@ -195,6 +207,7 @@
         """
         plugins = self._getOb( 'plugins' )
 
+        name = self.applyTransform( name )
         user_info = self._verifyUser( plugins, login=name )
 
         if not user_info:
@@ -294,7 +307,10 @@
         if search_name:
             if kw.get('id') is not None:
                 del kw['id'] # don't even bother searching by id
-            kw['login'] = kw['name']
+            # Note: name can be a sequence.
+            kw['login'] = self.applyTransform( kw['name'] )
+        if kw.get('login', None):
+            kw['login'] = self.applyTransform( kw['login'] )
 
         plugins = self._getOb( 'plugins' )
         enumerators = plugins.listPlugins( IUserEnumerationPlugin )
@@ -400,9 +416,14 @@
             if not kw.has_key('title'):
                 kw['title'] = search_name
             kw['login'] = search_name
-
+            
+        # For groups we search the original name
+        # (e.g. Administrators), for users we apply the transform,
+        # which could lowercase the name.
+        groups = [ d.copy() for d in self.searchGroups( **kw ) ]
+        if kw.get('login', None):
+            kw['login'] = self.applyTransform( kw['login'] )
         users = [ d.copy() for d in self.searchUsers( **kw ) ]
-        groups = [ d.copy() for d in self.searchGroups( **kw ) ]
 
         if groups_first:
             result = groups + users
@@ -613,6 +634,7 @@
                     return [ ( user_id, name ) ]
 
                 # Now see if the user ids can be retrieved from the cache
+                credentials['login'] = self.applyTransform( credentials.get('login') )
                 view_name = createViewName('_extractUserIds',
                                            credentials.get('login'))
                 keywords = createKeywords(**credentials)
@@ -723,6 +745,7 @@
 
         """ Allow IUserFactoryPlugins to create, or fall back to default.
         """
+        name = self.applyTransform( name )
         factories = plugins.listPlugins( IUserFactoryPlugin )
 
         for factory_id, factory in factories:
@@ -744,6 +767,7 @@
 
         # See if the user can be retrieved from the cache
         view_name = createViewName('_findUser', user_id)
+        name = self.applyTransform( name )
         keywords = createKeywords(user_id=user_id, name=name)
         user = self.ZCacheable_get( view_name=view_name
                                   , keywords=keywords
@@ -805,7 +829,7 @@
             criteria[ 'id' ] = user_id
 
         if login is not None:
-            criteria[ 'login' ] = login
+            criteria[ 'login' ] = self.applyTransform( login )
 
         view_name = createViewName('_verifyUser', user_id or login)
         keywords = createKeywords(**criteria)
@@ -969,6 +993,7 @@
         roleassigners = plugins.listPlugins( IRoleAssignerPlugin )
 
         user = None
+        login = self.applyTransform( login )
 
         if not (useradders and roleassigners):
             raise NotImplementedError( "There are no plugins"
@@ -1041,6 +1066,74 @@
         resp._unauthorized = self._unauthorized
         resp._has_challenged = False
 
+    security.declarePublic( 'applyTransform' )
+    def applyTransform( self, value ):
+        """ Transform for login name.
+
+        Possibly transform the login, for example by making it lower
+        case.
+
+        value must be a string (or unicode) or it may be a sequence
+        (list, tuple), in which case we need to iterate over it.
+        """
+        if not value:
+            return value
+        transform = self._get_login_transform_method()
+        if not transform:
+            return value
+        if isinstance(value, basestring):
+            return transform(value)
+        result = []
+        for v in value:
+            result.append(transform(v))
+        if isinstance(value, tuple):
+            return tuple(result)
+        return result
+
+    security.declarePrivate( '_get_login_transform_method' )
+    def _get_login_transform_method( self ):
+        """ Get the transform method for the login name or None.
+        """
+        login_transform = getattr(self, 'login_transform', None)
+        if not login_transform:
+            return
+        transform = getattr(self, login_transform.strip(), None)
+        if transform is None:
+            logger.debug("Transform method %r not found in plugin %r.",
+                         self.login_transform, self)
+            return
+        return transform
+
+    security.declarePrivate( '_setPropValue' )
+    def _setPropValue(self, id, value):
+        if id == 'login_transform':
+            orig_value = getattr(self, id)
+        super(PluggableAuthService, self)._setPropValue(id, value)
+        if id == 'login_transform' and value and value != orig_value:
+            logger.debug("login_transform changed from %r to %r. "
+                         "Updating existing login names.", orig_value, value)
+            self.updateAllLoginNames()
+
+    security.declarePublic( 'lower' )
+    def lower( self, value ):
+        """ Transform for login name.
+
+        Strip the value and lowercase it.
+
+        To use this, set login_tranform to 'lower'.
+        """
+        return value.strip().lower()
+
+    security.declarePublic( 'upper' )
+    def upper( self, value ):
+        """ Transform for login name.
+
+        Strip the value and uppercase it.
+
+        To use this, set login_tranform to 'upper'.
+        """
+        return value.strip().upper()
+
     #
     # Response override
     #
@@ -1133,6 +1226,7 @@
         but the credentials are not stored in the CookieAuthHelper cookie
         but somewhere else, like in a Session.
         """
+        login = self.applyTransform(login)
         plugins = self._getOb('plugins')
         cred_updaters = plugins.listPlugins(ICredentialsUpdatePlugin)
 
@@ -1164,6 +1258,94 @@
             for resetter_id, resetter in cred_resetters:
                 resetter.resetCredentials(request, response)
 
+
+    security.declareProtected( ManageUsers, 'updateLoginName')
+    def updateLoginName(self, user_id, login_name):
+        """Update login name of user.
+        """
+        logger.debug("Called updateLoginName, user_id=%r, login_name=%r",
+                     user_id, login_name)
+        login_name = self.applyTransform(login_name)
+        # Note: we do not call self.getUserById(user_id) here to see
+        # if this user exists, or call getUserName() on the answer to
+        # compare it with the transformed login name, because the user
+        # may be reported with a login name that is transformed on the
+        # fly, for example in the _verifyUser call, even though the
+        # plugin has not actually transformed the login name yet in
+        # the backend.
+        self._updateLoginName(user_id, login_name)
+
+    security.declarePublic('updateOwnLoginName')
+    def updateOwnLoginName(self, login_name):
+        """Update own login name of authenticated user.
+        """
+        logger.debug("Called updateOwnLoginName, login_name=%r", login_name)
+        login_name = self.applyTransform(login_name)
+        user = getSecurityManager().getUser()
+        if aq_base(user) is nobody:
+            return
+        user_id = user.getId()
+        # Note: we do not compare the login name here.  See the
+        # comment in updateLoginName above.
+        self._updateLoginName(user_id, login_name)
+
+    def _updateLoginName(self, user_id, login_name):
+        # Note: we do not compare the login name here.  See the
+        # comment in updateLoginName above.
+        plugins = self._getOb('plugins')
+        updaters = plugins.listPlugins(IUserEnumerationPlugin)
+
+        # Call the updaters.  One of them *must* succeed without an
+        # exception, even if it does not change anything.  When a
+        # login name is already taken, we do not want to fail
+        # silently.
+        success = False
+        for updater_id, updater in updaters:
+            if not hasattr(updater, 'updateUser'):
+                # This was a later addition to the interface, so we
+                # are forgiving.
+                logger.warn("%s plugin lacks updateUser method of "
+                            "IUserEnumerationPlugin.", updater_id)
+                continue
+            try:
+                updater.updateUser(user_id, login_name)
+            except _SWALLOWABLE_PLUGIN_EXCEPTIONS:
+                reraise(updater)
+                logger.debug('UpdateLoginNamePlugin %s error' % updater_id,
+                             exc_info=True)
+            else:
+                success = True
+                logger.debug("login name changed to: %r", login_name)
+
+        if not success:
+            raise ValueError("Cannot update login name of user %r to %r. "
+                             "Possibly duplicate." % (user_id, login_name))
+
+    security.declareProtected( ManageUsers, 'updateLoginName')
+    def updateAllLoginNames(self, quit_on_first_error=True):
+        """Update login names of all users to their canonical value.
+
+        This should be done after changing the login_transform
+        property of PAS.
+
+        You can set quit_on_first_error to False to report all errors
+        before quitting with an error.  This can be useful if you want
+        to know how many problems there are, if any.
+        """
+        plugins = self._getOb('plugins')
+        updaters = plugins.listPlugins(IUserEnumerationPlugin)
+        for updater_id, updater in updaters:
+            if not hasattr(updater, 'updateEveryLoginName'):
+                # This was a later addition to the interface, so we
+                # are forgiving.
+                logger.warn("%s plugin lacks updateEveryLoginName method of "
+                            "IUserEnumerationPlugin.", updater_id)
+                continue
+            # Note: do not swallow any exceptions here.
+            updater.updateEveryLoginName(
+                quit_on_first_error=quit_on_first_error)
+
+
 classImplements( PluggableAuthService
                , (IPluggableAuthService, IObjectManager, IPropertyManager)
                )

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/authservice.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/authservice.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/authservice.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -210,11 +210,31 @@
         default implementation redirects to HTTP_REFERER).
         """
 
-    def resetCredentials(self, request, response):
+    def resetCredentials(request, response):
         """Reset credentials by informing all active resetCredentials
         plugins
         """
 
+    def updateLoginName(user_id, login_name):
+        """Update login name of user.
+        """
+
+    def updateOwnLoginName(login_name):
+        """Update own login name of authenticated user.
+        """
+
+    def updateAllLoginNames(quit_on_first_error=True):
+        """Update login names of all users to their canonical value.
+
+        This should be done after changing the login_transform
+        property of PAS.
+
+        You can set quit_on_first_error to False to report all errors
+        before quitting with an error.  This can be useful if you want
+        to know how many problems there are, if any.
+        """
+
+
 # The IMutableUserFolder and IEnumerableFolder are not supported
 # out-of-the-box by the pluggable authentication service.  These
 # interfaces describe contracts that other standard Zope user folders

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/plugins.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/plugins.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/interfaces/plugins.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -345,6 +345,21 @@
           scaling issues for some implementations.
         """
 
+    def updateUser( user_id, login_name ):
+        """ Update the login name of the user with id user_id.
+        """
+
+    def updateEveryLoginName(quit_on_first_error=True):
+        """Update login names of all users to their canonical value.
+
+        This should be done after changing the login_transform
+        property of PAS.
+
+        You can set quit_on_first_error to False to report all errors
+        before quitting with an error.  This can be useful if you want
+        to know how many problems there are, if any.
+        """
+
 class IGroupEnumerationPlugin( Interface ):
 
     """ Allow querying groups by ID, and searching for groups.
@@ -364,8 +379,8 @@
         o Return mappings for groups matching the given criteria.
 
         o 'id' in combination with 'exact_match' true, will
-          return at most one mapping per supplied ID ('id' and 'login'
-          may be sequences).
+          return at most one mapping per supplied ID ('id'
+          may be a sequence).
 
         o If 'exact_match' is False, then 'id' may be treated by
           the plugin as "contains" searches (more complicated searches
@@ -415,8 +430,8 @@
         o Return mappings for roles matching the given criteria.
 
         o 'id' in combination with 'exact_match' true, will
-          return at most one mapping per supplied ID ('id' and 'login'
-          may be sequences).
+          return at most one mapping per supplied ID ('id'
+          may be a sequence).
 
         o If 'exact_match' is False, then 'id' may be treated by
           the plugin as "contains" searches (more complicated searches

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/BasePlugin.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/BasePlugin.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/BasePlugin.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -117,6 +117,23 @@
             view_name = createViewName('_findUser', id)
             pas.ZCacheable_invalidate(view_name)
 
+    security.declarePublic( 'applyTransform' )
+    def applyTransform( self, value ):
+        """ Transform for login name.
+
+        Possibly transform the login, for example by making it lower
+        case.
+
+        Normally this is done in PAS itself, but in some cases a
+        method in a plugin may need to do it itself, when there is no
+        method in PAS that calls that method.
+        """
+        pas = self._getPAS()
+        if pas is not None:
+            return pas.applyTransform(value)
+        return value
+
+
 classImplements(BasePlugin, *implementedBy(SimpleItem))
 
 InitializeClass(BasePlugin)

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/ZODBUserManager.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/ZODBUserManager.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/ZODBUserManager.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -16,6 +16,7 @@
 $Id$
 """
 import copy
+import logging
 try:
     from hashlib import sha1 as sha
 except:
@@ -46,7 +47,9 @@
 from Products.PluggableAuthService.utils import createViewName
 from Products.PluggableAuthService.utils import csrf_only
 
+logger = logging.getLogger('PluggableAuthService')
 
+
 class IZODBUserManager(Interface):
     """ Marker interface.
     """
@@ -323,6 +326,62 @@
             self._login_to_userid[login_name] = user_id
             self._userid_to_login[user_id] = login_name
 
+    security.declarePrivate('updateEveryLoginName')
+    def updateEveryLoginName(self, quit_on_first_error=True):
+        # Update all login names to their canonical value.  This
+        # should be done after changing the login_transform property
+        # of pas.  You can set quit_on_first_error to False to report
+        # all errors before quitting with an error.  This can be
+        # useful if you want to know how many problems there are, if
+        # any.
+        pas = self._getPAS()
+        transform = pas._get_login_transform_method()
+        if not transform:
+            logger.warn("PAS has a non-existing, empty or wrong "
+                        "login_transform property.")
+            return
+
+        # Make a fresh mapping, as we do not want to add or remove
+        # items to the original mapping while we are iterating over
+        # it.
+        new_login_to_userid = OOBTree()
+        errors = []
+        for old_login_name, user_id in self._login_to_userid.items():
+            new_login_name = transform(old_login_name)
+            if new_login_name in new_login_to_userid:
+                logger.error("User id %s: login name %r already taken.",
+                             user_id, new_login_name)
+                errors.append(new_login_name)
+                if quit_on_first_error:
+                    break
+            new_login_to_userid[new_login_name] = user_id
+            if new_login_name != old_login_name:
+                self._userid_to_login[user_id] = new_login_name
+                # Also, remove from the cache
+                view_name = createViewName('enumerateUsers', user_id)
+                self.ZCacheable_invalidate(view_name=view_name)
+                logger.debug("User id %s: changed login name from %r to %r.",
+                             user_id, old_login_name, new_login_name)
+
+        # If there were errors, we do not want to save any changes.
+        if errors:
+            logger.error("There were %d errors when updating login names. "
+                         "quit_on_first_error was %r", len(errors),
+                         quit_on_first_error)
+            # Make sure the exception we raise is not swallowed.
+            self._dont_swallow_my_exceptions = True
+            raise ValueError("Transformed login names are not unique: %s." %
+                             ', '.join(errors))
+
+        # Make sure we did not lose any users.
+        assert(len(self._login_to_userid.keys())
+               == len(new_login_to_userid.keys()))
+        # Empty the main cache.
+        view_name = createViewName('enumerateUsers')
+        self.ZCacheable_invalidate(view_name=view_name)
+        # Store the new login mapping.
+        self._login_to_userid = new_login_to_userid
+
     security.declarePrivate( 'removeUser' )
     def removeUser( self, user_id ):
 

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/plugins/tests/test_ZODBUserManager.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -26,6 +26,7 @@
 from Products.PluggableAuthService.plugins.tests.helpers \
      import makeRequestAndResponse
 
+
 class DummyUser:
 
     def __init__( self, id ):
@@ -34,6 +35,28 @@
     def getId( self ):
         return self._id
 
+
+class FakePAS(object):
+
+    def _get_login_transform_method(self):
+        return None
+
+    def applyTransform(self, value):
+        return value
+
+
+class FakeLowerCasePAS(object):
+
+    def _get_login_transform_method(self):
+        return self.lower
+
+    def lower(self, value):
+        return value.lower()
+
+    def applyTransform(self, value):
+        return value.lower()
+
+
 class ZODBUserManagerTests( unittest.TestCase
                           , IAuthenticationPlugin_conformance
                           , IUserEnumerationPlugin_conformance
@@ -470,6 +493,52 @@
         self.assertRaises(ValueError,
                           zum.updateUser, 'user1', 'user2 at example.com')
 
+    def test_updateEveryLoginName(self):
+
+        zum = self._makeOne()
+        zum._getPAS = lambda: FakePAS()
+
+        # Create two users and make sure we can authenticate with it
+        zum.addUser( 'User1', 'User1 at Example.Com', 'password' )
+        zum.addUser( 'User2', 'User2 at Example.Com', 'password' )
+        info1 = { 'login' : 'User1 at Example.Com', 'password' : 'password' }
+        info2 = { 'login' : 'User2 at Example.Com', 'password' : 'password' }
+        user_id, login = zum.authenticateCredentials(info1)
+        self.assertEqual(user_id, 'User1')
+        self.assertEqual(login, 'User1 at Example.Com')
+        user_id, login = zum.authenticateCredentials(info2)
+        self.assertEqual(user_id, 'User2')
+        self.assertEqual(login, 'User2 at Example.Com')
+
+        # Give all users a new login, using the applyTransform method
+        # of PAS.  There should be no changes.
+        zum.updateEveryLoginName()
+        self.failUnless(zum.authenticateCredentials(info1))
+        self.failUnless(zum.authenticateCredentials(info2))
+
+        # Use a PAS configured to transform login names to lower case.
+        zum._getPAS = lambda: FakeLowerCasePAS()
+
+        # Update all login names
+        zum.updateEveryLoginName()
+
+        # The old mixed case logins no longer work.  Note that if you
+        # query PAS (via the validate or _extractUserIds method), PAS
+        # is responsible for transforming the login before passing it
+        # to our plugin.
+        self.failIf(zum.authenticateCredentials(info1))
+        self.failIf(zum.authenticateCredentials(info2))
+
+        # Authentication with all lowercase login works.
+        info1 = { 'login' : 'user1 at example.com', 'password' : 'password' }
+        info2 = { 'login' : 'user2 at example.com', 'password' : 'password' }
+        user_id, login = zum.authenticateCredentials(info1)
+        self.assertEqual(user_id, 'User1')
+        self.assertEqual(login, 'user1 at example.com')
+        user_id, login = zum.authenticateCredentials(info2)
+        self.assertEqual(user_id, 'User2')
+        self.assertEqual(login, 'user2 at example.com')
+
     def test_enumerateUsersWithOptionalMangling(self):
 
         zum = self._makeOne()

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_PluggableAuthService.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_PluggableAuthService.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_PluggableAuthService.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -13,13 +13,12 @@
 ##############################################################################
 import unittest
 
-from Acquisition import Implicit, aq_base, aq_parent
+from Acquisition import Implicit, aq_base
 from AccessControl.SecurityManagement import newSecurityManager
 from AccessControl.SecurityManagement import noSecurityManager
 from AccessControl.SecurityManager import setSecurityPolicy
 from OFS.ObjectManager import ObjectManager
-from OFS.Folder import Folder
-from zExceptions import Unauthorized, Redirect
+from zExceptions import Unauthorized
 
 from Products.PluggableAuthService.utils import directlyProvides
 from zope.interface import implements
@@ -59,10 +58,17 @@
                    , 'pluginid' : self.PLUGINID
                    } ]
 
-        if kw.get( 'id' ) == _id:
+        # Both id and login can be strings or sequences.
+        user_id = kw.get( 'id' )
+        if isinstance( user_id, basestring ):
+            user_id = [ user_id ]
+        if user_id and _id in user_id:
             return tuple(result)
 
-        if kw.get( 'login' ) == self._login:
+        login = kw.get( 'login' )
+        if isinstance( login, basestring ):
+            login = [ login ]
+        if login and self._login in login:
             return tuple(result)
 
         return ()
@@ -107,6 +113,18 @@
 
         return tuple(results)
 
+    def updateUser(self, user_id, login_name):
+        for info in self.users:
+            if info['id'] == user_id:
+                info['login'] = login_name
+                return
+
+    def updateEveryLoginName(self, quit_on_first_error=True):
+        # Let's lowercase all login names.
+        for info in self.users:
+            info['login'] = info['login'].lower()
+        
+
 class WantonUserEnumerator(DummyMultiUserEnumerator):
     def enumerateUsers( self, *args, **kw):
         # Always returns everybody.
@@ -151,6 +169,7 @@
         self._group_id = group_id
         self.identifier = None
 
+
 class DummyGroupPlugin(DummyPlugin):
 
     def __init__(self, id, groups=()):
@@ -263,10 +282,6 @@
 
         self._dict[ key ] = value
 
-    def has_key( self, key ):
-
-        return self._dict.has_key(key)
-
     def _hold(self, something):
         self._held.append(something)
 
@@ -525,6 +540,20 @@
         directlyProvides( cp, IChallengePlugin )
         return cp
 
+    def _makeMultiUserEnumerator( self, *users ):
+        # users should be something like this:
+        # [{'id': 'Foo', 'login': 'foobar'},
+        #  {'id': 'Bar', 'login': 'BAR'}]
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        enumerator = DummyMultiUserEnumerator('enumerator', *users)
+        directlyProvides( enumerator, IUserEnumerationPlugin )
+
+        return enumerator
+
+
     def test_empty( self ):
 
         zcuf = self._makeOne()
@@ -733,7 +762,7 @@
     def test__extractUserIds_authenticate_emergency_user_with_broken_extractor( self ):
 
         from Products.PluggableAuthService.interfaces.plugins \
-            import IExtractionPlugin, IAuthenticationPlugin
+            import IExtractionPlugin
 
         from AccessControl.User import UnrestrictedUser
 
@@ -940,8 +969,89 @@
         finally:
             PluggableAuthService.emergency_user = old_eu
 
+    def test__extractUserIds_transform( self ):
 
+        from Products.PluggableAuthService.interfaces.plugins \
+            import IExtractionPlugin, IAuthenticationPlugin
 
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+
+        login = DummyPlugin()
+        directlyProvides( login, IExtractionPlugin, IAuthenticationPlugin )
+        login.extractCredentials = _extractLogin
+        login.authenticateCredentials = _authLogin
+        zcuf._setObject( 'login', login )
+        # Make login names lowercase.  User ids are not affected, but
+        # our dummy _authLogin simply reports a tuple with twice the
+        # login name.
+        zcuf.login_transform = 'lower'
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IExtractionPlugin, 'login' )
+        plugins.activatePlugin( IAuthenticationPlugin, 'login' )
+
+        # Mixed case here.
+        request = self._makeRequest( form={ 'login' : 'Foo'
+                                          , 'password' : 'Bar' } )
+
+        user_ids = zcuf._extractUserIds( request=request
+                                       , plugins=zcuf.plugins
+                                       )
+
+        self.assertEqual( len( user_ids ), 1 )
+        # Lower case here.
+        self.assertEqual( user_ids[0][0], 'foo' )
+
+    def test__extractUserIds_emergency_user_always_wins_in_transform( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+            import IExtractionPlugin, IAuthenticationPlugin
+
+        from AccessControl.User import UnrestrictedUser
+
+        from Products.PluggableAuthService import PluggableAuthService
+
+        old_eu = PluggableAuthService.emergency_user
+
+        # Mixed case here.  We want to test if an emergency user with
+        # mixed (or upper) case login name is found also when the
+        # login_transform is to lower case the login.
+        eu = UnrestrictedUser( 'Foo', 'Bar', ( 'manage', ), () )
+
+        PluggableAuthService.emergency_user = eu
+
+        try:
+            plugins = self._makePlugins()
+            zcuf = self._makeOne( plugins )
+
+            login = DummyPlugin()
+            directlyProvides( login, IExtractionPlugin, IAuthenticationPlugin )
+            login.extractCredentials = lambda req: {'login': 'baz', 'password': ''}
+            login.authenticateCredentials = _authLogin
+
+            zcuf._setObject( 'login', login )
+            zcuf.login_transform = 'lower'
+
+            plugins = zcuf._getOb( 'plugins' )
+
+            plugins.activatePlugin( IExtractionPlugin, 'login' )
+            plugins.activatePlugin( IAuthenticationPlugin, 'login' )
+
+            request = self._makeRequest( form={ 'login' : eu.getUserName()
+                                              , 'password' : eu._getPassword() } )
+
+            # This should authenticate the emergency user and not 'baz'
+            user_ids = zcuf._extractUserIds( request=request
+                                           , plugins=zcuf.plugins
+                                           )
+
+            self.assertEqual( len( user_ids ), 1 )
+            self.assertEqual( user_ids[0][0], 'Foo' )
+        finally:
+            PluggableAuthService.emergency_user = old_eu
+
+
     def _isNotCompetent_test( self, decisions, result):
         from Products.PluggableAuthService.interfaces.plugins \
             import INotCompetentPlugin
@@ -1229,7 +1339,67 @@
                          None)
         self.assertEqual(zcuf._verifyUser(plugins), None)
 
+    def test__verifyUser_login_transform_lower( self ):
 
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+        zcuf.login_transform = 'lower'
+
+        enumerator = DummyMultiUserEnumerator(
+            'enumerator',
+            {'id': 'Foo', 'login': 'foobar'},
+            {'id': 'Bar', 'login': 'BAR'})
+        directlyProvides( enumerator, IUserEnumerationPlugin )
+        zcuf._setObject( 'enumerator', enumerator )
+
+        plugins = zcuf._getOb( 'plugins' )
+
+        plugins.activatePlugin( IUserEnumerationPlugin, 'enumerator' )
+
+        # No matter what we try as login parameter, it is always lower
+        # cased before verifying a user.
+        self.failIf(zcuf._verifyUser(plugins, login='BAR'))
+        self.failIf(zcuf._verifyUser(plugins, login='Bar'))
+        self.failIf(zcuf._verifyUser(plugins, login='bar'))
+        self.failUnless(
+            zcuf._verifyUser(plugins, login='FOOBAR')['id'] == 'Foo')
+        self.failUnless(
+            zcuf._verifyUser(plugins, login='Foobar')['id'] == 'Foo')
+        self.failUnless(
+            zcuf._verifyUser(plugins, login='foobar')['id'] == 'Foo')
+
+    def test__verifyUser_login_transform_upper( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+        zcuf.login_transform = 'upper'
+
+        enumerator = DummyMultiUserEnumerator(
+            'enumerator',
+            {'id': 'Foo', 'login': 'foobar'},
+            {'id': 'Bar', 'login': 'BAR'})
+        directlyProvides( enumerator, IUserEnumerationPlugin )
+        zcuf._setObject( 'enumerator', enumerator )
+
+        plugins = zcuf._getOb( 'plugins' )
+
+        plugins.activatePlugin( IUserEnumerationPlugin, 'enumerator' )
+
+        # No matter what we try as login parameter, it is always upper
+        # cased before verifying a user.
+        self.failUnless(zcuf._verifyUser(plugins, login='BAR')['id'] == 'Bar')
+        self.failUnless(zcuf._verifyUser(plugins, login='Bar')['id'] == 'Bar')
+        self.failUnless(zcuf._verifyUser(plugins, login='bar')['id'] == 'Bar')
+        self.failIf(zcuf._verifyUser(plugins, login='FOOBAR'))
+        self.failIf(zcuf._verifyUser(plugins, login='Foobar'))
+        self.failIf(zcuf._verifyUser(plugins, login='foobar'))
+
     def test__findUser_no_plugins( self ):
 
         plugins = self._makePlugins()
@@ -1294,6 +1464,42 @@
 
         self.failUnless( faux_user.__class__ is FauxUser )
 
+    def test__findUser_with_userfactory_plugin_and_transform( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+            import IUserFactoryPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+        zcuf.login_transform = 'lower'
+
+        bar = DummyPlugin()
+        directlyProvides( bar, IUserFactoryPlugin )
+
+        def _makeUser( user_id, name ):
+            user = FauxUser( user_id )
+            user._name = name
+            return user
+
+        bar.createUser = _makeUser
+
+        zcuf._setObject( 'bar', bar )
+
+        plugins = zcuf._getOb( 'plugins' )
+
+        real_user = zcuf._findUser( plugins, 'Mixed', 'Case' )
+        self.failIf( real_user.__class__ is FauxUser )
+
+        plugins.activatePlugin( IUserFactoryPlugin , 'bar' )
+
+        faux_user = zcuf._findUser( plugins, 'Mixed', 'Case' )
+
+        self.assertEqual( faux_user.getId(), 'Mixed' )
+        # This is lower case:
+        self.assertEqual( faux_user.getUserName(), 'case' )
+
+        self.failUnless( faux_user.__class__ is FauxUser )
+
     def test__findUser_with_plugins( self ):
 
         from Products.PluggableAuthService.interfaces.plugins \
@@ -1613,6 +1819,35 @@
         self.assertEqual( user2.getId(), 'bar/bar')
         self.assertEqual( user2.getUserName(), 'bar at example.com' )
 
+    def test_getUser_login_transform( self ):
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+        zcuf.login_transform = 'lower'
+
+        # The login_transform is applied in PAS, so we need to lower
+        # case the login ourselves in this test when passing it to a
+        # plugin.
+        bar = self._makeUserEnumerator( 'bar', 'bar at example.com' )
+        bar.identifier = 'bar/'
+        zcuf._setObject( 'bar', bar )
+
+        zcuf.plugins.activatePlugin(IUserEnumerationPlugin, 'bar')
+        # Fetch the new user by ID and name, and check we get the same.
+        user = zcuf.getUserById('bar/bar')
+        self.assertEqual( user.getId(), 'bar/bar')
+        self.assertEqual( user.getUserName(), 'bar at example.com' )
+
+        user2 = zcuf.getUser('bar at example.com')
+        self.assertEqual( user2.getId(), 'bar/bar')
+        self.assertEqual( user2.getUserName(), 'bar at example.com' )
+
+        user3 = zcuf.getUser('Bar at Example.Com')
+        self.assertEqual( user3.getId(), 'bar/bar')
+        self.assertEqual( user3.getUserName(), 'bar at example.com' )
+
     def test_simple_getUserGroups_with_Groupplugin(self):
 
         from Products.PluggableAuthService.interfaces.plugins \
@@ -1784,6 +2019,60 @@
         validated = wrapped.validate( request )
         self.assertEqual( validated.getUserName(), 'olivier' )
 
+    def test_validate_simple_authenticated_transform( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+            import IExtractionPlugin, \
+                   IAuthenticationPlugin, \
+                   IUserEnumerationPlugin, \
+                   IRolesPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+
+        login = DummyPlugin()
+        directlyProvides( login, IExtractionPlugin, IAuthenticationPlugin )
+        login.extractCredentials = _extractLogin
+        login.authenticateCredentials = _authLogin
+        zcuf._setObject( 'login', login )
+        # Lower case all logins.
+        zcuf.login_transform = 'lower'
+
+        olivier = DummyPlugin()
+        directlyProvides( olivier, IUserEnumerationPlugin, IRolesPlugin )
+        olivier.enumerateUsers = lambda id: id == 'foo' or None
+        olivier.getRolesForPrincipal = lambda user, req: (
+                     user.getId() == 'olivier' and ( 'Hamlet', ) or () )
+
+        zcuf._setObject( 'olivier', olivier )
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IExtractionPlugin, 'login' )
+        plugins.activatePlugin( IAuthenticationPlugin, 'login' )
+        plugins.activatePlugin( IUserEnumerationPlugin, 'olivier' )
+        plugins.activatePlugin( IRolesPlugin, 'olivier' )
+
+        rc, root, folder, object = self._makeTree()
+
+        index = FauxObject( 'index_html' )
+        index.__roles__ = ( 'Hamlet', )
+        acquired_index = index.__of__( root ).__of__( object )
+
+        request = self._makeRequest( ( 'folder', 'object', 'index_html' )
+                                   , RESPONSE=FauxResponse()
+                                   , PARENTS=[ object, folder, root ]
+                                   , PUBLISHED=acquired_index.__of__( object )
+                                   , form={ 'login' : 'OLIVIER'
+                                          , 'password' : 'arras'
+                                          }
+                                   )
+
+
+        wrapped = zcuf.__of__( root )
+
+        validated = wrapped.validate( request )
+        self.assertEqual( validated.getUserName(), 'olivier' )
+
     def test_validate_with_anonymous_factory( self ):
 
         from Products.PluggableAuthService.interfaces.plugins \
@@ -1976,6 +2265,89 @@
         self.failIf( plugins.listPlugins( IAuthenticationPlugin ) )
         self.failIf( plugins.listPlugins( IUserEnumerationPlugin ) )
 
+    def test_searchUsers( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+
+        foo = self._makeUserEnumerator( 'foo' )
+        zcuf._setObject( 'foo', foo )
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IUserEnumerationPlugin, 'foo' )
+
+        # Search by id
+        self.failIf( zcuf.searchUsers( id='zope' ) )
+        self.failUnless( zcuf.searchUsers( id='foo' ) )
+        self.failUnless( len( zcuf.searchUsers( id='foo' )) == 1 )
+
+        # Search by login name
+        self.failIf( zcuf.searchUsers( name='zope' ) )
+        self.failUnless( zcuf.searchUsers( name='foo' ) )
+        self.failUnless( len( zcuf.searchUsers( name='foo' )) == 1 )
+
+        # Login name can be a sequence
+        self.failIf( zcuf.searchUsers( name=['zope'] ) )
+        self.failUnless( zcuf.searchUsers( name=['foo'] ) )
+        self.failUnless( len( zcuf.searchUsers( name=['foo'] )) == 1 )
+        self.failIf( zcuf.searchUsers( name=('zope', ) ) )
+        self.failUnless( zcuf.searchUsers( name=('foo', ) ) )
+        self.failUnless( len( zcuf.searchUsers( name=('foo', ) )) == 1 )
+        self.failUnless( zcuf.searchUsers( name=('foo', 'bar' ) ) )
+        self.failUnless( len( zcuf.searchUsers( name=('foo', 'bar') )) == 1 )
+
+    def test_searchUsers_transform( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+        zcuf.login_transform = 'lower'
+
+        # user id upper case, login name lower case
+        foo = self._makeUserEnumerator( 'FOO', 'foo' )
+        zcuf._setObject( 'foo', foo )
+        bar = self._makeUserEnumerator( 'BAR', 'bar' )
+        zcuf._setObject( 'bar', bar )
+
+        plugins = zcuf._getOb( 'plugins' )
+        # Note: we only activate 'foo' for now.
+        plugins.activatePlugin( IUserEnumerationPlugin, 'foo' )
+
+        # Search by id
+        self.failIf( zcuf.searchUsers( id='ZOPE' ) )
+        self.failUnless( zcuf.searchUsers( id='FOO' ) )
+        self.failUnless( len( zcuf.searchUsers( id='FOO' )) == 1 )
+
+        # Search by login name
+        self.failIf( zcuf.searchUsers( name='Zope' ) )
+        self.failUnless( zcuf.searchUsers( name='Foo' ) )
+        self.failUnless( len( zcuf.searchUsers( name='Foo' )) == 1 )
+
+        # Login name can be a sequence
+        self.failIf( zcuf.searchUsers( name=['Zope'] ) )
+        self.failUnless( zcuf.searchUsers( name=['Foo'] ) )
+        self.failUnless( len( zcuf.searchUsers( name=['Foo'] )) == 1 )
+        self.failIf( zcuf.searchUsers( name=('Zope', ) ) )
+        self.failUnless( zcuf.searchUsers( name=('Foo', ) ) )
+        self.failUnless( len( zcuf.searchUsers( name=('Foo', ) )) == 1 )
+
+        # Search for more ids or names.
+        self.failUnless( zcuf.searchUsers( id=['FOO', 'BAR', 'ZOPE'] ) )
+        self.failUnless( len( zcuf.searchUsers( id=['FOO', 'BAR', 'ZOPE'] )) == 1 )
+        self.failUnless( zcuf.searchUsers( name=('Foo', 'Bar' , 'Zope' ) ) )
+        self.failUnless( len( zcuf.searchUsers( name=('Foo', 'Bar', 'Zope') )) == 1 )
+
+        # Activate the bar plugin and try again.
+        plugins.activatePlugin( IUserEnumerationPlugin, 'bar' )
+        self.failUnless( len( zcuf.searchUsers( id=['FOO', 'BAR', 'ZOPE'] )) == 2 )
+        self.failUnless( len( zcuf.searchUsers( name=('Foo', 'Bar', 'Zope') )) == 2 )
+
+
     def test_searchGroups( self ):
 
         from Products.PluggableAuthService.interfaces.plugins \
@@ -2040,7 +2412,170 @@
         self.failUnless(
             len( zcuf.searchPrincipals(id='group')) == 1 )
 
+    def test_searchPrincipals_transform( self ):
 
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IGroupEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+        zcuf.login_transform = 'lower'
+
+        foo = self._makeUserEnumerator( 'foo' )
+        zcuf._setObject( 'foo', foo )
+        foobar = self._makeGroupEnumerator( 'Foobar' )
+        zcuf._setObject( 'foobar', foobar )
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IUserEnumerationPlugin, 'foo' )
+        plugins.activatePlugin( IGroupEnumerationPlugin, 'foobar' )
+
+        self.failIf( zcuf.searchPrincipals( name='zope' ) )
+        # Note that groups are never found by name, only by id.
+        self.failUnless( len( zcuf.searchPrincipals( name='foo' ) ) == 1 )
+        user1 = zcuf.searchPrincipals( name='foo' )[0]
+        self.assertEqual(user1['principal_type'], 'user')
+        self.assertEqual(user1['id'], 'foo')
+        self.assertEqual(user1['login'], 'foo')
+
+        # Search for mixed case.
+        self.failUnless( len( zcuf.searchPrincipals( name='Foo' ) ) == 1 )
+        user2 = zcuf.searchPrincipals( name='Foo' )[0]
+        self.assertEqual(user1, user2)
+
+        # Search for upper case.
+        self.failUnless( len( zcuf.searchPrincipals( name='FOO' ) ) == 1 )
+        user3 = zcuf.searchPrincipals( name='FOO' )[0]
+        self.assertEqual(user1, user3)
+
+    def test_updateLoginName( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+
+        foo = self._makeMultiUserEnumerator(
+            {'id': 'JOE', 'login': 'Joe'},
+            {'id': 'BART', 'login': 'Bart'})
+        zcuf._setObject( 'foo', foo )
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IUserEnumerationPlugin, 'foo' )
+
+        users = zcuf.searchUsers(login='Joe')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'JOE')
+        self.assertEqual(users[0]['login'], 'Joe')
+
+        # Change the login name.
+        zcuf.updateLoginName('JOE', 'James')
+        users = zcuf.searchUsers(login='James')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'JOE')
+        self.assertEqual(users[0]['login'], 'James')
+
+        # Try lowercase
+        zcuf.login_transform = 'lower'
+        zcuf.updateLoginName('JOE', 'James')
+
+        users = zcuf.searchUsers(login='James')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'JOE')
+        self.assertEqual(users[0]['login'], 'james')
+
+        users = zcuf.searchUsers(login='james')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'JOE')
+        self.assertEqual(users[0]['login'], 'james')
+
+    def test_updateOwnLoginName( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+
+        foo = self._makeMultiUserEnumerator(
+            {'id': 'bart', 'login': 'bart'},
+            {'id': 'joe', 'login': 'joe'})
+        zcuf._setObject( 'foo', foo )
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IUserEnumerationPlugin, 'foo' )
+
+        users = zcuf.searchUsers(login='joe')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'joe')
+        self.assertEqual(users[0]['login'], 'joe')
+        users = zcuf.searchUsers(login='bart')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'bart')
+        self.assertEqual(users[0]['login'], 'bart')
+
+        # Changing the login name will not work when you are not logged in.
+        zcuf.updateOwnLoginName('james')
+        users = zcuf.searchUsers(login='james')
+        self.assertEqual(len(users), 0)
+
+        # Fake a login.
+        newSecurityManager(None, FauxUser('joe', 'joe'))
+        zcuf.updateOwnLoginName('james')
+
+        users = zcuf.searchUsers(login='james')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'joe')
+        self.assertEqual(users[0]['login'], 'james')
+
+        # The login for bart has not changed.
+        users = zcuf.searchUsers(login='bart')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'bart')
+        self.assertEqual(users[0]['login'], 'bart')
+
+    def test_updateAllLoginNames( self ):
+
+        from Products.PluggableAuthService.interfaces.plugins \
+             import IUserEnumerationPlugin
+
+        plugins = self._makePlugins()
+        zcuf = self._makeOne( plugins )
+
+        foo = self._makeMultiUserEnumerator(
+            {'id': 'JOE', 'login': 'Joe'},
+            {'id': 'BART', 'login': 'Bart'})
+        zcuf._setObject( 'foo', foo )
+
+        plugins = zcuf._getOb( 'plugins' )
+        plugins.activatePlugin( IUserEnumerationPlugin, 'foo' )
+
+        users = foo.enumerateUsers(login='Joe')
+        self.assertEqual(len(users), 1)
+        self.assertEqual(users[0]['id'], 'JOE')
+        self.assertEqual(users[0]['login'], 'Joe')
+        users = foo.enumerateUsers(login='Bart')
+        self.assertEqual(len(users), 1)
+
+        # Update all login names.  The dummy updater makes every login
+        # name lowercase.
+        zcuf.updateAllLoginNames()
+
+        self.assertEqual(len(foo.enumerateUsers(login='Joe')), 0)
+        self.assertEqual(len(foo.enumerateUsers(login='joe')), 1)
+        self.assertEqual(len(foo.enumerateUsers(login='Bart')), 0)
+        self.assertEqual(len(foo.enumerateUsers(login='bart')), 1)
+
+        # PAS applies the login_transform when searching for users.
+        zcuf.login_transform = 'lower'
+        self.assertEqual(len(zcuf.searchUsers(login='Joe')), 1)
+        self.assertEqual(len(zcuf.searchUsers(login='joe')), 1)
+        self.assertEqual(len(zcuf.searchUsers(login='Bart')), 1)
+        self.assertEqual(len(zcuf.searchUsers(login='bart')), 1)
+
     def test_no_challenger(self):
         # make sure that the response's _unauthorized gets propogated
         # if no challengers exist (or have fired)
@@ -2228,6 +2763,54 @@
         extracted = creds_store.extractCredentials(request)
         self.failUnless(len(extracted.keys()) == 0)
 
+    def test_applyTransform( self ):
+        zcuf = self._makeOne()
+        self.assertEqual(zcuf.applyTransform(' User '), ' User ')
+        zcuf.login_transform =  'lower'
+        self.assertEqual(zcuf.applyTransform(' User '), 'user')
+        self.assertEqual(zcuf.applyTransform(u' User '), u'user')
+        self.assertEqual(zcuf.applyTransform(''), '')
+        self.assertEqual(zcuf.applyTransform(None), None)
+        self.assertEqual(zcuf.applyTransform([' User ']), ['user'])
+        self.assertEqual(zcuf.applyTransform(('User', ' joe  ', 'Diana')),
+                         ('user', 'joe', 'diana'))
+        self.assertRaises(TypeError, zcuf.applyTransform, 123)
+        zcuf.login_transform =  'upper'
+        self.assertEqual(zcuf.applyTransform(' User '), 'USER')
+        # Let's not fail just because a user has accidentally left a
+        # space at the end of the login_transform name.  That could
+        # lead to hard-to-debug behaviour.
+        zcuf.login_transform =  ' upper  '
+        self.assertEqual(zcuf.applyTransform(' User '), 'USER')
+        # We would want to test what happens when the login_transform
+        # attribute is not there, but the following only removes it
+        # from the instance, not the class.  Oh well.
+        del zcuf.login_transform
+        self.assertEqual(zcuf.applyTransform(' User '), ' User ')
+        zcuf.login_transform =  'nonexisting'
+        self.assertEqual(zcuf.applyTransform(' User '), ' User ')
+
+    def test_get_login_transform_method( self ):
+        zcuf = self._makeOne()
+        self.assertEqual(zcuf._get_login_transform_method(), None)
+        zcuf.login_transform =  'lower'
+        self.assertEqual(zcuf._get_login_transform_method(), zcuf.lower)
+        zcuf.login_transform =  'upper'
+        self.assertEqual(zcuf._get_login_transform_method(), zcuf.upper)
+        # Let's not fail just because a user has accidentally left a
+        # space at the end of the login_transform name.  That could
+        # lead to hard-to-debug behaviour.
+        zcuf.login_transform =  ' upper  '
+        self.assertEqual(zcuf._get_login_transform_method(), zcuf.upper)
+        # We would want to test what happens when the login_transform
+        # attribute is not there, but the following only removes it
+        # from the instance, not the class.  Oh well.
+        del zcuf.login_transform
+        self.assertEqual(zcuf._get_login_transform_method(), None)
+        zcuf.login_transform =  'nonexisting'
+        self.assertEqual(zcuf._get_login_transform_method(), None)
+
+
 if __name__ == "__main__":
     unittest.main()
 

Modified: Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_exportimport.py
===================================================================
--- Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_exportimport.py	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/Products/PluggableAuthService/tests/test_exportimport.py	2013-01-22 10:36:26 UTC (rev 129077)
@@ -128,9 +128,11 @@
             self.assertEqual(filename, 'PAS/.properties')
             self.assertEqual(content_type, 'text/plain')
             lines = filter(None, [x.strip() for x in text.splitlines()])
-            self.assertEqual(len(lines), 2)
+            lines = sorted(lines)
+            self.assertEqual(len(lines), 3)
             self.assertEqual(lines[0], '[DEFAULT]')
-            self.assertEqual(lines[1], 'title =')
+            self.assertEqual(lines[1], 'login_transform =')
+            self.assertEqual(lines[2], 'title =')
 
             filename, text, content_type = context._wrote[2]
             self.assertEqual(filename, 'PAS/pluginregistry.xml')
@@ -173,9 +175,11 @@
             self.assertEqual(filename, 'PAS/.properties')
             self.assertEqual(content_type, 'text/plain')
             lines = filter(None, [x.strip() for x in text.splitlines()])
-            self.assertEqual(len(lines), 2)
+            lines = sorted(lines)
+            self.assertEqual(len(lines), 3)
             self.assertEqual(lines[0], '[DEFAULT]')
-            self.assertEqual(lines[1], 'title =')
+            self.assertEqual(lines[1], 'login_transform =')
+            self.assertEqual(lines[2], 'title =')
 
             filename, text, content_type = context._wrote[2]
             self.assertEqual(filename, 'PAS/pluginregistry.xml')

Modified: Products.PluggableAuthService/trunk/buildout.cfg
===================================================================
--- Products.PluggableAuthService/trunk/buildout.cfg	2013-01-22 10:34:39 UTC (rev 129076)
+++ Products.PluggableAuthService/trunk/buildout.cfg	2013-01-22 10:36:26 UTC (rev 129077)
@@ -7,6 +7,8 @@
     http://pypi.python.org/packages/source/P/Products.GenericSetup/
     http://pypi.python.org/packages/source/P/Products.PluginRegistry/
     http://pypi.python.org/packages/source/f/five.localsitemanager/
+extends =
+    http://download.zope.org/Zope2/index/2.13.19/versions.cfg
 
 [interpreter]
 recipe = zc.recipe.egg



More information about the checkins mailing list