Package: collective.emaillogin4
IntroductionSince Plone 4.0 you can configure Plone to allow users to login with
their email address, using a setting in the Security control panel.
This works fine out of the box.  Some improvements would be useful
though that need some more careful consideration before being added to
core Plone.  That is where this package comes in.This is a temporary package with some fixes for when you want to use
the email address of a user as login name in Plone 4.  It also
introduces a few hooks for determining the user id and login name of a
new user.Plone versionThis package is tested with Plone 4.1, 4.2 and 4.3.  It will not work
in 4.0.For Plone 3, you must use thecollective.emailloginpackage.DependenciesWe need a newer version ofProducts.PluggableAuthServicethan is
available currently in the latest Plone versions.  Assuming you are
using buildout for your Plone site, you need to add a line to a
versions section:Products.PluggableAuthService = 1.10.0Any version newer than this is fine as well.  If your Plone version
already has this version or a newer one pinned, then you do not need
to add this line.What does this package do?Clearer separation between user id and login nameThe validation of theregisterbrowser view uses two methods to
get a user id and login name:# Generate a nice user id and store that in the data.
user_id = self.generate_user_id(data)
# Generate a nice login name and store that in the data.
login_name = self.generate_login_name(data)After this, thedatadictionary will have keysuser_idandlogin_nameset accordingly.We avoid as much as possible the use ofusernameas a variable,
because no one ever knows if that is meant as a user id or as a login
name.  In standard Plone this is always the same, but this need not be
true, especially when using the email address as login name.These changes are intended to be merged toplone.app.users.Control over user idsAnIUserIdGeneratorinterface is defined.  This is used in the newgenerate_user_idmethod of theregisterbrowser view (also
used when adding a new user as admin).  Two sample implementations:def uuid_userid_generator(data=None):
    # Return a uuid, independent of the data.
    # This is available in utils.py in the plone.app.users patches.
    from zope.component import getUtility
    from plone.uuid.interfaces import IUUIDGenerator
    generator = getUtility(IUUIDGenerator)
    return generator()

def login_name_as_userid_generator(data):
    # We like to keep it simple.
    return data.get('username')Ingenerate_user_idwe try a few options for coming up with a good
user id:We query a utility, so integrators can register a hook to
generate a user id using their own logic:generator = queryUtility(IUserIdGenerator)
if generator:
    userid = generator(data)
    if userid:
        data['user_id'] = userid
        return useridIfuse_uuid_as_useridis set in the site_properties, we
generate a uuid.  This is a new property introduced by this
package and can be set in the Security control panel.If a username is given and we do not use email as login,
then we simply return that username as the user id.We create a user id based on the full name, if that is
passed.  This may result in an id likebob-jones-2.When the email address is used as login name, we originally
used the email address as user id as well.  This has a few
possible downsides, which are the main reasons for the new,
pluggable approach:It does not work for some valid email addresses.Exposing the email address in this way may not be wanted.When the user later changes his email address, the user id
will still be his old address.  It works, but may be
confusing.Another possibility would be to simply generate a uuid, but that is
ugly.  We could certainly try that though: the big plus here would be
that you then cannot create a new user with the same user id as a
previously existing user if this ever gets removed.  If you would get
the same id, this new user would get the same global and local roles,
if those have not been cleaned up.When a user id is chosen, theuser_idkey of the data gets
set and the user id is returned.These changes are intended to be merged toplone.app.users.Control over login namesSimilarly, anILoginNameGeneratorinterface is defined.Usually the login name and user id are the same, but this is
not necessarily true.  When using the email address as login
name, we may have a different user id, generated by calling
the generate_user_id method.We try a few options for coming up with a good login name:We query a utility, so integrators can register a hook to
generate a login name using their own logic:pas = getToolByName(self.context, 'acl_users')
generator = queryUtility(ILoginNameGenerator)
if generator:
    login_name = generator(data)
    if login_name:
        login_name = pas.applyTransform(login_name)
        data['login_name'] = login_name
      return login_nameIf a username is given and we do not use email as login,
then we simply return that username as the login name.When using email as login, we use the email address.In all cases, we call PAS.applyTransform on the login name, if
that is defined.  This is a recent addition to PAS, currently
under development.When a login name is chosen, thelogin_namekey of the data gets
set and the login name is returned.These changes are intended to be merged toplone.app.users.Lowercase login namesWe store login names as lowercase.  The email addresses themselves can
actually be mixed case, though that is not really by design, more a
(happy) circumstance.This needs branchmaurits-login-transformofProducts.PluggableAuthService.  That branch introduces a propertylogin_transform.  Setting this tolowerthelowermethod
of PAS is called whenever a login name is given.All relevant places inplone.app.usershave been changed to take
this new property into account, using code like this:login_name = pas.loginTransform(login_name)In the security panel ofplone.app.controlpanelwe change theset_use_email_as_loginmethod to setlogin_transformto lower
case when switching on email as login name.  For safety, we never
change this back to the default empty string.  This is fine for normal
non-email login names as well.Note that whenlogin_transformislower, the end user can
login with upper caseJOEand he will then be logged in with login
namejoe, as long as the password is correct of course.  If you
somehow still have an upper or mixed case login name, you cannot
login.Setting the login_transform to a non empty string will
automatically apply this transform to all existing logins in your
database.Note: when this is merged to core Plone, login names will not be
transformed to lowercase by default.  The option will simply be
available if the site admin wants it.  Switching on email as login
will also switch on lowercase login names.Updating login namesWe have a patch for theZODBMutablePropertyProviderofProducts.PlonePASthat adds two new but empty methods required by
the changedIUserEnumerationPlugininterface of PAS:def updateUser(self, user_id, login_name):
   pass


def updateEveryLoginName(self, quit_on_first_error=True):
   passThis has been merged toProducts.PlonePAS.Control panelsSwitching email as login name on or off in the security panel now
automatically updates existing login names.  It may fail when there
are duplicates.The updating of existing users used to be done in the@@migrate-to-emailloginview (classEmailView) fromplone.app.controlpanel.  We have simplified this page to only
search for duplicate login names.  You can search for duplicate email
addresses or duplicate user ids, always lower case.The security panel now has an optionUse UUID user ids, by default
switched off.Set own login nameTheProducts.CMFPlone.utils.set_own_login_namemethod is
simplified, with much of the former code being moved to PAS
itself:def set_own_login_name(member, loginname):
    """Allow the user to set his/her own login name.

    If you have the Manage Users permission, you can update the login
    name of another member too, though the name of this function is a
    bit weird then.  Historical accident.
    """
    pas = getToolByName(member, 'acl_users')
    mt = getToolByName(member, 'portal_membership')
    if member.getId() == mt.getAuthenticatedMember().getId():
        pas.updateOwnLoginName(loginname)
        return
    secman = getSecurityManager()
    if not secman.checkPermission(ManageUsers, member):
        raise Unauthorized('You can only change your OWN login name.')
    pas.updateLoginName(member.getId(), loginname)InstallationWhen installing this add-on in the Add-ons control panel, the
following is done.It adds theuse_uuid_as_useridsite property, by default False.If email as login is already used in the site, we setlogin_transformtolower.  This could give an error and quit
the installation.  Maybe we want to catch this and just log a
warning.It explicitly enables email as login name.  This wouldnotbe done
when merging this package back to core Plone.Changelog1.3 (2013-02-19)Depend onProducts.PluggableAuthService >= 1.10.0which has
been released recently.
[maurits]1.2 (2013-01-23)Make sure user ids are strings, not unicode.
[maurits]1.1 (2013-01-23)Make sure the Manage Users permission is checked in set_own_login_name.
[maurits]1.0 (2013-01-22)Initial release