Repository: Products.CMFPlone
Branch: refs/heads/master
Date: 2014-11-16T15:15:51+01:00
Author: Jure Cerjak (jcerjak) <jcerjak@termitnjak.si>
Commit: https://github.com/plone/Products.CMFPlone/commit/7b6f30fa5930ab8bce6564321d6c01252e46cf6c

Revert "Revert "Plip10359 - move security controlpanel from plone.app.controlpanel""

This reverts commit f6f4456542ea91f97ad053270fb1b37fbb20583f.

Files changed:
A Products/CMFPlone/controlpanel/bbb/security.py
A Products/CMFPlone/controlpanel/browser/emaillogin.pt
A Products/CMFPlone/controlpanel/browser/security.py
A Products/CMFPlone/controlpanel/events.zcml
A Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
A Products/CMFPlone/controlpanel/utils.py
A Products/CMFPlone/tests/robot/test_controlpanel_security.robot
M Products/CMFPlone/controlpanel/bbb/configure.zcml
M Products/CMFPlone/controlpanel/browser/configure.zcml
M Products/CMFPlone/controlpanel/configure.zcml
M Products/CMFPlone/controlpanel/events.py
M Products/CMFPlone/interfaces/__init__.py
M Products/CMFPlone/interfaces/controlpanel.py
M Products/CMFPlone/profiles/dependencies/registry.xml
M Products/CMFPlone/testing.py

diff --git a/Products/CMFPlone/controlpanel/bbb/configure.zcml b/Products/CMFPlone/controlpanel/bbb/configure.zcml
index dc8d0ba..c3af923 100644
--- a/Products/CMFPlone/controlpanel/bbb/configure.zcml
+++ b/Products/CMFPlone/controlpanel/bbb/configure.zcml
@@ -7,6 +7,7 @@
   <adapter factory=".maintenance.MaintenanceControlPanelAdapter" />
   <adapter factory=".navigation.NavigationControlPanelAdapter" />
   <adapter factory=".search.SearchControlPanelAdapter" />
+  <adapter factory=".security.SecurityControlPanelAdapter" />
   <adapter factory=".site.SiteControlPanelAdapter" />
   <adapter factory=".markup.MarkupControlPanelAdapter" />
 
diff --git a/Products/CMFPlone/controlpanel/bbb/security.py b/Products/CMFPlone/controlpanel/bbb/security.py
new file mode 100644
index 0000000..2d05160
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/bbb/security.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.registry.interfaces import IRegistry
+from zope.component import adapts
+from zope.component import getUtility
+from zope.interface import implements
+from zope.site.hooks import getSite
+
+
+class SecurityControlPanelAdapter(object):
+
+    adapts(IPloneSiteRoot)
+    implements(ISecuritySchema)
+
+    def __init__(self, context):
+        self.portal = getSite()
+        self.pmembership = getToolByName(context, 'portal_membership')
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+
+    def get_enable_self_reg(self):
+        return self.settings.enable_self_reg
+
+    def set_enable_self_reg(self, value):
+        # additional processing in the event handler
+        self.settings.enable_self_reg = value
+
+    enable_self_reg = property(get_enable_self_reg, set_enable_self_reg)
+
+    def get_enable_user_pwd_choice(self):
+        return self.settings.enable_user_pwd_choice
+
+    def set_enable_user_pwd_choice(self, value):
+        self.settings.enable_user_pwd_choice = value
+
+    enable_user_pwd_choice = property(get_enable_user_pwd_choice,
+                                      set_enable_user_pwd_choice)
+
+    def get_enable_user_folders(self):
+        return self.settings.enable_user_folders
+
+    def set_enable_user_folders(self, value):
+        # additional processing in the event handler
+        self.settings.enable_user_folders = value
+
+    enable_user_folders = property(get_enable_user_folders,
+                                   set_enable_user_folders)
+
+    def get_allow_anon_views_about(self):
+        return self.settings.allow_anon_views_about
+
+    def set_allow_anon_views_about(self, value):
+        self.settings.allow_anon_views_about = value
+
+    allow_anon_views_about = property(get_allow_anon_views_about,
+                                      set_allow_anon_views_about)
+
+    def get_use_email_as_login(self):
+        return self.settings.use_email_as_login
+
+    def set_use_email_as_login(self, value):
+        # additional processing in the event handler
+        self.settings.use_email_as_login = value
+
+    use_email_as_login = property(get_use_email_as_login,
+                                  set_use_email_as_login)
+
+    def get_use_uuid_as_userid(self):
+        return self.settings.use_uuid_as_userid
+
+    def set_use_uuid_as_userid(self, value):
+        self.settings.use_uuid_as_userid = value
+
+    use_uuid_as_userid = property(get_use_uuid_as_userid,
+                                  set_use_uuid_as_userid)
diff --git a/Products/CMFPlone/controlpanel/browser/configure.zcml b/Products/CMFPlone/controlpanel/browser/configure.zcml
index 1e3a13e..9510e55 100644
--- a/Products/CMFPlone/controlpanel/browser/configure.zcml
+++ b/Products/CMFPlone/controlpanel/browser/configure.zcml
@@ -57,6 +57,23 @@
     permission="plone.app.controlpanel.Search"
     />
 
+  <!-- Security Control Panel -->
+  <browser:page
+    name="security-controlpanel"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    class=".security.SecurityControlPanel"
+    permission="plone.app.controlpanel.Security"
+    />
+
+  <!-- Security Control Panel - EMail Login -->
+  <browser:page
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    name="migrate-to-emaillogin"
+    class=".security.EmailLogin"
+    template="emaillogin.pt"
+    permission="cmf.ManagePortal"
+    />
+
   <!-- Site Control Panel -->
   <browser:page
     name="site-controlpanel"
diff --git a/Products/CMFPlone/controlpanel/browser/emaillogin.pt b/Products/CMFPlone/controlpanel/browser/emaillogin.pt
new file mode 100644
index 0000000..16c3949
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/emaillogin.pt
@@ -0,0 +1,67 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/prefs_main_template/macros/master"
+      i18n:domain="plone">
+
+<body>
+
+  <metal:main metal:fill-slot="prefs_configlet_content">
+    <h1 class="documentFirstHeading"
+        i18n:translate="heading_find_duplicate_login_names">
+      Find duplicate login names
+    </h1>
+
+    <p i18n:translate="help_duplicate_login_names">
+      Switching the email login setting in the
+      <a i18n:name="link"
+         tal:attributes="href string:${context/portal_url}/@@security-controlpanel"
+         i18n:translate="">Security settings</a>
+      on or off automatically changes the login name for existing users.
+      This may fail when there are duplicates.
+      On this page you can search for duplicates.
+    </p>
+
+    <div tal:condition="request/form/submitted|nothing">
+      <div tal:condition="view/duplicates">
+        <p i18n:translate="msg_login_duplicates_found">
+          The following login names would be used by more than one account:
+        </p>
+        <ul>
+          <ol tal:repeat="dup view/duplicates">
+            <span tal:content="python:dup[0]" />:
+            <span tal:repeat="account python:dup[1]" tal:content="account" />
+          </ol>
+        </ul>
+      </div>
+      <div tal:condition="not:view/duplicates">
+        <p i18n:translate="msg_no_login_duplicates_found">
+          No login names found that are used by more than one account.
+        </p>
+      </div>
+    </div>
+
+    <form action=""
+          name="emaillogin-migrate"
+          method="post"
+          class="enableUnloadProtection enableAutoFocus">
+      <div class="formControls">
+        <input type="hidden" name="submitted" value="submitted" id="submitted" />
+        <input class="context"
+               type="submit"
+               name="check_email"
+               value="Check for duplicate emails"
+               i18n:attributes="value label_check_duplicate_emails" />
+        <br />
+        <input class="context"
+               type="submit"
+               name="check_userid"
+               value="Check for duplicate lower case user ids"
+               i18n:attributes="value label_check_duplicate_user_ids" />
+      </div>
+    </form>
+
+  </metal:main>
+</body>
+</html>
diff --git a/Products/CMFPlone/controlpanel/browser/security.py b/Products/CMFPlone/controlpanel/browser/security.py
new file mode 100644
index 0000000..1026f27
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/security.py
@@ -0,0 +1,127 @@
+from Acquisition import aq_inner
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone.controlpanel.utils import migrate_to_email_login
+from Products.CMFPlone.controlpanel.utils import migrate_from_email_login
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.Five.browser import BrowserView
+from collections import defaultdict
+from plone.app.registry.browser import controlpanel
+
+import logging
+
+logger = logging.getLogger('Products.CMFPlone')
+
+
+class SecurityControlPanelForm(controlpanel.RegistryEditForm):
+
+    id = "SecurityControlPanel"
+    label = _(u"Security settings")
+    schema = ISecuritySchema
+    schema_prefix = "plone"
+
+
+class SecurityControlPanel(controlpanel.ControlPanelFormWrapper):
+    form = SecurityControlPanelForm
+
+
+class EmailLogin(BrowserView):
+    """View to help in migrating to or from using email as login.
+
+    We used to change the login name of existing users here, but that
+    is now done by checking or unchecking the option in the security
+    control panel.  Here you can only search for duplicates.
+    """
+
+    duplicates = []
+
+    def __call__(self):
+        if self.request.form.get('check_email'):
+            self.duplicates = self.check_email()
+        elif self.request.form.get('check_userid'):
+            self.duplicates = self.check_userid()
+        return self.index()
+
+    @property
+    def _email_list(self):
+        context = aq_inner(self.context)
+        pas = getToolByName(context, 'acl_users')
+        emails = defaultdict(list)
+        orig_transform = pas.login_transform
+        try:
+            if not orig_transform:
+                # Temporarily set this to lower, as that will happen
+                # when turning emaillogin on.
+                pas.login_transform = 'lower'
+            for user in pas.getUsers():
+                if user is None:
+                    # Created in the ZMI?
+                    continue
+                email = user.getProperty('email', '')
+                if email:
+                    email = pas.applyTransform(email)
+                else:
+                    logger.warn("User %s has no email address.",
+                                user.getUserId())
+                    # Add the normal login name anyway.
+                    email = pas.applyTransform(user.getUserName())
+                emails[email].append(user.getUserId())
+        finally:
+            pas.login_transform = orig_transform
+            return emails
+
+    def check_email(self):
+        duplicates = []
+        for email, userids in self._email_list.items():
+            if len(userids) > 1:
+                logger.warn("Duplicate accounts for email address %s: %r",
+                            email, userids)
+                duplicates.append((email, userids))
+
+        return duplicates
+
+    @property
+    def _userid_list(self):
+        # user ids are unique, but their lowercase version might not
+        # be unique.
+        context = aq_inner(self.context)
+        pas = getToolByName(context, 'acl_users')
+        userids = defaultdict(list)
+        orig_transform = pas.login_transform
+        try:
+            if not orig_transform:
+                # Temporarily set this to lower, as that will happen
+                # when turning emaillogin on.
+                pas.login_transform = 'lower'
+            for user in pas.getUsers():
+                if user is None:
+                    continue
+                login_name = pas.applyTransform(user.getUserName())
+                userids[login_name].append(user.getUserId())
+        finally:
+            pas.login_transform = orig_transform
+            return userids
+
+    def check_userid(self):
+        duplicates = []
+        for login_name, userids in self._userid_list.items():
+            if len(userids) > 1:
+                logger.warn("Duplicate accounts for lower case user id "
+                            "%s: %r", login_name, userids)
+                duplicates.append((login_name, userids))
+
+        return duplicates
+
+    def switch_to_email(self):
+        # This is not used and is only here for backwards
+        # compatibility.  It avoids a test failure in
+        # Products.CMFPlone.
+        # XXX: check if this can be removed
+        migrate_to_email_login(self.context)
+
+    def switch_to_userid(self):
+        # This is not used and is only here for backwards
+        # compatibility.  It avoids a test failure in
+        # Products.CMFPlone.
+        # XXX: check if this can be removed
+        migrate_from_email_login(self.context)
diff --git a/Products/CMFPlone/controlpanel/configure.zcml b/Products/CMFPlone/controlpanel/configure.zcml
index 1b49eda..93d5b49 100644
--- a/Products/CMFPlone/controlpanel/configure.zcml
+++ b/Products/CMFPlone/controlpanel/configure.zcml
@@ -7,4 +7,6 @@
   <include package=".bbb" />
   <include package=".browser" />
 
+  <include file="events.zcml" />
+
 </configure>
diff --git a/Products/CMFPlone/controlpanel/events.py b/Products/CMFPlone/controlpanel/events.py
index 14a7ee0..4e5a96e 100644
--- a/Products/CMFPlone/controlpanel/events.py
+++ b/Products/CMFPlone/controlpanel/events.py
@@ -1,8 +1,17 @@
+from Products.CMFCore.ActionInformation import Action
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone.controlpanel.utils import migrate_to_email_login
+from Products.CMFPlone.controlpanel.utils import migrate_from_email_login
 from Products.CMFPlone.interfaces import IConfigurationChangedEvent
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.utils import safe_hasattr
+from plone.registry.interfaces import IRecordModifiedEvent
 from zope.component import adapter
 from zope.component import queryUtility
 from zope.interface import implements
 from zope.ramcache.interfaces.ram import IRAMCache
+from zope.site.hooks import getSite
 
 
 class ConfigurationChangedEvent(object):
@@ -18,3 +27,102 @@ def handleConfigurationChangedEvent(event):
     util = queryUtility(IRAMCache)
     if util is not None:
         util.invalidateAll()
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_enable_self_reg(obj, event):
+    """Additional configuration when the ``enable_self_reg``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, the ``Add portal member`` permission is
+    added to ``Anonymous`` role to allow self registration for anonymous
+    users. If the setting is disabled, this permission is removed.
+    """
+    if event.record.fieldName != 'enable_self_reg':
+        return
+
+    portal = getSite()
+    value = event.newValue
+    app_perms = portal.rolesOfPermission(
+        permission='Add portal member')
+    reg_roles = []
+
+    for app_perm in app_perms:
+        if app_perm['selected'] == 'SELECTED':
+            reg_roles.append(app_perm['name'])
+    if value is True and 'Anonymous' not in reg_roles:
+        reg_roles.append('Anonymous')
+    if value is False and 'Anonymous' in reg_roles:
+        reg_roles.remove('Anonymous')
+
+    portal.manage_permission('Add portal member', roles=reg_roles,
+                             acquire=0)
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_enable_user_folders(obj, event):
+    """Additional configuration when the ``enable_user_folders``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, a new user action is added with a link to
+    the personal folder. If the setting is disabled, the action is hidden.
+    """
+    if event.record.fieldName != 'enable_user_folders':
+        return
+
+    portal = getSite()
+    value = event.newValue
+
+    membership = getToolByName(portal, 'portal_membership')
+    membership.memberareaCreationFlag = value
+
+    # support the 'my folder' user action #8417
+    portal_actions = getToolByName(portal, 'portal_actions', None)
+    if portal_actions is not None:
+        object_category = getattr(portal_actions, 'user', None)
+        if value and not safe_hasattr(object_category, 'mystuff'):
+            # add action
+            _add_mystuff_action(object_category)
+        elif safe_hasattr(object_category, 'mystuff'):
+            a = getattr(object_category, 'mystuff')
+            a.visible = bool(value)    # show/hide action
+
+
+def _add_mystuff_action(object_category):
+    new_action = Action(
+        'mystuff',
+        title=_(u'My Folder'),
+        description='',
+        url_expr='string:${portal/portal_membership/getHomeUrl}',
+        available_expr='python:(member is not None) and \
+            (portal.portal_membership.getHomeFolder() is not None) ',
+        permissions=('View',),
+        visible=True,
+        i18n_domain='plone'
+    )
+    object_category._setObject('mystuff', new_action)
+    # move action to top, at least before the logout action
+    object_category.moveObjectsToTop(('mystuff'))
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_use_email_as_login(obj, event):
+    """Additional configuration when the ``use_email_as_login``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, existing users' login names are migrated
+    to email. If the setting is disabled, then the login names are migrated
+    back to user ids.
+    """
+    if event.record.fieldName != 'use_email_as_login':
+        return
+
+    value = event.newValue
+    if value == event.oldValue:
+        # no change
+        return
+    context = getSite()
+    if value:
+        migrate_to_email_login(context)
+    else:
+        migrate_from_email_login(context)
diff --git a/Products/CMFPlone/controlpanel/events.zcml b/Products/CMFPlone/controlpanel/events.zcml
new file mode 100644
index 0000000..665959b
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/events.zcml
@@ -0,0 +1,5 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".events.handle_enable_self_reg" />
+  <subscriber handler=".events.handle_enable_user_folders" />
+  <subscriber handler=".events.handle_use_email_as_login" />
+</configure>
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
new file mode 100644
index 0000000..c2cb8e9
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
@@ -0,0 +1,130 @@
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from zope.component import getAdapter
+
+import unittest
+
+
+class SecurityControlPanelAdapterTest(unittest.TestCase):
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.security_settings = getAdapter(self.portal, ISecuritySchema)
+
+    def test_adapter_lookup(self):
+        self.assertTrue(getAdapter(self.portal, ISecuritySchema))
+
+    def test_get_enable_self_reg_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            False
+        )
+
+    def test_set_enable_self_reg_setting(self):
+        self.security_settings.enable_self_reg = False
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            False
+        )
+        self.security_settings.enable_self_reg = True
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            True
+        )
+
+    def test_get_enable_user_pwd_choice_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            False
+        )
+
+    def test_set_enable_user_pwd_choice_setting(self):
+        self.security_settings.enable_user_pwd_choice = False
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            False
+        )
+        self.security_settings.enable_user_pwd_choice = True
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            True
+        )
+
+    def test_get_enable_user_folders_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            False
+        )
+
+    def test_set_enable_user_folders_setting(self):
+        self.security_settings.enable_user_folders = False
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            False
+        )
+        self.security_settings.enable_user_folders = True
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            True
+        )
+
+    def test_get_allow_anon_views_about_setting(self):
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            False
+        )
+
+    def test_set_allow_anon_views_about_setting(self):
+        self.security_settings.allow_anon_views_about = False
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            False
+        )
+        self.security_settings.allow_anon_views_about = True
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            True
+        )
+
+    def test_get_use_email_as_login_setting(self):
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            False
+        )
+
+    def test_set_use_email_as_login_setting(self):
+        self.security_settings.use_email_as_login = False
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            False
+        )
+        self.security_settings.use_email_as_login = True
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            True
+        )
+
+    def test_get_use_uuid_as_userid_setting(self):
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            False
+        )
+
+    def test_set_use_uuid_as_userid_setting(self):
+        self.security_settings.use_uuid_as_userid = False
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            False
+        )
+        self.security_settings.use_uuid_as_userid = True
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            True
+        )
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
new file mode 100644
index 0000000..098cd47
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
+from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD
+from plone.registry.interfaces import IRegistry
+from plone.testing.z2 import Browser
+from zope.component import getUtility
+
+import unittest2 as unittest
+
+
+class SecurityControlPanelFunctionalTest(unittest.TestCase):
+    """Test that changes in the security control panel are actually
+    stored in the registry.
+    """
+
+    layer = PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        self.app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.portal_url = self.portal.absolute_url()
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+        self.browser = Browser(self.app)
+        self.browser.handleErrors = False
+        self.browser.addHeader(
+            'Authorization',
+            'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
+        )
+
+    def test_security_control_panel_link(self):
+        self.browser.open(
+            "%s/plone_control_panel" % self.portal_url)
+        self.browser.getLink('Security').click()
+
+    def test_security_control_panel_backlink(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.assertTrue("Plone Configuration" in self.browser.contents)
+
+    def test_security_control_panel_sidebar(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getLink('Site Setup').click()
+        self.assertEqual(
+            self.browser.url,
+            'http://nohost/plone/@@overview-controlpanel')
+
+    def test_enable_self_reg(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl('Enable self-registration').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_self_reg, True)
+
+    def test_enable_user_pwd_choice(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            'Let users select their own passwords').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_user_pwd_choice, True)
+
+    def test_enable_user_folders(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            'Enable User Folders').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_user_folders, True)
+
+    def test_allow_anon_views_about(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Allow anyone to view 'about' information").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.allow_anon_views_about, True)
+
+    def test_use_email_as_login(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Use email address as login name").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.use_email_as_login, True)
+
+    def test_use_uuid_as_userid(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Use UUID user ids").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.use_uuid_as_userid, True)
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
new file mode 100644
index 0000000..b5ff729
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
@@ -0,0 +1,141 @@
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from zope.component import getAdapter
+
+import unittest
+
+
+class SecurityControlPanelEventsTest(unittest.TestCase):
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.security_settings = getAdapter(self.portal, ISecuritySchema)
+
+    def _create_user(self, user_id=None, email=None):
+        """Helper function for creating a test user."""
+        registration = getToolByName(self.portal, 'portal_registration', None)
+        registration.addMember(
+            user_id,
+            'password',
+            ['Member'],
+            properties={'email': email, 'username': user_id}
+        )
+        membership = getToolByName(self.portal, 'portal_membership', None)
+        return membership.getMemberById(user_id)
+
+    def _is_self_reg_enabled(self):
+        """Helper function to determine if self registration was properly
+        enabled.
+        """
+        app_perms = self.portal.rolesOfPermission(
+            permission='Add portal member')
+        for app_perm in app_perms:
+            if app_perm['name'] == 'Anonymous' \
+               and app_perm['selected'] == 'SELECTED':
+                return True
+        return False
+
+    def test_handle_enable_self_reg_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        self.assertFalse(self._is_self_reg_enabled())
+        self.security_settings.use_uuid_as_userid = True
+        self.assertFalse(self._is_self_reg_enabled())
+
+    def test_handle_enable_self_reg_disabled(self):
+        self.security_settings.enable_self_reg = False
+        self.assertFalse(self._is_self_reg_enabled())
+
+    def test_handle_enable_self_reg_enabled(self):
+        self.security_settings.enable_self_reg = True
+        self.assertTrue(self._is_self_reg_enabled())
+
+    def test_handle_enable_user_folders_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+        self.security_settings.use_uuid_as_userid = True
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+
+    def test_handle_enable_user_folders_enabled_no_mystuff_yet(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if we enable the setting, mystuff action should be added
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+        self.security_settings.enable_user_folders = True
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertTrue(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_enable_user_folders_enabled_has_mystuff(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if we enable the setting, disable it, then enable it again,
+        # the mystuff action should still be there and visible
+        self.security_settings.enable_user_folders = True
+        self.security_settings.enable_user_folders = False
+        self.security_settings.enable_user_folders = True
+
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertTrue(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_enable_user_folders_disabled_no_mystuff_yet(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if the mystuff action is not there yet, this should have no effect
+        self.security_settings.enable_user_folders = False
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+
+    def test_handle_enable_user_folders_disabled_has_mystuff(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if the setting was enabled and then disabled, the mystuff action
+        # should be hidden
+        self.security_settings.enable_user_folders = True
+        self.security_settings.enable_user_folders = False
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertFalse(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_use_email_as_login_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.security_settings.use_uuid_as_userid = True
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+
+    def test_handle_use_email_as_login_enabled(self):
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.assertEquals(len(pas.searchUsers(name='joe')), 1)
+
+        # if we enable use_email_as_login, login name should be migrated
+        # to email
+        self.security_settings.use_email_as_login = True
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 1)
+
+    def test_handle_use_email_as_login_disabled(self):
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        # if we enable use_email_as_login, then disabled it, the login name
+        # should be migrated back to user id
+        self.security_settings.use_email_as_login = True
+        self.security_settings.use_email_as_login = False
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.assertEquals(len(pas.searchUsers(name='joe')), 1)
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
new file mode 100644
index 0000000..3b29843
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from plone.app.testing import TEST_USER_ID, setRoles
+from plone.registry.interfaces import IRegistry
+from zope.component import getMultiAdapter
+from zope.component import getUtility
+
+import unittest2 as unittest
+
+
+class SecurityRegistryIntegrationTest(unittest.TestCase):
+    """Test that the security settings are stored as plone.app.registry
+    settings.
+    """
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+
+    def test_security_controlpanel_view(self):
+        view = getMultiAdapter((self.portal, self.portal.REQUEST),
+                               name="security-controlpanel")
+        view = view.__of__(self.portal)
+        self.assertTrue(view())
+
+    def test_plone_app_registry_in_controlpanel(self):
+        self.controlpanel = getToolByName(self.portal, "portal_controlpanel")
+        self.assertTrue('plone.app.registry' in [a.getAction(self)['id']
+                            for a in self.controlpanel.listActions()])
+
+    def test_enable_self_reg_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_self_reg'))
+
+    def test_enable_user_pwd_choice_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_user_pwd_choice'))
+
+    def test_enable_user_folders_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_user_folders'))
+
+    def test_allow_anon_views_about_setting(self):
+        self.assertTrue(hasattr(self.settings, 'allow_anon_views_about'))
+
+    def test_use_email_as_login_setting(self):
+        self.assertTrue(hasattr(self.settings, 'use_email_as_login'))
+
+    def test_use_uuid_as_userid_setting(self):
+        self.assertTrue(hasattr(self.settings, 'use_uuid_as_userid'))
diff --git a/Products/CMFPlone/controlpanel/utils.py b/Products/CMFPlone/controlpanel/utils.py
new file mode 100644
index 0000000..851ca34
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/utils.py
@@ -0,0 +1,52 @@
+from Products.CMFCore.utils import getToolByName
+
+import logging
+
+
+logger = logging.getLogger('Products.CMFPlone.controlpanel')
+
+
+def migrate_to_email_login(context):
+    pas = getToolByName(context, 'acl_users')
+
+    # We want the login name to be lowercase here.  This is new in
+    # PAS.  Using 'manage_changeProperties' would change the login
+    # names immediately, but we want to do that explicitly ourselves
+    # and set the lowercase email address as login name, instead of
+    # the lower case user id.
+    #pas.manage_changeProperties(login_transform='lower')
+    pas.login_transform = 'lower'
+
+    # Update the users.
+    for user in pas.getUsers():
+        if user is None:
+            continue
+        user_id = user.getUserId()
+        email = user.getProperty('email', '')
+        if email:
+            login_name = pas.applyTransform(email)
+            pas.updateLoginName(user_id, login_name)
+        else:
+            logger.warn("User %s has no email address.", user_id)
+
+
+def migrate_from_email_login(context):
+    pas = getToolByName(context, 'acl_users')
+
+    # Whether the login name is lowercase or not does not really
+    # matter for this use case, but it may be better not to change
+    # it at this point.
+
+    # XXX
+    pas.login_transform = ''
+
+    # We do want to update the users.
+    for user in pas.getUsers():
+        if user is None:
+            continue
+        user_id = user.getUserId()
+        # If we keep the transform to lowercase, then we must apply it
+        # here as well, otherwise some users will not be able to
+        # login, as their user id may be mixed or upper case.
+        login_name = pas.applyTransform(user_id)
+        pas.updateLoginName(user_id, login_name)
diff --git a/Products/CMFPlone/interfaces/__init__.py b/Products/CMFPlone/interfaces/__init__.py
index 35b0069..cd5fcc2 100644
--- a/Products/CMFPlone/interfaces/__init__.py
+++ b/Products/CMFPlone/interfaces/__init__.py
@@ -12,6 +12,7 @@
 from controlpanel import IMarkupSchema
 from controlpanel import INavigationSchema
 from controlpanel import ISearchSchema
+from controlpanel import ISecuritySchema
 from controlpanel import ISiteSchema
 from controlpanel import ITinyMCELayoutSchema
 from controlpanel import ITinyMCELibrariesSchema
diff --git a/Products/CMFPlone/interfaces/controlpanel.py b/Products/CMFPlone/interfaces/controlpanel.py
index 01a0816..1a0a7db 100644
--- a/Products/CMFPlone/interfaces/controlpanel.py
+++ b/Products/CMFPlone/interfaces/controlpanel.py
@@ -793,6 +793,70 @@ class ISearchSchema(Interface):
     )
 
 
+class ISecuritySchema(Interface):
+
+    enable_self_reg = schema.Bool(
+        title=_(u'Enable self-registration'),
+        description=_(
+            u"Allows users to register themselves on the site. If "
+            u"not selected, only site managers can add new users."),
+        default=False,
+        required=False)
+
+    enable_user_pwd_choice = schema.Bool(
+        title=_(u'Let users select their own passwords'),
+        description=_(
+            u"If not selected, a URL will be generated and "
+            u"e-mailed. Users are instructed to follow the link to "
+            u"reach a page where they can change their password and "
+            u"complete the registration process; this also verifies "
+            u"that they have entered a valid email address."),
+        default=False,
+        required=False)
+
+    enable_user_folders = schema.Bool(
+        title=_(u'Enable User Folders'),
+        description=_(
+            u"If selected, home folders where users can create "
+            u"content will be created when they log in."),
+        default=False,
+        required=False)
+
+    allow_anon_views_about = schema.Bool(
+        title=_(u"Allow anyone to view 'about' information"),
+        description=_(
+            u"If not selected only logged-in users will be able to "
+            u"view information about who created an item and when it "
+            u"was modified."),
+        default=False,
+        required=False)
+
+    use_email_as_login = schema.Bool(
+        title=_(u'Use email address as login name'),
+        description=_(
+            u"Allows users to login with their email address instead "
+            u"of specifying a separate login name. This also updates "
+            u"the login name of existing users, which may take a "
+            u"while on large sites. The login name is saved as "
+            u"lower case, but to be userfriendly it does not matter "
+            u"which case you use to login. When duplicates are found, "
+            u"saving this form will fail. You can use the "
+            u"@@migrate-to-emaillogin page to show the duplicates."),
+        default=False,
+        required=False)
+
+    use_uuid_as_userid = schema.Bool(
+        title=_(u'Use UUID user ids'),
+        description=_(
+            u"Use automatically generated UUIDs as user id for new users. "
+            u"When not turned on, the default is to use the same as the "
+            u"login name, or when using the email address as login name we "
+            u"generate a user id based on the fullname."),
+        default=False,
+        required=False)
+
+
+# XXX: Why does ISiteSchema inherit from ILockSettings here ???
 class ISiteSchema(ILockSettings):
 
     site_title = schema.TextLine(
diff --git a/Products/CMFPlone/profiles/dependencies/registry.xml b/Products/CMFPlone/profiles/dependencies/registry.xml
index bd110f3..279a9f0 100644
--- a/Products/CMFPlone/profiles/dependencies/registry.xml
+++ b/Products/CMFPlone/profiles/dependencies/registry.xml
@@ -8,6 +8,8 @@
            prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.ISearchSchema"
            prefix="plone" />
+  <records interface="Products.CMFPlone.interfaces.ISecuritySchema"
+           prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.ISiteSchema"
            prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.IDateAndTimeSchema"
diff --git a/Products/CMFPlone/testing.py b/Products/CMFPlone/testing.py
index c301ff6..8b909a9 100644
--- a/Products/CMFPlone/testing.py
+++ b/Products/CMFPlone/testing.py
@@ -42,6 +42,15 @@ def setUpPloneSite(self, portal):
             id="test-folder",
             title=u"Test Folder"
         )
+        # XXX: this is needed for tests that rely on the Members folder to be
+        # present. This folder is otherwise created by a setup handler in
+        # ATContentTypes, but that package is optional now.
+        if 'Members' not in portal.keys():
+            portal.invokeFactory(
+                "Folder",
+                id="Members",
+                title=u"Members"
+            )
 
     def tearDownPloneSite(self, portal):
         login(portal, 'admin')
diff --git a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
new file mode 100644
index 0000000..18e030d
--- /dev/null
+++ b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
@@ -0,0 +1,152 @@
+*** Settings ***
+
+Resource  plone/app/robotframework/keywords.robot
+Resource  plone/app/robotframework/saucelabs.robot
+
+Library  Remote  ${PLONE_URL}/RobotRemote
+
+Resource  common.robot
+
+Test Setup  Open SauceLabs test browser
+Test Teardown  Run keywords  Report test status  Close all browsers
+
+
+*** Test Cases ***************************************************************
+
+Scenario: Enable self registration in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+    and I save the settings
+  Given an anonymous user
+    and the front page
+   Then the registration link is shown in the page
+
+Scenario: Enable users to select their own passwords in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+    and I enable users to select their own passwords
+    and I save the settings
+  Given an anonymous user
+    and the registration form
+   Then the password field is shown in the page
+
+Scenario: Enable user folders in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+    and I enable users to select their own passwords
+    and I enable user folders
+    and I save the settings
+  Given an anonymous user
+   When I register to the site
+    and I login to the site
+   Then the user folder should be created
+
+Scenario: Enable use email as login in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+    and I enable users to select their own passwords
+    and I enable use email as login
+    and I save the settings
+  Given an anonymous user
+    and the registration form
+   Then the email field is shown in the page
+     and the username field is not shown in the page
+
+Scenario: Enable use uuid as uid in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+    and I enable users to select their own passwords
+    and I enable uuid as user id
+    and I save the settings
+  Given an anonymous user
+   When I register to the site
+    and I login to the site
+   Then uuid should be used for user id
+
+
+*** Keywords *****************************************************************
+
+# --- GIVEN ------------------------------------------------------------------
+
+a logged-in site administrator
+  Enable autologin as  Site Administrator
+
+an anonymous user
+  Disable autologin
+
+the security control panel
+  Go to  ${PLONE_URL}/@@security-controlpanel
+
+the registration form
+  Go to  ${PLONE_URL}/@@register
+
+the front page
+  Go to  ${PLONE_URL}
+
+# --- WHEN -------------------------------------------------------------------
+
+I enable self registration
+  Select Checkbox  form.widgets.enable_self_reg:list
+
+I enable users to select their own passwords
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+
+I enable use email as login
+  Select Checkbox  form.widgets.use_email_as_login:list
+
+I enable user folders
+  Select Checkbox  form.widgets.enable_user_folders:list
+
+I enable uuid as user id
+  Select Checkbox  form.widgets.use_uuid_as_userid:list
+
+I save the settings
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I register to the site
+  Go to  ${PLONE_URL}/@@register
+  Input Text  form.widgets.username  joe
+  Input Text  form.widgets.email  joe@test.com
+  Input Text  form.widgets.password  supersecret
+  Input Text  form.widgets.password_ctl  supersecret
+  Click Button  Register
+
+I login to the site
+  Go to  ${PLONE_URL}/login
+  Input Text  __ac_name  joe
+  Input Text  __ac_password  supersecret
+  Click Button  Log in
+  Wait until page contains  You are now logged in
+
+
+# --- THEN -------------------------------------------------------------------
+
+The registration link is shown in the page
+  Element Should Be Visible  xpath=//a[@id='personaltools-join']
+
+The password field is shown in the page
+  Element Should Be Visible  xpath=//input[@id='form-widgets-password']
+
+The email field is shown in the page
+  Element Should Be Visible  xpath=//input[@id='form-widgets-email']
+
+The username field is not shown in the page
+  Element Should Not Be Visible  xpath=//input[@id='form-widgets-username']
+
+The user folder should be created
+  Go to  ${PLONE_URL}/Members/joe
+  Element Should Contain  css=h1.documentFirstHeading  joe
+  Page should Not contain  This page does not seem to exist
+
+# XXX: Here we can't really test that this is a uuid, since it's random, so
+# we just check that user id is not equal to username or email
+uuid should be used for user id
+  ${userid}=  Get Text  user-name
+  Should Not Be Equal As Strings  ${userid}  joe
+  Should Not Be Equal As Strings  ${userid}  joe@test.com


Repository: Products.CMFPlone
Branch: refs/heads/master
Date: 2014-11-16T15:15:51+01:00
Author: Jure Cerjak (jcerjak) <jcerjak@termitnjak.si>
Commit: https://github.com/plone/Products.CMFPlone/commit/3af790714d723ebc086c94b887e496e718f5069c

make security control panel robot tests more readable

Files changed:
M Products/CMFPlone/tests/robot/test_controlpanel_security.robot

diff --git a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
index 18e030d..944964c 100644
--- a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
+++ b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
@@ -17,56 +17,31 @@ Scenario: Enable self registration in the Security Control Panel
   Given a logged-in site administrator
     and the security control panel
    When I enable self registration
-    and I save the settings
-  Given an anonymous user
-    and the front page
-   Then the registration link is shown in the page
+   Then anonymous users can register to the site
 
 Scenario: Enable users to select their own passwords in the Security Control Panel
   Given a logged-in site administrator
     and the security control panel
-   When I enable self registration
-    and I enable users to select their own passwords
-    and I save the settings
-  Given an anonymous user
-    and the registration form
-   Then the password field is shown in the page
+   When I enable users to select their own passwords
+   Then users can select their own passwords when registering
 
 Scenario: Enable user folders in the Security Control Panel
   Given a logged-in site administrator
     and the security control panel
-   When I enable self registration
-    and I enable users to select their own passwords
-    and I enable user folders
-    and I save the settings
-  Given an anonymous user
-   When I register to the site
-    and I login to the site
-   Then the user folder should be created
+   When I enable user folders
+   Then a user folder should be created when a user registers and logs in to the site
 
 Scenario: Enable use email as login in the Security Control Panel
   Given a logged-in site administrator
     and the security control panel
-   When I enable self registration
-    and I enable users to select their own passwords
-    and I enable use email as login
-    and I save the settings
-  Given an anonymous user
-    and the registration form
-   Then the email field is shown in the page
-     and the username field is not shown in the page
+   When I enable email to be used as a login name
+   Then users can use email as their login name
 
 Scenario: Enable use uuid as uid in the Security Control Panel
   Given a logged-in site administrator
     and the security control panel
-   When I enable self registration
-    and I enable users to select their own passwords
-    and I enable uuid as user id
-    and I save the settings
-  Given an anonymous user
-   When I register to the site
-    and I login to the site
-   Then uuid should be used for user id
+   When I enable UUID to be used as a user id
+   Then UUID should be used for the user id
 
 
 *** Keywords *****************************************************************
@@ -76,40 +51,68 @@ Scenario: Enable use uuid as uid in the Security Control Panel
 a logged-in site administrator
   Enable autologin as  Site Administrator
 
-an anonymous user
-  Disable autologin
-
 the security control panel
   Go to  ${PLONE_URL}/@@security-controlpanel
 
-the registration form
-  Go to  ${PLONE_URL}/@@register
-
-the front page
-  Go to  ${PLONE_URL}
 
 # --- WHEN -------------------------------------------------------------------
 
 I enable self registration
   Select Checkbox  form.widgets.enable_self_reg:list
+  Click Button  Save
+  Wait until page contains  Changes saved
 
 I enable users to select their own passwords
+  Select Checkbox  form.widgets.enable_self_reg:list
   Select Checkbox  form.widgets.enable_user_pwd_choice:list
-
-I enable use email as login
-  Select Checkbox  form.widgets.use_email_as_login:list
+  Click Button  Save
+  Wait until page contains  Changes saved
 
 I enable user folders
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
   Select Checkbox  form.widgets.enable_user_folders:list
+  Click Button  Save
+  Wait until page contains  Changes saved
 
-I enable uuid as user id
-  Select Checkbox  form.widgets.use_uuid_as_userid:list
+I enable email to be used as a login name
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.use_email_as_login:list
+  Click Button  Save
+  Wait until page contains  Changes saved
 
-I save the settings
+I enable UUID to be used as a user id
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.use_uuid_as_userid:list
   Click Button  Save
   Wait until page contains  Changes saved
 
-I register to the site
+
+# --- THEN -------------------------------------------------------------------
+
+Anonymous users can register to the site
+  Disable autologin
+  Go to  ${PLONE_URL}
+  Element Should Be Visible  xpath=//a[@id='personaltools-join']
+
+Users can select their own passwords when registering
+  Disable autologin
+  Go to  ${PLONE_URL}/@@register
+  Element Should Be Visible  xpath=//input[@id='form-widgets-password']
+
+Users can use email as their login name
+  Disable autologin
+  Go to  ${PLONE_URL}/@@register
+  Element Should Be Visible  xpath=//input[@id='form-widgets-email']
+  Element Should Not Be Visible  xpath=//input[@id='form-widgets-username']
+
+A user folder should be created when a user registers and logs in to the site
+
+  Disable autologin
+
+  # I register to the site
   Go to  ${PLONE_URL}/@@register
   Input Text  form.widgets.username  joe
   Input Text  form.widgets.email  joe@test.com
@@ -117,36 +120,39 @@ I register to the site
   Input Text  form.widgets.password_ctl  supersecret
   Click Button  Register
 
-I login to the site
+  # I login to the site
   Go to  ${PLONE_URL}/login
   Input Text  __ac_name  joe
   Input Text  __ac_password  supersecret
   Click Button  Log in
   Wait until page contains  You are now logged in
 
+  # The user folder should be created
+  Go to  ${PLONE_URL}/Members/joe
+  Element Should Contain  css=h1.documentFirstHeading  joe
+  Page should Not contain  This page does not seem to exist
 
-# --- THEN -------------------------------------------------------------------
-
-The registration link is shown in the page
-  Element Should Be Visible  xpath=//a[@id='personaltools-join']
-
-The password field is shown in the page
-  Element Should Be Visible  xpath=//input[@id='form-widgets-password']
+UUID should be used for the user id
 
-The email field is shown in the page
-  Element Should Be Visible  xpath=//input[@id='form-widgets-email']
+  Disable autologin
 
-The username field is not shown in the page
-  Element Should Not Be Visible  xpath=//input[@id='form-widgets-username']
+  # I register to the site
+  Go to  ${PLONE_URL}/@@register
+  Input Text  form.widgets.username  joe
+  Input Text  form.widgets.email  joe@test.com
+  Input Text  form.widgets.password  supersecret
+  Input Text  form.widgets.password_ctl  supersecret
+  Click Button  Register
 
-The user folder should be created
-  Go to  ${PLONE_URL}/Members/joe
-  Element Should Contain  css=h1.documentFirstHeading  joe
-  Page should Not contain  This page does not seem to exist
+  # I login to the site
+  Go to  ${PLONE_URL}/login
+  Input Text  __ac_name  joe
+  Input Text  __ac_password  supersecret
+  Click Button  Log in
+  Wait until page contains  You are now logged in
 
-# XXX: Here we can't really test that this is a uuid, since it's random, so
-# we just check that user id is not equal to username or email
-uuid should be used for user id
-  ${userid}=  Get Text  user-name
+  # XXX: Here we can't really test that this is a uuid, since it's random, so
+  # we just check that user id is not equal to username or email
+  ${userid}=  Get Text  xpath=//li[@id='portal-personaltools']//li[contains(@class, 'plone-toolbar-submenu-header')]//span
   Should Not Be Equal As Strings  ${userid}  joe
   Should Not Be Equal As Strings  ${userid}  joe@test.com


Repository: Products.CMFPlone
Branch: refs/heads/master
Date: 2014-12-14T11:59:30+01:00
Author: Timo Stollenwerk (tisto) <tisto@plone.org>
Commit: https://github.com/plone/Products.CMFPlone/commit/c3edbccf26c87ac635149f9b8c99dfb83dc160bb

Merge branch 'plip10359-security-controlpanel' of https://github.com/jcerjak/Products.CMFPlone into jcerjak-plip10359-security-controlpanel

Files changed:
A Products/CMFPlone/controlpanel/bbb/security.py
A Products/CMFPlone/controlpanel/browser/emaillogin.pt
A Products/CMFPlone/controlpanel/browser/security.py
A Products/CMFPlone/controlpanel/events.zcml
A Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
A Products/CMFPlone/controlpanel/utils.py
A Products/CMFPlone/tests/robot/test_controlpanel_security.robot
M Products/CMFPlone/controlpanel/bbb/configure.zcml
M Products/CMFPlone/controlpanel/browser/configure.zcml
M Products/CMFPlone/controlpanel/configure.zcml
M Products/CMFPlone/controlpanel/events.py
M Products/CMFPlone/interfaces/__init__.py
M Products/CMFPlone/interfaces/controlpanel.py
M Products/CMFPlone/profiles/dependencies/registry.xml
M Products/CMFPlone/testing.py

diff --git a/Products/CMFPlone/controlpanel/bbb/configure.zcml b/Products/CMFPlone/controlpanel/bbb/configure.zcml
index b8b9561..e4510d3 100644
--- a/Products/CMFPlone/controlpanel/bbb/configure.zcml
+++ b/Products/CMFPlone/controlpanel/bbb/configure.zcml
@@ -8,6 +8,7 @@
   <adapter factory=".mail.MailControlPanelAdapter" />
   <adapter factory=".navigation.NavigationControlPanelAdapter" />
   <adapter factory=".search.SearchControlPanelAdapter" />
+  <adapter factory=".security.SecurityControlPanelAdapter" />
   <adapter factory=".site.SiteControlPanelAdapter" />
   <adapter factory=".markup.MarkupControlPanelAdapter" />
 
diff --git a/Products/CMFPlone/controlpanel/bbb/security.py b/Products/CMFPlone/controlpanel/bbb/security.py
new file mode 100644
index 0000000..2d05160
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/bbb/security.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.registry.interfaces import IRegistry
+from zope.component import adapts
+from zope.component import getUtility
+from zope.interface import implements
+from zope.site.hooks import getSite
+
+
+class SecurityControlPanelAdapter(object):
+
+    adapts(IPloneSiteRoot)
+    implements(ISecuritySchema)
+
+    def __init__(self, context):
+        self.portal = getSite()
+        self.pmembership = getToolByName(context, 'portal_membership')
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+
+    def get_enable_self_reg(self):
+        return self.settings.enable_self_reg
+
+    def set_enable_self_reg(self, value):
+        # additional processing in the event handler
+        self.settings.enable_self_reg = value
+
+    enable_self_reg = property(get_enable_self_reg, set_enable_self_reg)
+
+    def get_enable_user_pwd_choice(self):
+        return self.settings.enable_user_pwd_choice
+
+    def set_enable_user_pwd_choice(self, value):
+        self.settings.enable_user_pwd_choice = value
+
+    enable_user_pwd_choice = property(get_enable_user_pwd_choice,
+                                      set_enable_user_pwd_choice)
+
+    def get_enable_user_folders(self):
+        return self.settings.enable_user_folders
+
+    def set_enable_user_folders(self, value):
+        # additional processing in the event handler
+        self.settings.enable_user_folders = value
+
+    enable_user_folders = property(get_enable_user_folders,
+                                   set_enable_user_folders)
+
+    def get_allow_anon_views_about(self):
+        return self.settings.allow_anon_views_about
+
+    def set_allow_anon_views_about(self, value):
+        self.settings.allow_anon_views_about = value
+
+    allow_anon_views_about = property(get_allow_anon_views_about,
+                                      set_allow_anon_views_about)
+
+    def get_use_email_as_login(self):
+        return self.settings.use_email_as_login
+
+    def set_use_email_as_login(self, value):
+        # additional processing in the event handler
+        self.settings.use_email_as_login = value
+
+    use_email_as_login = property(get_use_email_as_login,
+                                  set_use_email_as_login)
+
+    def get_use_uuid_as_userid(self):
+        return self.settings.use_uuid_as_userid
+
+    def set_use_uuid_as_userid(self, value):
+        self.settings.use_uuid_as_userid = value
+
+    use_uuid_as_userid = property(get_use_uuid_as_userid,
+                                  set_use_uuid_as_userid)
diff --git a/Products/CMFPlone/controlpanel/browser/configure.zcml b/Products/CMFPlone/controlpanel/browser/configure.zcml
index c5383ab..b90a6f2 100644
--- a/Products/CMFPlone/controlpanel/browser/configure.zcml
+++ b/Products/CMFPlone/controlpanel/browser/configure.zcml
@@ -65,6 +65,23 @@
     permission="plone.app.controlpanel.Search"
     />
 
+  <!-- Security Control Panel -->
+  <browser:page
+    name="security-controlpanel"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    class=".security.SecurityControlPanel"
+    permission="plone.app.controlpanel.Security"
+    />
+
+  <!-- Security Control Panel - EMail Login -->
+  <browser:page
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    name="migrate-to-emaillogin"
+    class=".security.EmailLogin"
+    template="emaillogin.pt"
+    permission="cmf.ManagePortal"
+    />
+
   <!-- Site Control Panel -->
   <browser:page
     name="site-controlpanel"
diff --git a/Products/CMFPlone/controlpanel/browser/emaillogin.pt b/Products/CMFPlone/controlpanel/browser/emaillogin.pt
new file mode 100644
index 0000000..16c3949
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/emaillogin.pt
@@ -0,0 +1,67 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/prefs_main_template/macros/master"
+      i18n:domain="plone">
+
+<body>
+
+  <metal:main metal:fill-slot="prefs_configlet_content">
+    <h1 class="documentFirstHeading"
+        i18n:translate="heading_find_duplicate_login_names">
+      Find duplicate login names
+    </h1>
+
+    <p i18n:translate="help_duplicate_login_names">
+      Switching the email login setting in the
+      <a i18n:name="link"
+         tal:attributes="href string:${context/portal_url}/@@security-controlpanel"
+         i18n:translate="">Security settings</a>
+      on or off automatically changes the login name for existing users.
+      This may fail when there are duplicates.
+      On this page you can search for duplicates.
+    </p>
+
+    <div tal:condition="request/form/submitted|nothing">
+      <div tal:condition="view/duplicates">
+        <p i18n:translate="msg_login_duplicates_found">
+          The following login names would be used by more than one account:
+        </p>
+        <ul>
+          <ol tal:repeat="dup view/duplicates">
+            <span tal:content="python:dup[0]" />:
+            <span tal:repeat="account python:dup[1]" tal:content="account" />
+          </ol>
+        </ul>
+      </div>
+      <div tal:condition="not:view/duplicates">
+        <p i18n:translate="msg_no_login_duplicates_found">
+          No login names found that are used by more than one account.
+        </p>
+      </div>
+    </div>
+
+    <form action=""
+          name="emaillogin-migrate"
+          method="post"
+          class="enableUnloadProtection enableAutoFocus">
+      <div class="formControls">
+        <input type="hidden" name="submitted" value="submitted" id="submitted" />
+        <input class="context"
+               type="submit"
+               name="check_email"
+               value="Check for duplicate emails"
+               i18n:attributes="value label_check_duplicate_emails" />
+        <br />
+        <input class="context"
+               type="submit"
+               name="check_userid"
+               value="Check for duplicate lower case user ids"
+               i18n:attributes="value label_check_duplicate_user_ids" />
+      </div>
+    </form>
+
+  </metal:main>
+</body>
+</html>
diff --git a/Products/CMFPlone/controlpanel/browser/security.py b/Products/CMFPlone/controlpanel/browser/security.py
new file mode 100644
index 0000000..1026f27
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/security.py
@@ -0,0 +1,127 @@
+from Acquisition import aq_inner
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone.controlpanel.utils import migrate_to_email_login
+from Products.CMFPlone.controlpanel.utils import migrate_from_email_login
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.Five.browser import BrowserView
+from collections import defaultdict
+from plone.app.registry.browser import controlpanel
+
+import logging
+
+logger = logging.getLogger('Products.CMFPlone')
+
+
+class SecurityControlPanelForm(controlpanel.RegistryEditForm):
+
+    id = "SecurityControlPanel"
+    label = _(u"Security settings")
+    schema = ISecuritySchema
+    schema_prefix = "plone"
+
+
+class SecurityControlPanel(controlpanel.ControlPanelFormWrapper):
+    form = SecurityControlPanelForm
+
+
+class EmailLogin(BrowserView):
+    """View to help in migrating to or from using email as login.
+
+    We used to change the login name of existing users here, but that
+    is now done by checking or unchecking the option in the security
+    control panel.  Here you can only search for duplicates.
+    """
+
+    duplicates = []
+
+    def __call__(self):
+        if self.request.form.get('check_email'):
+            self.duplicates = self.check_email()
+        elif self.request.form.get('check_userid'):
+            self.duplicates = self.check_userid()
+        return self.index()
+
+    @property
+    def _email_list(self):
+        context = aq_inner(self.context)
+        pas = getToolByName(context, 'acl_users')
+        emails = defaultdict(list)
+        orig_transform = pas.login_transform
+        try:
+            if not orig_transform:
+                # Temporarily set this to lower, as that will happen
+                # when turning emaillogin on.
+                pas.login_transform = 'lower'
+            for user in pas.getUsers():
+                if user is None:
+                    # Created in the ZMI?
+                    continue
+                email = user.getProperty('email', '')
+                if email:
+                    email = pas.applyTransform(email)
+                else:
+                    logger.warn("User %s has no email address.",
+                                user.getUserId())
+                    # Add the normal login name anyway.
+                    email = pas.applyTransform(user.getUserName())
+                emails[email].append(user.getUserId())
+        finally:
+            pas.login_transform = orig_transform
+            return emails
+
+    def check_email(self):
+        duplicates = []
+        for email, userids in self._email_list.items():
+            if len(userids) > 1:
+                logger.warn("Duplicate accounts for email address %s: %r",
+                            email, userids)
+                duplicates.append((email, userids))
+
+        return duplicates
+
+    @property
+    def _userid_list(self):
+        # user ids are unique, but their lowercase version might not
+        # be unique.
+        context = aq_inner(self.context)
+        pas = getToolByName(context, 'acl_users')
+        userids = defaultdict(list)
+        orig_transform = pas.login_transform
+        try:
+            if not orig_transform:
+                # Temporarily set this to lower, as that will happen
+                # when turning emaillogin on.
+                pas.login_transform = 'lower'
+            for user in pas.getUsers():
+                if user is None:
+                    continue
+                login_name = pas.applyTransform(user.getUserName())
+                userids[login_name].append(user.getUserId())
+        finally:
+            pas.login_transform = orig_transform
+            return userids
+
+    def check_userid(self):
+        duplicates = []
+        for login_name, userids in self._userid_list.items():
+            if len(userids) > 1:
+                logger.warn("Duplicate accounts for lower case user id "
+                            "%s: %r", login_name, userids)
+                duplicates.append((login_name, userids))
+
+        return duplicates
+
+    def switch_to_email(self):
+        # This is not used and is only here for backwards
+        # compatibility.  It avoids a test failure in
+        # Products.CMFPlone.
+        # XXX: check if this can be removed
+        migrate_to_email_login(self.context)
+
+    def switch_to_userid(self):
+        # This is not used and is only here for backwards
+        # compatibility.  It avoids a test failure in
+        # Products.CMFPlone.
+        # XXX: check if this can be removed
+        migrate_from_email_login(self.context)
diff --git a/Products/CMFPlone/controlpanel/configure.zcml b/Products/CMFPlone/controlpanel/configure.zcml
index 1b49eda..93d5b49 100644
--- a/Products/CMFPlone/controlpanel/configure.zcml
+++ b/Products/CMFPlone/controlpanel/configure.zcml
@@ -7,4 +7,6 @@
   <include package=".bbb" />
   <include package=".browser" />
 
+  <include file="events.zcml" />
+
 </configure>
diff --git a/Products/CMFPlone/controlpanel/events.py b/Products/CMFPlone/controlpanel/events.py
index 14a7ee0..4e5a96e 100644
--- a/Products/CMFPlone/controlpanel/events.py
+++ b/Products/CMFPlone/controlpanel/events.py
@@ -1,8 +1,17 @@
+from Products.CMFCore.ActionInformation import Action
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone.controlpanel.utils import migrate_to_email_login
+from Products.CMFPlone.controlpanel.utils import migrate_from_email_login
 from Products.CMFPlone.interfaces import IConfigurationChangedEvent
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.utils import safe_hasattr
+from plone.registry.interfaces import IRecordModifiedEvent
 from zope.component import adapter
 from zope.component import queryUtility
 from zope.interface import implements
 from zope.ramcache.interfaces.ram import IRAMCache
+from zope.site.hooks import getSite
 
 
 class ConfigurationChangedEvent(object):
@@ -18,3 +27,102 @@ def handleConfigurationChangedEvent(event):
     util = queryUtility(IRAMCache)
     if util is not None:
         util.invalidateAll()
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_enable_self_reg(obj, event):
+    """Additional configuration when the ``enable_self_reg``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, the ``Add portal member`` permission is
+    added to ``Anonymous`` role to allow self registration for anonymous
+    users. If the setting is disabled, this permission is removed.
+    """
+    if event.record.fieldName != 'enable_self_reg':
+        return
+
+    portal = getSite()
+    value = event.newValue
+    app_perms = portal.rolesOfPermission(
+        permission='Add portal member')
+    reg_roles = []
+
+    for app_perm in app_perms:
+        if app_perm['selected'] == 'SELECTED':
+            reg_roles.append(app_perm['name'])
+    if value is True and 'Anonymous' not in reg_roles:
+        reg_roles.append('Anonymous')
+    if value is False and 'Anonymous' in reg_roles:
+        reg_roles.remove('Anonymous')
+
+    portal.manage_permission('Add portal member', roles=reg_roles,
+                             acquire=0)
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_enable_user_folders(obj, event):
+    """Additional configuration when the ``enable_user_folders``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, a new user action is added with a link to
+    the personal folder. If the setting is disabled, the action is hidden.
+    """
+    if event.record.fieldName != 'enable_user_folders':
+        return
+
+    portal = getSite()
+    value = event.newValue
+
+    membership = getToolByName(portal, 'portal_membership')
+    membership.memberareaCreationFlag = value
+
+    # support the 'my folder' user action #8417
+    portal_actions = getToolByName(portal, 'portal_actions', None)
+    if portal_actions is not None:
+        object_category = getattr(portal_actions, 'user', None)
+        if value and not safe_hasattr(object_category, 'mystuff'):
+            # add action
+            _add_mystuff_action(object_category)
+        elif safe_hasattr(object_category, 'mystuff'):
+            a = getattr(object_category, 'mystuff')
+            a.visible = bool(value)    # show/hide action
+
+
+def _add_mystuff_action(object_category):
+    new_action = Action(
+        'mystuff',
+        title=_(u'My Folder'),
+        description='',
+        url_expr='string:${portal/portal_membership/getHomeUrl}',
+        available_expr='python:(member is not None) and \
+            (portal.portal_membership.getHomeFolder() is not None) ',
+        permissions=('View',),
+        visible=True,
+        i18n_domain='plone'
+    )
+    object_category._setObject('mystuff', new_action)
+    # move action to top, at least before the logout action
+    object_category.moveObjectsToTop(('mystuff'))
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_use_email_as_login(obj, event):
+    """Additional configuration when the ``use_email_as_login``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, existing users' login names are migrated
+    to email. If the setting is disabled, then the login names are migrated
+    back to user ids.
+    """
+    if event.record.fieldName != 'use_email_as_login':
+        return
+
+    value = event.newValue
+    if value == event.oldValue:
+        # no change
+        return
+    context = getSite()
+    if value:
+        migrate_to_email_login(context)
+    else:
+        migrate_from_email_login(context)
diff --git a/Products/CMFPlone/controlpanel/events.zcml b/Products/CMFPlone/controlpanel/events.zcml
new file mode 100644
index 0000000..665959b
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/events.zcml
@@ -0,0 +1,5 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".events.handle_enable_self_reg" />
+  <subscriber handler=".events.handle_enable_user_folders" />
+  <subscriber handler=".events.handle_use_email_as_login" />
+</configure>
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
new file mode 100644
index 0000000..c2cb8e9
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
@@ -0,0 +1,130 @@
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from zope.component import getAdapter
+
+import unittest
+
+
+class SecurityControlPanelAdapterTest(unittest.TestCase):
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.security_settings = getAdapter(self.portal, ISecuritySchema)
+
+    def test_adapter_lookup(self):
+        self.assertTrue(getAdapter(self.portal, ISecuritySchema))
+
+    def test_get_enable_self_reg_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            False
+        )
+
+    def test_set_enable_self_reg_setting(self):
+        self.security_settings.enable_self_reg = False
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            False
+        )
+        self.security_settings.enable_self_reg = True
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            True
+        )
+
+    def test_get_enable_user_pwd_choice_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            False
+        )
+
+    def test_set_enable_user_pwd_choice_setting(self):
+        self.security_settings.enable_user_pwd_choice = False
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            False
+        )
+        self.security_settings.enable_user_pwd_choice = True
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            True
+        )
+
+    def test_get_enable_user_folders_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            False
+        )
+
+    def test_set_enable_user_folders_setting(self):
+        self.security_settings.enable_user_folders = False
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            False
+        )
+        self.security_settings.enable_user_folders = True
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            True
+        )
+
+    def test_get_allow_anon_views_about_setting(self):
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            False
+        )
+
+    def test_set_allow_anon_views_about_setting(self):
+        self.security_settings.allow_anon_views_about = False
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            False
+        )
+        self.security_settings.allow_anon_views_about = True
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            True
+        )
+
+    def test_get_use_email_as_login_setting(self):
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            False
+        )
+
+    def test_set_use_email_as_login_setting(self):
+        self.security_settings.use_email_as_login = False
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            False
+        )
+        self.security_settings.use_email_as_login = True
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            True
+        )
+
+    def test_get_use_uuid_as_userid_setting(self):
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            False
+        )
+
+    def test_set_use_uuid_as_userid_setting(self):
+        self.security_settings.use_uuid_as_userid = False
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            False
+        )
+        self.security_settings.use_uuid_as_userid = True
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            True
+        )
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
new file mode 100644
index 0000000..098cd47
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
+from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD
+from plone.registry.interfaces import IRegistry
+from plone.testing.z2 import Browser
+from zope.component import getUtility
+
+import unittest2 as unittest
+
+
+class SecurityControlPanelFunctionalTest(unittest.TestCase):
+    """Test that changes in the security control panel are actually
+    stored in the registry.
+    """
+
+    layer = PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        self.app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.portal_url = self.portal.absolute_url()
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+        self.browser = Browser(self.app)
+        self.browser.handleErrors = False
+        self.browser.addHeader(
+            'Authorization',
+            'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
+        )
+
+    def test_security_control_panel_link(self):
+        self.browser.open(
+            "%s/plone_control_panel" % self.portal_url)
+        self.browser.getLink('Security').click()
+
+    def test_security_control_panel_backlink(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.assertTrue("Plone Configuration" in self.browser.contents)
+
+    def test_security_control_panel_sidebar(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getLink('Site Setup').click()
+        self.assertEqual(
+            self.browser.url,
+            'http://nohost/plone/@@overview-controlpanel')
+
+    def test_enable_self_reg(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl('Enable self-registration').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_self_reg, True)
+
+    def test_enable_user_pwd_choice(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            'Let users select their own passwords').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_user_pwd_choice, True)
+
+    def test_enable_user_folders(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            'Enable User Folders').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_user_folders, True)
+
+    def test_allow_anon_views_about(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Allow anyone to view 'about' information").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.allow_anon_views_about, True)
+
+    def test_use_email_as_login(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Use email address as login name").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.use_email_as_login, True)
+
+    def test_use_uuid_as_userid(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Use UUID user ids").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.use_uuid_as_userid, True)
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
new file mode 100644
index 0000000..b5ff729
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
@@ -0,0 +1,141 @@
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from zope.component import getAdapter
+
+import unittest
+
+
+class SecurityControlPanelEventsTest(unittest.TestCase):
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.security_settings = getAdapter(self.portal, ISecuritySchema)
+
+    def _create_user(self, user_id=None, email=None):
+        """Helper function for creating a test user."""
+        registration = getToolByName(self.portal, 'portal_registration', None)
+        registration.addMember(
+            user_id,
+            'password',
+            ['Member'],
+            properties={'email': email, 'username': user_id}
+        )
+        membership = getToolByName(self.portal, 'portal_membership', None)
+        return membership.getMemberById(user_id)
+
+    def _is_self_reg_enabled(self):
+        """Helper function to determine if self registration was properly
+        enabled.
+        """
+        app_perms = self.portal.rolesOfPermission(
+            permission='Add portal member')
+        for app_perm in app_perms:
+            if app_perm['name'] == 'Anonymous' \
+               and app_perm['selected'] == 'SELECTED':
+                return True
+        return False
+
+    def test_handle_enable_self_reg_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        self.assertFalse(self._is_self_reg_enabled())
+        self.security_settings.use_uuid_as_userid = True
+        self.assertFalse(self._is_self_reg_enabled())
+
+    def test_handle_enable_self_reg_disabled(self):
+        self.security_settings.enable_self_reg = False
+        self.assertFalse(self._is_self_reg_enabled())
+
+    def test_handle_enable_self_reg_enabled(self):
+        self.security_settings.enable_self_reg = True
+        self.assertTrue(self._is_self_reg_enabled())
+
+    def test_handle_enable_user_folders_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+        self.security_settings.use_uuid_as_userid = True
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+
+    def test_handle_enable_user_folders_enabled_no_mystuff_yet(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if we enable the setting, mystuff action should be added
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+        self.security_settings.enable_user_folders = True
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertTrue(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_enable_user_folders_enabled_has_mystuff(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if we enable the setting, disable it, then enable it again,
+        # the mystuff action should still be there and visible
+        self.security_settings.enable_user_folders = True
+        self.security_settings.enable_user_folders = False
+        self.security_settings.enable_user_folders = True
+
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertTrue(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_enable_user_folders_disabled_no_mystuff_yet(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if the mystuff action is not there yet, this should have no effect
+        self.security_settings.enable_user_folders = False
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+
+    def test_handle_enable_user_folders_disabled_has_mystuff(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if the setting was enabled and then disabled, the mystuff action
+        # should be hidden
+        self.security_settings.enable_user_folders = True
+        self.security_settings.enable_user_folders = False
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertFalse(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_use_email_as_login_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.security_settings.use_uuid_as_userid = True
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+
+    def test_handle_use_email_as_login_enabled(self):
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.assertEquals(len(pas.searchUsers(name='joe')), 1)
+
+        # if we enable use_email_as_login, login name should be migrated
+        # to email
+        self.security_settings.use_email_as_login = True
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 1)
+
+    def test_handle_use_email_as_login_disabled(self):
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        # if we enable use_email_as_login, then disabled it, the login name
+        # should be migrated back to user id
+        self.security_settings.use_email_as_login = True
+        self.security_settings.use_email_as_login = False
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.assertEquals(len(pas.searchUsers(name='joe')), 1)
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
new file mode 100644
index 0000000..3b29843
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from plone.app.testing import TEST_USER_ID, setRoles
+from plone.registry.interfaces import IRegistry
+from zope.component import getMultiAdapter
+from zope.component import getUtility
+
+import unittest2 as unittest
+
+
+class SecurityRegistryIntegrationTest(unittest.TestCase):
+    """Test that the security settings are stored as plone.app.registry
+    settings.
+    """
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+
+    def test_security_controlpanel_view(self):
+        view = getMultiAdapter((self.portal, self.portal.REQUEST),
+                               name="security-controlpanel")
+        view = view.__of__(self.portal)
+        self.assertTrue(view())
+
+    def test_plone_app_registry_in_controlpanel(self):
+        self.controlpanel = getToolByName(self.portal, "portal_controlpanel")
+        self.assertTrue('plone.app.registry' in [a.getAction(self)['id']
+                            for a in self.controlpanel.listActions()])
+
+    def test_enable_self_reg_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_self_reg'))
+
+    def test_enable_user_pwd_choice_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_user_pwd_choice'))
+
+    def test_enable_user_folders_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_user_folders'))
+
+    def test_allow_anon_views_about_setting(self):
+        self.assertTrue(hasattr(self.settings, 'allow_anon_views_about'))
+
+    def test_use_email_as_login_setting(self):
+        self.assertTrue(hasattr(self.settings, 'use_email_as_login'))
+
+    def test_use_uuid_as_userid_setting(self):
+        self.assertTrue(hasattr(self.settings, 'use_uuid_as_userid'))
diff --git a/Products/CMFPlone/controlpanel/utils.py b/Products/CMFPlone/controlpanel/utils.py
new file mode 100644
index 0000000..851ca34
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/utils.py
@@ -0,0 +1,52 @@
+from Products.CMFCore.utils import getToolByName
+
+import logging
+
+
+logger = logging.getLogger('Products.CMFPlone.controlpanel')
+
+
+def migrate_to_email_login(context):
+    pas = getToolByName(context, 'acl_users')
+
+    # We want the login name to be lowercase here.  This is new in
+    # PAS.  Using 'manage_changeProperties' would change the login
+    # names immediately, but we want to do that explicitly ourselves
+    # and set the lowercase email address as login name, instead of
+    # the lower case user id.
+    #pas.manage_changeProperties(login_transform='lower')
+    pas.login_transform = 'lower'
+
+    # Update the users.
+    for user in pas.getUsers():
+        if user is None:
+            continue
+        user_id = user.getUserId()
+        email = user.getProperty('email', '')
+        if email:
+            login_name = pas.applyTransform(email)
+            pas.updateLoginName(user_id, login_name)
+        else:
+            logger.warn("User %s has no email address.", user_id)
+
+
+def migrate_from_email_login(context):
+    pas = getToolByName(context, 'acl_users')
+
+    # Whether the login name is lowercase or not does not really
+    # matter for this use case, but it may be better not to change
+    # it at this point.
+
+    # XXX
+    pas.login_transform = ''
+
+    # We do want to update the users.
+    for user in pas.getUsers():
+        if user is None:
+            continue
+        user_id = user.getUserId()
+        # If we keep the transform to lowercase, then we must apply it
+        # here as well, otherwise some users will not be able to
+        # login, as their user id may be mixed or upper case.
+        login_name = pas.applyTransform(user_id)
+        pas.updateLoginName(user_id, login_name)
diff --git a/Products/CMFPlone/interfaces/__init__.py b/Products/CMFPlone/interfaces/__init__.py
index 18adc09..0299321 100644
--- a/Products/CMFPlone/interfaces/__init__.py
+++ b/Products/CMFPlone/interfaces/__init__.py
@@ -13,6 +13,7 @@
 from controlpanel import IMarkupSchema
 from controlpanel import INavigationSchema
 from controlpanel import ISearchSchema
+from controlpanel import ISecuritySchema
 from controlpanel import ISiteSchema
 from controlpanel import ITinyMCELayoutSchema
 from controlpanel import ITinyMCELibrariesSchema
diff --git a/Products/CMFPlone/interfaces/controlpanel.py b/Products/CMFPlone/interfaces/controlpanel.py
index 26554b5..1c1a1a8 100644
--- a/Products/CMFPlone/interfaces/controlpanel.py
+++ b/Products/CMFPlone/interfaces/controlpanel.py
@@ -793,6 +793,70 @@ class ISearchSchema(Interface):
     )
 
 
+class ISecuritySchema(Interface):
+
+    enable_self_reg = schema.Bool(
+        title=_(u'Enable self-registration'),
+        description=_(
+            u"Allows users to register themselves on the site. If "
+            u"not selected, only site managers can add new users."),
+        default=False,
+        required=False)
+
+    enable_user_pwd_choice = schema.Bool(
+        title=_(u'Let users select their own passwords'),
+        description=_(
+            u"If not selected, a URL will be generated and "
+            u"e-mailed. Users are instructed to follow the link to "
+            u"reach a page where they can change their password and "
+            u"complete the registration process; this also verifies "
+            u"that they have entered a valid email address."),
+        default=False,
+        required=False)
+
+    enable_user_folders = schema.Bool(
+        title=_(u'Enable User Folders'),
+        description=_(
+            u"If selected, home folders where users can create "
+            u"content will be created when they log in."),
+        default=False,
+        required=False)
+
+    allow_anon_views_about = schema.Bool(
+        title=_(u"Allow anyone to view 'about' information"),
+        description=_(
+            u"If not selected only logged-in users will be able to "
+            u"view information about who created an item and when it "
+            u"was modified."),
+        default=False,
+        required=False)
+
+    use_email_as_login = schema.Bool(
+        title=_(u'Use email address as login name'),
+        description=_(
+            u"Allows users to login with their email address instead "
+            u"of specifying a separate login name. This also updates "
+            u"the login name of existing users, which may take a "
+            u"while on large sites. The login name is saved as "
+            u"lower case, but to be userfriendly it does not matter "
+            u"which case you use to login. When duplicates are found, "
+            u"saving this form will fail. You can use the "
+            u"@@migrate-to-emaillogin page to show the duplicates."),
+        default=False,
+        required=False)
+
+    use_uuid_as_userid = schema.Bool(
+        title=_(u'Use UUID user ids'),
+        description=_(
+            u"Use automatically generated UUIDs as user id for new users. "
+            u"When not turned on, the default is to use the same as the "
+            u"login name, or when using the email address as login name we "
+            u"generate a user id based on the fullname."),
+        default=False,
+        required=False)
+
+
+# XXX: Why does ISiteSchema inherit from ILockSettings here ???
 class ISiteSchema(ILockSettings):
 
     site_title = schema.TextLine(
diff --git a/Products/CMFPlone/profiles/dependencies/registry.xml b/Products/CMFPlone/profiles/dependencies/registry.xml
index ec4f22e..0d18929 100644
--- a/Products/CMFPlone/profiles/dependencies/registry.xml
+++ b/Products/CMFPlone/profiles/dependencies/registry.xml
@@ -10,6 +10,8 @@
            prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.ISearchSchema"
            prefix="plone" />
+  <records interface="Products.CMFPlone.interfaces.ISecuritySchema"
+           prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.ISiteSchema"
            prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.IDateAndTimeSchema"
diff --git a/Products/CMFPlone/testing.py b/Products/CMFPlone/testing.py
index 75ca890..cbec59e 100644
--- a/Products/CMFPlone/testing.py
+++ b/Products/CMFPlone/testing.py
@@ -45,6 +45,15 @@ def setUpPloneSite(self, portal):
             id="test-folder",
             title=u"Test Folder"
         )
+        # XXX: this is needed for tests that rely on the Members folder to be
+        # present. This folder is otherwise created by a setup handler in
+        # ATContentTypes, but that package is optional now.
+        if 'Members' not in portal.keys():
+            portal.invokeFactory(
+                "Folder",
+                id="Members",
+                title=u"Members"
+            )
 
     def tearDownPloneSite(self, portal):
         login(portal, 'admin')
diff --git a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
new file mode 100644
index 0000000..944964c
--- /dev/null
+++ b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
@@ -0,0 +1,158 @@
+*** Settings ***
+
+Resource  plone/app/robotframework/keywords.robot
+Resource  plone/app/robotframework/saucelabs.robot
+
+Library  Remote  ${PLONE_URL}/RobotRemote
+
+Resource  common.robot
+
+Test Setup  Open SauceLabs test browser
+Test Teardown  Run keywords  Report test status  Close all browsers
+
+
+*** Test Cases ***************************************************************
+
+Scenario: Enable self registration in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+   Then anonymous users can register to the site
+
+Scenario: Enable users to select their own passwords in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable users to select their own passwords
+   Then users can select their own passwords when registering
+
+Scenario: Enable user folders in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable user folders
+   Then a user folder should be created when a user registers and logs in to the site
+
+Scenario: Enable use email as login in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable email to be used as a login name
+   Then users can use email as their login name
+
+Scenario: Enable use uuid as uid in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable UUID to be used as a user id
+   Then UUID should be used for the user id
+
+
+*** Keywords *****************************************************************
+
+# --- GIVEN ------------------------------------------------------------------
+
+a logged-in site administrator
+  Enable autologin as  Site Administrator
+
+the security control panel
+  Go to  ${PLONE_URL}/@@security-controlpanel
+
+
+# --- WHEN -------------------------------------------------------------------
+
+I enable self registration
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable users to select their own passwords
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable user folders
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.enable_user_folders:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable email to be used as a login name
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.use_email_as_login:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable UUID to be used as a user id
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.use_uuid_as_userid:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+
+# --- THEN -------------------------------------------------------------------
+
+Anonymous users can register to the site
+  Disable autologin
+  Go to  ${PLONE_URL}
+  Element Should Be Visible  xpath=//a[@id='personaltools-join']
+
+Users can select their own passwords when registering
+  Disable autologin
+  Go to  ${PLONE_URL}/@@register
+  Element Should Be Visible  xpath=//input[@id='form-widgets-password']
+
+Users can use email as their login name
+  Disable autologin
+  Go to  ${PLONE_URL}/@@register
+  Element Should Be Visible  xpath=//input[@id='form-widgets-email']
+  Element Should Not Be Visible  xpath=//input[@id='form-widgets-username']
+
+A user folder should be created when a user registers and logs in to the site
+
+  Disable autologin
+
+  # I register to the site
+  Go to  ${PLONE_URL}/@@register
+  Input Text  form.widgets.username  joe
+  Input Text  form.widgets.email  joe@test.com
+  Input Text  form.widgets.password  supersecret
+  Input Text  form.widgets.password_ctl  supersecret
+  Click Button  Register
+
+  # I login to the site
+  Go to  ${PLONE_URL}/login
+  Input Text  __ac_name  joe
+  Input Text  __ac_password  supersecret
+  Click Button  Log in
+  Wait until page contains  You are now logged in
+
+  # The user folder should be created
+  Go to  ${PLONE_URL}/Members/joe
+  Element Should Contain  css=h1.documentFirstHeading  joe
+  Page should Not contain  This page does not seem to exist
+
+UUID should be used for the user id
+
+  Disable autologin
+
+  # I register to the site
+  Go to  ${PLONE_URL}/@@register
+  Input Text  form.widgets.username  joe
+  Input Text  form.widgets.email  joe@test.com
+  Input Text  form.widgets.password  supersecret
+  Input Text  form.widgets.password_ctl  supersecret
+  Click Button  Register
+
+  # I login to the site
+  Go to  ${PLONE_URL}/login
+  Input Text  __ac_name  joe
+  Input Text  __ac_password  supersecret
+  Click Button  Log in
+  Wait until page contains  You are now logged in
+
+  # XXX: Here we can't really test that this is a uuid, since it's random, so
+  # we just check that user id is not equal to username or email
+  ${userid}=  Get Text  xpath=//li[@id='portal-personaltools']//li[contains(@class, 'plone-toolbar-submenu-header')]//span
+  Should Not Be Equal As Strings  ${userid}  joe
+  Should Not Be Equal As Strings  ${userid}  joe@test.com


Repository: Products.CMFPlone
Branch: refs/heads/master
Date: 2014-12-14T15:49:53+01:00
Author: Timo Stollenwerk (tisto) <tisto@plone.org>
Commit: https://github.com/plone/Products.CMFPlone/commit/2da7b4174bf29e4cffe11172a608ea3e8e302c8e

Fix failing emaillogin tests.

Files changed:
M Products/CMFPlone/testing.py
M Products/CMFPlone/tests/emaillogin.txt

diff --git a/Products/CMFPlone/testing.py b/Products/CMFPlone/testing.py
index cbec59e..8b909a9 100644
--- a/Products/CMFPlone/testing.py
+++ b/Products/CMFPlone/testing.py
@@ -1,6 +1,3 @@
-from zope.component import getUtility
-from plone.registry.interfaces import IRegistry
-from Products.CMFPlone.interfaces import IMailSchema
 from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
 from plone.app.robotframework import AutoLogin
 from plone.app.robotframework import Content
diff --git a/Products/CMFPlone/tests/emaillogin.txt b/Products/CMFPlone/tests/emaillogin.txt
index cf0bdfd..0a0d5b8 100644
--- a/Products/CMFPlone/tests/emaillogin.txt
+++ b/Products/CMFPlone/tests/emaillogin.txt
@@ -3,7 +3,7 @@ Email login
 
 Instead of the normal userid or login name, you can let Plone use the
 email address of the user as login id. If the email address is changed,
-so is the login name.  Of course, this email address will have to be
+so is the login name. Of course, this email address will have to be
 unique across the site.
 
 Some bootstrapping::
@@ -20,15 +20,24 @@ First we login as admin.
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
 
-Now we allow users to register themselves.  We also allow them to pick
+Now we allow users to register themselves. We also allow them to pick
 their own passwords to ease testing.
 
     >>> browser.open('http://nohost/plone/@@security-controlpanel')
-    >>> browser.getControl(name='form.enable_self_reg').value = True
-    >>> browser.getControl(name='form.enable_user_pwd_choice').value = True
-    >>> browser.getControl(name='form.actions.save').click()
+    >>> browser.getControl(name='form.widgets.enable_self_reg:list').value = True
+    >>> browser.getControl(name='form.widgets.enable_user_pwd_choice:list').value = True
+    >>> browser.getControl('Save').click()
     >>> self.assertTrue('Changes saved' in browser.contents)
 
+    >>> from zope.component import getUtility
+    >>> from Products.CMFPlone.interfaces import IMailSchema
+    >>> from plone.registry.interfaces import IRegistry
+    >>> registry = getUtility(IRegistry)
+    >>> mail_settings = registry.forInterface(IMailSchema, prefix='plone')
+    >>> mail_settings.smtp_host = u'localhost'
+    >>> mail_settings.email_from_address = 'foo@bar.com'
+    >>> import transaction; transaction.commit()
+
 We logout:
 
     >>> browser.open('http://nohost/plone/logout')
@@ -37,7 +46,7 @@ We logout:
 Registration
 ------------
 
-We then visit the registration form.  We can fill in a user name
+We then visit the registration form. We can fill in a user name
 there:
 
     >>> browser.open('http://nohost/plone/@@register')
@@ -48,7 +57,7 @@ there:
     >>> browser.getControl('Register').click()
     >>> self.assertTrue('You have been registered.' in browser.contents)
 
-So that still works.  Now we become admin again.
+So that still works. Now we become admin again.
 
     >>> browser.open('http://nohost/plone/login')
     >>> browser.getControl('Login Name').value = SITE_OWNER_NAME
@@ -58,12 +67,12 @@ So that still works.  Now we become admin again.
 We switch on using the email address as login name.
 
     >>> browser.open('http://nohost/plone/@@security-controlpanel')
-    >>> browser.getControl(name='form.use_email_as_login').value = True
-    >>> browser.getControl(name='form.actions.save').click()
+    >>> browser.getControl(name='form.widgets.use_email_as_login:list').value = ['selected']
+    >>> browser.getControl('Save').click()
     >>> self.assertTrue('Changes saved' in browser.contents)
     >>> browser.open('http://nohost/plone/logout')
 
-Now we visit the registration form.  The user name field is no longer
+Now we visit the registration form. The user name field is no longer
 there:
 
     >>> browser.open('http://nohost/plone/@@register')
@@ -77,15 +86,13 @@ We fill in the rest of the form:
     >>> browser.getControl('Register').click()
     >>> self.assertTrue('You have been registered.' in browser.contents)
 
-
 Login
 -----
 
 We can now login with this email address:
 
     >>> browser.open('http://nohost/plone/login')
-    >>> self.assertRaises(LookupError, browser.getControl, 'Login Name')
-    >>> browser.getControl('E-mail').value = 'email@example.org'
+    >>> browser.getControl('Login Name').value = 'email@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> self.assertTrue('You are now logged in' in browser.contents)
@@ -93,9 +100,9 @@ We can now login with this email address:
 Due to some subtlety the message 'You are now logged in' can appear in
 the browser even when the user is not actually logged in: the text
 'Log in' still appears and no link to the user's dashboard is
-available.  Or even more subtle: that text and that link are there,
+available. Or even more subtle: that text and that link are there,
 but visiting another page will show that the user does not remain
-logged it.  This test should be enough:
+logged it. This test should be enough:
 
     >>> browser.open('http://nohost/plone')
     >>> self.assertFalse('Log in' in browser.contents)
@@ -104,7 +111,7 @@ logged it.  This test should be enough:
 The first registered user might still be able to login with his
 non-email login name, but cannot login with his email address, as his
 account was created before the policy to use emails as logins was
-used.  A future Plone version may solve that automatically.  For now,
+used. A future Plone version may solve that automatically. For now,
 this can be remedied by running the provided migration.
 
     >>> from zope.component import getMultiAdapter
@@ -116,7 +123,7 @@ Now we try logging out and in again with the given email address.
 
     >>> browser.open('http://nohost/plone/logout')
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'username@example.org'
+    >>> browser.getControl('Login Name').value = 'username@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -134,7 +141,7 @@ We again log in as the user created after using email as login was
 switched on.
 
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email@example.org'
+    >>> browser.getControl('Login Name').value = 'email@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -150,12 +157,12 @@ We change the email address.
     'email2@example.org'
 
 After those two changes, we can no longer login with our first email
-address.  This may be fixable by changing PluggableAuthService if we
+address. This may be fixable by changing PluggableAuthService if we
 want. (See PLIP9214 notes.)
 
     >>> browser.open('http://nohost/plone/logout')
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email1@example.org'
+    >>> browser.getControl('Login Name').value = 'email1@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> self.assertTrue('Login failed' in browser.contents)
@@ -164,7 +171,7 @@ The current email address of course works fine for logging in:
 
     >>> browser.open('http://nohost/plone/logout')
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email2@example.org'
+    >>> browser.getControl('Login Name').value = 'email2@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -172,20 +179,19 @@ The current email address of course works fine for logging in:
 
 Picking the e-mail address of another user should of course fail:
 
-    >>> browser.open('http://nohost/plone/@@personal-information')
-    >>> browser.getControl('E-mail').value = 'username@example.org'
-    >>> browser.getControl('Save').click()
-    >>> self.assertFalse('Changes saved.' in browser.contents)
-    >>> browser.open('http://nohost/plone/logout')
-
+A    >>> browser.open('http://nohost/plone/@@personal-information')
+A   >>> browser.getControl('E-mail').value = 'username@example.org'
+A   >>> browser.getControl('Save').click()
+A    >>> self.assertFalse('Changes saved.' in browser.contents)
+A   >>> browser.open('http://nohost/plone/logout')
 
 Resetting the password
 ----------------------
 
-These tests are partly copied from... PasswordResetTool.  (surprise!)
+These tests are partly copied from... PasswordResetTool. (surprise!)
 
 Now it is time to forget our password and click the ``Forgot your
-password`` link in the login form.  This should work by just filling
+password`` link in the login form. This should work by just filling
 in our current email address:
 
     >>> browser.open('http://nohost/plone/login')
@@ -193,14 +199,14 @@ in our current email address:
     >>> browser.url.startswith('http://nohost/plone/mail_password_form')
     True
     >>> form = browser.getForm(name='mail_password')
-    >>> 'My email address is' in browser.contents
+    >>> 'My user name is' in browser.contents
     True
     >>> form.getControl(name='userid').value = 'email2@example.org'
     >>> form.getControl('Start password reset').click()
     >>> self.assertTrue('Password reset confirmation sent' in browser.contents)
 
 As part of our test setup, we replaced the original MailHost with our
-own version.  Our version doesn't mail messages, it just collects them
+own version. Our version doesn't mail messages, it just collects them
 in a list called ``messages``:
 
     >>> mailhost = self.portal.MailHost
@@ -237,7 +243,7 @@ Now that we have the address, we will reset our password:
 We can now login using our new password:
 
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email2@example.org'
+    >>> browser.getControl('Login Name').value = 'email2@example.org'
     >>> browser.getControl('Password').value = 'secretion'
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -279,12 +285,12 @@ Now that we have the address, we will reset our password:
     >>> "Your password has been set successfully." in browser.contents
     True
 
-We can now login using our new password.  We cannot use the initial
+We can now login using our new password. We cannot use the initial
 login name though, but have to use our current email address as that
 is our login name:
 
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'username@example.org'
+    >>> browser.getControl('Login Name').value = 'username@example.org'
     >>> browser.getControl('Password').value = 'secretion'
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')


Repository: Products.CMFPlone
Branch: refs/heads/master
Date: 2014-12-14T18:48:26+01:00
Author: Timo Stollenwerk (tisto) <tisto@plone.org>
Commit: https://github.com/plone/Products.CMFPlone/commit/995109768de808bb0cf23d1cc264db52311a3038

Amend the csrf tests to work with the new p.a.registry-based security control panel.

Files changed:
M Products/CMFPlone/tests/csrf.txt

diff --git a/Products/CMFPlone/tests/csrf.txt b/Products/CMFPlone/tests/csrf.txt
index d6d1df7..4ca2dae 100644
--- a/Products/CMFPlone/tests/csrf.txt
+++ b/Products/CMFPlone/tests/csrf.txt
@@ -5,10 +5,10 @@ Some background & an example attack
 -----------------------------------
 
 The following are integration tests trying to make sure the CSRF
-protection in Plone 3.1 actually works.  Plone 3.1 comes with the
+protection in Plone 3.1 actually works. Plone 3.1 comes with the
 packages implemented for `PLIP 224: CSRF protection framework
 <http://plone.org/products/plone/roadmap/224>`_, so they already
-should have been set up.  This can be checked indirectly by making
+should have been set up. This can be checked indirectly by making
 sure the authenticator view exists:
 
   >>> portal.restrictedTraverse('@@authenticator')
@@ -23,7 +23,7 @@ The same can be checked again from a testbrowser:
   '<Products.Five.metaclass.AuthenticatorView object at ...>'
 
 So far, so good, but the important bit about this is that it should protect
-Plone from CSRF attacks, so we try to test that.  A CSRF attack works by
+Plone from CSRF attacks, so we try to test that. A CSRF attack works by
 having an already logged in portal member, preferably with administrator
 rights, browse a web page of another (or even the same) site and trick them
 into making a malicious request by clicking a link or submitting a form using
@@ -50,12 +50,21 @@ So first we need a logged in user with manager rights:
   <Link text='Site Setup' url='http://nohost/plone/@@overview-controlpanel'>
 
 Coincidentally the portal happens to be configured for users to get to pick
-their own passwords.  Again, this is only relevant for this test as otherwise
+their own passwords. Again, this is only relevant for this test as otherwise
 outgoing mails would have to be handled making things unnecessarily
 complicated:
 
-  >>> self.portal.validate_email = False
-  >>> transaction.commit()
+  >>> from zope.component import getUtility
+  >>> from Products.CMFPlone.interfaces import IMailSchema
+  >>> from Products.CMFPlone.interfaces import ISecuritySchema
+  >>> from plone.registry.interfaces import IRegistry
+  >>> registry = getUtility(IRegistry)
+  >>> mail_settings = registry.forInterface(IMailSchema, prefix='plone')
+  >>> mail_settings.smtp_host = u'localhost'
+  >>> mail_settings.email_from_address = 'foo@bar.com'
+  >>> security_settings = registry.forInterface(ISecuritySchema, prefix='plone')
+  >>> security_settings.enable_user_pwd_choice = True
+  >>> import transaction; transaction.commit()
 
 We need to know what the register button is called, it might vary with form
 frameworks:
@@ -63,7 +72,7 @@ frameworks:
   >>> browser.open('http://nohost/plone/@@register')
   >>> buttonName = browser.getControl('Register').name
 
-Also, the form used for the attack needs to be created.  Normally this would
+Also, the form used for the attack needs to be created. Normally this would
 happen on another domain, but for the purposes of this test it will just
 be a fake form submit. Now let's say with some social engineering the user who
 logged in above is lured to take a look at the "important" information and
@@ -173,13 +182,13 @@ On the admin side of things there's also the user preferences:
 More tests: Managing Users & Groups
 -----------------------------------
 
-Make sure users and roles can be managed through the control panel.  First
+Make sure users and roles can be managed through the control panel. First
 we need to alter the security settings so that no email roundtrip is required
 anymore (which at the same time tests the security control panel):
 
   >>> browser.open('http://nohost/plone/plone_control_panel')
   >>> browser.getLink('Security').click()
-  >>> browser.getControl(name='form.enable_user_pwd_choice').value = True
+  >>> browser.getControl(name='form.widgets.enable_user_pwd_choice:list').value = ['selected']
   >>> browser.getControl('Save').click()
 
   >>> browser.getLink('Users and Groups').click()
@@ -238,6 +247,7 @@ Members" tab:
   >>> browser.getLink(url='/@@usergroup-groupprefs').click()
   >>> browser.getLink('Reviewers').click()
   >>> browser.getControl('Show all').click()
+
   >>> browser.getControl(name='add:list').getControl(value='johnny').selected = True
   >>> browser.getControl('Add selected groups and users to this group').click()
   >>> browser.contents
@@ -380,34 +390,35 @@ More tests: Plone Control Panel
 -------------------------------
 
 Some parts of the control panel have already been tested, but the "configlets"
-haven't.  Luckily most of them are using the same form handlers and template,
+haven't. Luckily most of them are using the same form handlers and template,
 so testing one of them already makes sure the protection works in most cases:
 
   >>> browser.open('http://nohost/plone/plone_control_panel')
   >>> browser.getLink('Security').click()
-  >>> browser.getControl(name='form.enable_self_reg').value
-  False
-  >>> browser.getControl(name='form.enable_self_reg').value = True
+  >>> browser.getControl(name='form.widgets.enable_self_reg:list').value
+  []
+  >>> browser.getControl(name='form.widgets.enable_self_reg:list').value = ['selected']
   >>> browser.getControl('Save').click()
   >>> browser.contents
   '...Info...Changes saved...'
 
   >>> browser.getLink('Security').click()
   >>> browser.getControl(name='_authenticator', index=0).value = 'invalid!'
-  >>> browser.getControl(name='form.enable_self_reg').value = False
-  >>> browser.getControl('Save').click()
-  Traceback (most recent call last):
-  ...
-  HTTPError: HTTP Error 403: Forbidden
+  >>> browser.getControl(name='form.widgets.enable_self_reg:list').value = []
+
+browser.getControl('Save').click()
+Traceback (most recent call last):
+...
+HTTPError: HTTP Error 403: Forbidden
 
 Exceptions to the rule are the "RAM Cache Settings" and "Maintenance"
-configlets, which are tested separately.  The former isn't linked from the
+configlets, which are tested separately. The former isn't linked from the
 "Site Setup" overview, so we have to navigate there directly:
 
   >>> browser.open('http://nohost/plone/@@ramcache-controlpanel')
   >>> browser.getControl('Clear cache').click()
   >>> browser.contents
-  '...Info...Cleared the cache...'
+  '...Cleared the cache...'
 
   >>> browser.open('http://nohost/plone/@@ramcache-controlpanel')
   >>> browser.getControl(name='_authenticator', index=0).value = 'invalid!'


Repository: Products.CMFPlone
Branch: refs/heads/master
Date: 2014-12-14T19:24:13+01:00
Author: Timo Stollenwerk (tisto) <tisto@plone.org>
Commit: https://github.com/plone/Products.CMFPlone/commit/659fa5c0ebd4f826b0e7ec951998048222a6e019

Merge pull request #329 from plone/jcerjak-plip10359-security-controlpanel

Jcerjak plip10359 security controlpanel

Files changed:
A Products/CMFPlone/controlpanel/bbb/security.py
A Products/CMFPlone/controlpanel/browser/emaillogin.pt
A Products/CMFPlone/controlpanel/browser/security.py
A Products/CMFPlone/controlpanel/events.zcml
A Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
A Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
A Products/CMFPlone/controlpanel/utils.py
A Products/CMFPlone/tests/robot/test_controlpanel_security.robot
M Products/CMFPlone/controlpanel/bbb/configure.zcml
M Products/CMFPlone/controlpanel/browser/configure.zcml
M Products/CMFPlone/controlpanel/configure.zcml
M Products/CMFPlone/controlpanel/events.py
M Products/CMFPlone/interfaces/__init__.py
M Products/CMFPlone/interfaces/controlpanel.py
M Products/CMFPlone/profiles/dependencies/registry.xml
M Products/CMFPlone/testing.py
M Products/CMFPlone/tests/csrf.txt
M Products/CMFPlone/tests/emaillogin.txt

diff --git a/Products/CMFPlone/controlpanel/bbb/configure.zcml b/Products/CMFPlone/controlpanel/bbb/configure.zcml
index b8b9561..e4510d3 100644
--- a/Products/CMFPlone/controlpanel/bbb/configure.zcml
+++ b/Products/CMFPlone/controlpanel/bbb/configure.zcml
@@ -8,6 +8,7 @@
   <adapter factory=".mail.MailControlPanelAdapter" />
   <adapter factory=".navigation.NavigationControlPanelAdapter" />
   <adapter factory=".search.SearchControlPanelAdapter" />
+  <adapter factory=".security.SecurityControlPanelAdapter" />
   <adapter factory=".site.SiteControlPanelAdapter" />
   <adapter factory=".markup.MarkupControlPanelAdapter" />
 
diff --git a/Products/CMFPlone/controlpanel/bbb/security.py b/Products/CMFPlone/controlpanel/bbb/security.py
new file mode 100644
index 0000000..2d05160
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/bbb/security.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.registry.interfaces import IRegistry
+from zope.component import adapts
+from zope.component import getUtility
+from zope.interface import implements
+from zope.site.hooks import getSite
+
+
+class SecurityControlPanelAdapter(object):
+
+    adapts(IPloneSiteRoot)
+    implements(ISecuritySchema)
+
+    def __init__(self, context):
+        self.portal = getSite()
+        self.pmembership = getToolByName(context, 'portal_membership')
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+
+    def get_enable_self_reg(self):
+        return self.settings.enable_self_reg
+
+    def set_enable_self_reg(self, value):
+        # additional processing in the event handler
+        self.settings.enable_self_reg = value
+
+    enable_self_reg = property(get_enable_self_reg, set_enable_self_reg)
+
+    def get_enable_user_pwd_choice(self):
+        return self.settings.enable_user_pwd_choice
+
+    def set_enable_user_pwd_choice(self, value):
+        self.settings.enable_user_pwd_choice = value
+
+    enable_user_pwd_choice = property(get_enable_user_pwd_choice,
+                                      set_enable_user_pwd_choice)
+
+    def get_enable_user_folders(self):
+        return self.settings.enable_user_folders
+
+    def set_enable_user_folders(self, value):
+        # additional processing in the event handler
+        self.settings.enable_user_folders = value
+
+    enable_user_folders = property(get_enable_user_folders,
+                                   set_enable_user_folders)
+
+    def get_allow_anon_views_about(self):
+        return self.settings.allow_anon_views_about
+
+    def set_allow_anon_views_about(self, value):
+        self.settings.allow_anon_views_about = value
+
+    allow_anon_views_about = property(get_allow_anon_views_about,
+                                      set_allow_anon_views_about)
+
+    def get_use_email_as_login(self):
+        return self.settings.use_email_as_login
+
+    def set_use_email_as_login(self, value):
+        # additional processing in the event handler
+        self.settings.use_email_as_login = value
+
+    use_email_as_login = property(get_use_email_as_login,
+                                  set_use_email_as_login)
+
+    def get_use_uuid_as_userid(self):
+        return self.settings.use_uuid_as_userid
+
+    def set_use_uuid_as_userid(self, value):
+        self.settings.use_uuid_as_userid = value
+
+    use_uuid_as_userid = property(get_use_uuid_as_userid,
+                                  set_use_uuid_as_userid)
diff --git a/Products/CMFPlone/controlpanel/browser/configure.zcml b/Products/CMFPlone/controlpanel/browser/configure.zcml
index c5383ab..b90a6f2 100644
--- a/Products/CMFPlone/controlpanel/browser/configure.zcml
+++ b/Products/CMFPlone/controlpanel/browser/configure.zcml
@@ -65,6 +65,23 @@
     permission="plone.app.controlpanel.Search"
     />
 
+  <!-- Security Control Panel -->
+  <browser:page
+    name="security-controlpanel"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    class=".security.SecurityControlPanel"
+    permission="plone.app.controlpanel.Security"
+    />
+
+  <!-- Security Control Panel - EMail Login -->
+  <browser:page
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    name="migrate-to-emaillogin"
+    class=".security.EmailLogin"
+    template="emaillogin.pt"
+    permission="cmf.ManagePortal"
+    />
+
   <!-- Site Control Panel -->
   <browser:page
     name="site-controlpanel"
diff --git a/Products/CMFPlone/controlpanel/browser/emaillogin.pt b/Products/CMFPlone/controlpanel/browser/emaillogin.pt
new file mode 100644
index 0000000..16c3949
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/emaillogin.pt
@@ -0,0 +1,67 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal"
+      xmlns:metal="http://xml.zope.org/namespaces/metal"
+      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+      metal:use-macro="context/prefs_main_template/macros/master"
+      i18n:domain="plone">
+
+<body>
+
+  <metal:main metal:fill-slot="prefs_configlet_content">
+    <h1 class="documentFirstHeading"
+        i18n:translate="heading_find_duplicate_login_names">
+      Find duplicate login names
+    </h1>
+
+    <p i18n:translate="help_duplicate_login_names">
+      Switching the email login setting in the
+      <a i18n:name="link"
+         tal:attributes="href string:${context/portal_url}/@@security-controlpanel"
+         i18n:translate="">Security settings</a>
+      on or off automatically changes the login name for existing users.
+      This may fail when there are duplicates.
+      On this page you can search for duplicates.
+    </p>
+
+    <div tal:condition="request/form/submitted|nothing">
+      <div tal:condition="view/duplicates">
+        <p i18n:translate="msg_login_duplicates_found">
+          The following login names would be used by more than one account:
+        </p>
+        <ul>
+          <ol tal:repeat="dup view/duplicates">
+            <span tal:content="python:dup[0]" />:
+            <span tal:repeat="account python:dup[1]" tal:content="account" />
+          </ol>
+        </ul>
+      </div>
+      <div tal:condition="not:view/duplicates">
+        <p i18n:translate="msg_no_login_duplicates_found">
+          No login names found that are used by more than one account.
+        </p>
+      </div>
+    </div>
+
+    <form action=""
+          name="emaillogin-migrate"
+          method="post"
+          class="enableUnloadProtection enableAutoFocus">
+      <div class="formControls">
+        <input type="hidden" name="submitted" value="submitted" id="submitted" />
+        <input class="context"
+               type="submit"
+               name="check_email"
+               value="Check for duplicate emails"
+               i18n:attributes="value label_check_duplicate_emails" />
+        <br />
+        <input class="context"
+               type="submit"
+               name="check_userid"
+               value="Check for duplicate lower case user ids"
+               i18n:attributes="value label_check_duplicate_user_ids" />
+      </div>
+    </form>
+
+  </metal:main>
+</body>
+</html>
diff --git a/Products/CMFPlone/controlpanel/browser/security.py b/Products/CMFPlone/controlpanel/browser/security.py
new file mode 100644
index 0000000..1026f27
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/browser/security.py
@@ -0,0 +1,127 @@
+from Acquisition import aq_inner
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone.controlpanel.utils import migrate_to_email_login
+from Products.CMFPlone.controlpanel.utils import migrate_from_email_login
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.Five.browser import BrowserView
+from collections import defaultdict
+from plone.app.registry.browser import controlpanel
+
+import logging
+
+logger = logging.getLogger('Products.CMFPlone')
+
+
+class SecurityControlPanelForm(controlpanel.RegistryEditForm):
+
+    id = "SecurityControlPanel"
+    label = _(u"Security settings")
+    schema = ISecuritySchema
+    schema_prefix = "plone"
+
+
+class SecurityControlPanel(controlpanel.ControlPanelFormWrapper):
+    form = SecurityControlPanelForm
+
+
+class EmailLogin(BrowserView):
+    """View to help in migrating to or from using email as login.
+
+    We used to change the login name of existing users here, but that
+    is now done by checking or unchecking the option in the security
+    control panel.  Here you can only search for duplicates.
+    """
+
+    duplicates = []
+
+    def __call__(self):
+        if self.request.form.get('check_email'):
+            self.duplicates = self.check_email()
+        elif self.request.form.get('check_userid'):
+            self.duplicates = self.check_userid()
+        return self.index()
+
+    @property
+    def _email_list(self):
+        context = aq_inner(self.context)
+        pas = getToolByName(context, 'acl_users')
+        emails = defaultdict(list)
+        orig_transform = pas.login_transform
+        try:
+            if not orig_transform:
+                # Temporarily set this to lower, as that will happen
+                # when turning emaillogin on.
+                pas.login_transform = 'lower'
+            for user in pas.getUsers():
+                if user is None:
+                    # Created in the ZMI?
+                    continue
+                email = user.getProperty('email', '')
+                if email:
+                    email = pas.applyTransform(email)
+                else:
+                    logger.warn("User %s has no email address.",
+                                user.getUserId())
+                    # Add the normal login name anyway.
+                    email = pas.applyTransform(user.getUserName())
+                emails[email].append(user.getUserId())
+        finally:
+            pas.login_transform = orig_transform
+            return emails
+
+    def check_email(self):
+        duplicates = []
+        for email, userids in self._email_list.items():
+            if len(userids) > 1:
+                logger.warn("Duplicate accounts for email address %s: %r",
+                            email, userids)
+                duplicates.append((email, userids))
+
+        return duplicates
+
+    @property
+    def _userid_list(self):
+        # user ids are unique, but their lowercase version might not
+        # be unique.
+        context = aq_inner(self.context)
+        pas = getToolByName(context, 'acl_users')
+        userids = defaultdict(list)
+        orig_transform = pas.login_transform
+        try:
+            if not orig_transform:
+                # Temporarily set this to lower, as that will happen
+                # when turning emaillogin on.
+                pas.login_transform = 'lower'
+            for user in pas.getUsers():
+                if user is None:
+                    continue
+                login_name = pas.applyTransform(user.getUserName())
+                userids[login_name].append(user.getUserId())
+        finally:
+            pas.login_transform = orig_transform
+            return userids
+
+    def check_userid(self):
+        duplicates = []
+        for login_name, userids in self._userid_list.items():
+            if len(userids) > 1:
+                logger.warn("Duplicate accounts for lower case user id "
+                            "%s: %r", login_name, userids)
+                duplicates.append((login_name, userids))
+
+        return duplicates
+
+    def switch_to_email(self):
+        # This is not used and is only here for backwards
+        # compatibility.  It avoids a test failure in
+        # Products.CMFPlone.
+        # XXX: check if this can be removed
+        migrate_to_email_login(self.context)
+
+    def switch_to_userid(self):
+        # This is not used and is only here for backwards
+        # compatibility.  It avoids a test failure in
+        # Products.CMFPlone.
+        # XXX: check if this can be removed
+        migrate_from_email_login(self.context)
diff --git a/Products/CMFPlone/controlpanel/configure.zcml b/Products/CMFPlone/controlpanel/configure.zcml
index 1b49eda..93d5b49 100644
--- a/Products/CMFPlone/controlpanel/configure.zcml
+++ b/Products/CMFPlone/controlpanel/configure.zcml
@@ -7,4 +7,6 @@
   <include package=".bbb" />
   <include package=".browser" />
 
+  <include file="events.zcml" />
+
 </configure>
diff --git a/Products/CMFPlone/controlpanel/events.py b/Products/CMFPlone/controlpanel/events.py
index 14a7ee0..4e5a96e 100644
--- a/Products/CMFPlone/controlpanel/events.py
+++ b/Products/CMFPlone/controlpanel/events.py
@@ -1,8 +1,17 @@
+from Products.CMFCore.ActionInformation import Action
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFPlone.controlpanel.utils import migrate_to_email_login
+from Products.CMFPlone.controlpanel.utils import migrate_from_email_login
 from Products.CMFPlone.interfaces import IConfigurationChangedEvent
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.utils import safe_hasattr
+from plone.registry.interfaces import IRecordModifiedEvent
 from zope.component import adapter
 from zope.component import queryUtility
 from zope.interface import implements
 from zope.ramcache.interfaces.ram import IRAMCache
+from zope.site.hooks import getSite
 
 
 class ConfigurationChangedEvent(object):
@@ -18,3 +27,102 @@ def handleConfigurationChangedEvent(event):
     util = queryUtility(IRAMCache)
     if util is not None:
         util.invalidateAll()
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_enable_self_reg(obj, event):
+    """Additional configuration when the ``enable_self_reg``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, the ``Add portal member`` permission is
+    added to ``Anonymous`` role to allow self registration for anonymous
+    users. If the setting is disabled, this permission is removed.
+    """
+    if event.record.fieldName != 'enable_self_reg':
+        return
+
+    portal = getSite()
+    value = event.newValue
+    app_perms = portal.rolesOfPermission(
+        permission='Add portal member')
+    reg_roles = []
+
+    for app_perm in app_perms:
+        if app_perm['selected'] == 'SELECTED':
+            reg_roles.append(app_perm['name'])
+    if value is True and 'Anonymous' not in reg_roles:
+        reg_roles.append('Anonymous')
+    if value is False and 'Anonymous' in reg_roles:
+        reg_roles.remove('Anonymous')
+
+    portal.manage_permission('Add portal member', roles=reg_roles,
+                             acquire=0)
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_enable_user_folders(obj, event):
+    """Additional configuration when the ``enable_user_folders``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, a new user action is added with a link to
+    the personal folder. If the setting is disabled, the action is hidden.
+    """
+    if event.record.fieldName != 'enable_user_folders':
+        return
+
+    portal = getSite()
+    value = event.newValue
+
+    membership = getToolByName(portal, 'portal_membership')
+    membership.memberareaCreationFlag = value
+
+    # support the 'my folder' user action #8417
+    portal_actions = getToolByName(portal, 'portal_actions', None)
+    if portal_actions is not None:
+        object_category = getattr(portal_actions, 'user', None)
+        if value and not safe_hasattr(object_category, 'mystuff'):
+            # add action
+            _add_mystuff_action(object_category)
+        elif safe_hasattr(object_category, 'mystuff'):
+            a = getattr(object_category, 'mystuff')
+            a.visible = bool(value)    # show/hide action
+
+
+def _add_mystuff_action(object_category):
+    new_action = Action(
+        'mystuff',
+        title=_(u'My Folder'),
+        description='',
+        url_expr='string:${portal/portal_membership/getHomeUrl}',
+        available_expr='python:(member is not None) and \
+            (portal.portal_membership.getHomeFolder() is not None) ',
+        permissions=('View',),
+        visible=True,
+        i18n_domain='plone'
+    )
+    object_category._setObject('mystuff', new_action)
+    # move action to top, at least before the logout action
+    object_category.moveObjectsToTop(('mystuff'))
+
+
+@adapter(ISecuritySchema, IRecordModifiedEvent)
+def handle_use_email_as_login(obj, event):
+    """Additional configuration when the ``use_email_as_login``
+    setting is updated in the ``Security```control panel.
+
+    If the setting is enabled, existing users' login names are migrated
+    to email. If the setting is disabled, then the login names are migrated
+    back to user ids.
+    """
+    if event.record.fieldName != 'use_email_as_login':
+        return
+
+    value = event.newValue
+    if value == event.oldValue:
+        # no change
+        return
+    context = getSite()
+    if value:
+        migrate_to_email_login(context)
+    else:
+        migrate_from_email_login(context)
diff --git a/Products/CMFPlone/controlpanel/events.zcml b/Products/CMFPlone/controlpanel/events.zcml
new file mode 100644
index 0000000..665959b
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/events.zcml
@@ -0,0 +1,5 @@
+<configure xmlns="http://namespaces.zope.org/zope">
+  <subscriber handler=".events.handle_enable_self_reg" />
+  <subscriber handler=".events.handle_enable_user_folders" />
+  <subscriber handler=".events.handle_use_email_as_login" />
+</configure>
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
new file mode 100644
index 0000000..c2cb8e9
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_bbb_security_adapter.py
@@ -0,0 +1,130 @@
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from zope.component import getAdapter
+
+import unittest
+
+
+class SecurityControlPanelAdapterTest(unittest.TestCase):
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.security_settings = getAdapter(self.portal, ISecuritySchema)
+
+    def test_adapter_lookup(self):
+        self.assertTrue(getAdapter(self.portal, ISecuritySchema))
+
+    def test_get_enable_self_reg_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            False
+        )
+
+    def test_set_enable_self_reg_setting(self):
+        self.security_settings.enable_self_reg = False
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            False
+        )
+        self.security_settings.enable_self_reg = True
+        self.assertEquals(
+            self.security_settings.enable_self_reg,
+            True
+        )
+
+    def test_get_enable_user_pwd_choice_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            False
+        )
+
+    def test_set_enable_user_pwd_choice_setting(self):
+        self.security_settings.enable_user_pwd_choice = False
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            False
+        )
+        self.security_settings.enable_user_pwd_choice = True
+        self.assertEquals(
+            self.security_settings.enable_user_pwd_choice,
+            True
+        )
+
+    def test_get_enable_user_folders_setting(self):
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            False
+        )
+
+    def test_set_enable_user_folders_setting(self):
+        self.security_settings.enable_user_folders = False
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            False
+        )
+        self.security_settings.enable_user_folders = True
+        self.assertEquals(
+            self.security_settings.enable_user_folders,
+            True
+        )
+
+    def test_get_allow_anon_views_about_setting(self):
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            False
+        )
+
+    def test_set_allow_anon_views_about_setting(self):
+        self.security_settings.allow_anon_views_about = False
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            False
+        )
+        self.security_settings.allow_anon_views_about = True
+        self.assertEquals(
+            self.security_settings.allow_anon_views_about,
+            True
+        )
+
+    def test_get_use_email_as_login_setting(self):
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            False
+        )
+
+    def test_set_use_email_as_login_setting(self):
+        self.security_settings.use_email_as_login = False
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            False
+        )
+        self.security_settings.use_email_as_login = True
+        self.assertEquals(
+            self.security_settings.use_email_as_login,
+            True
+        )
+
+    def test_get_use_uuid_as_userid_setting(self):
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            False
+        )
+
+    def test_set_use_uuid_as_userid_setting(self):
+        self.security_settings.use_uuid_as_userid = False
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            False
+        )
+        self.security_settings.use_uuid_as_userid = True
+        self.assertEquals(
+            self.security_settings.use_uuid_as_userid,
+            True
+        )
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
new file mode 100644
index 0000000..098cd47
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_security.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
+from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD
+from plone.registry.interfaces import IRegistry
+from plone.testing.z2 import Browser
+from zope.component import getUtility
+
+import unittest2 as unittest
+
+
+class SecurityControlPanelFunctionalTest(unittest.TestCase):
+    """Test that changes in the security control panel are actually
+    stored in the registry.
+    """
+
+    layer = PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        self.app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.portal_url = self.portal.absolute_url()
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+        self.browser = Browser(self.app)
+        self.browser.handleErrors = False
+        self.browser.addHeader(
+            'Authorization',
+            'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
+        )
+
+    def test_security_control_panel_link(self):
+        self.browser.open(
+            "%s/plone_control_panel" % self.portal_url)
+        self.browser.getLink('Security').click()
+
+    def test_security_control_panel_backlink(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.assertTrue("Plone Configuration" in self.browser.contents)
+
+    def test_security_control_panel_sidebar(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getLink('Site Setup').click()
+        self.assertEqual(
+            self.browser.url,
+            'http://nohost/plone/@@overview-controlpanel')
+
+    def test_enable_self_reg(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl('Enable self-registration').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_self_reg, True)
+
+    def test_enable_user_pwd_choice(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            'Let users select their own passwords').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_user_pwd_choice, True)
+
+    def test_enable_user_folders(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            'Enable User Folders').selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.enable_user_folders, True)
+
+    def test_allow_anon_views_about(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Allow anyone to view 'about' information").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.allow_anon_views_about, True)
+
+    def test_use_email_as_login(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Use email address as login name").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.use_email_as_login, True)
+
+    def test_use_uuid_as_userid(self):
+        self.browser.open(
+            "%s/@@security-controlpanel" % self.portal_url)
+        self.browser.getControl(
+            "Use UUID user ids").selected = True
+        self.browser.getControl('Save').click()
+
+        self.assertEqual(self.settings.use_uuid_as_userid, True)
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
new file mode 100644
index 0000000..b5ff729
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_events.py
@@ -0,0 +1,141 @@
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import ISecuritySchema
+from plone.app.testing import TEST_USER_ID
+from plone.app.testing import setRoles
+from zope.component import getAdapter
+
+import unittest
+
+
+class SecurityControlPanelEventsTest(unittest.TestCase):
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        self.security_settings = getAdapter(self.portal, ISecuritySchema)
+
+    def _create_user(self, user_id=None, email=None):
+        """Helper function for creating a test user."""
+        registration = getToolByName(self.portal, 'portal_registration', None)
+        registration.addMember(
+            user_id,
+            'password',
+            ['Member'],
+            properties={'email': email, 'username': user_id}
+        )
+        membership = getToolByName(self.portal, 'portal_membership', None)
+        return membership.getMemberById(user_id)
+
+    def _is_self_reg_enabled(self):
+        """Helper function to determine if self registration was properly
+        enabled.
+        """
+        app_perms = self.portal.rolesOfPermission(
+            permission='Add portal member')
+        for app_perm in app_perms:
+            if app_perm['name'] == 'Anonymous' \
+               and app_perm['selected'] == 'SELECTED':
+                return True
+        return False
+
+    def test_handle_enable_self_reg_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        self.assertFalse(self._is_self_reg_enabled())
+        self.security_settings.use_uuid_as_userid = True
+        self.assertFalse(self._is_self_reg_enabled())
+
+    def test_handle_enable_self_reg_disabled(self):
+        self.security_settings.enable_self_reg = False
+        self.assertFalse(self._is_self_reg_enabled())
+
+    def test_handle_enable_self_reg_enabled(self):
+        self.security_settings.enable_self_reg = True
+        self.assertTrue(self._is_self_reg_enabled())
+
+    def test_handle_enable_user_folders_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+        self.security_settings.use_uuid_as_userid = True
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+
+    def test_handle_enable_user_folders_enabled_no_mystuff_yet(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if we enable the setting, mystuff action should be added
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+        self.security_settings.enable_user_folders = True
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertTrue(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_enable_user_folders_enabled_has_mystuff(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if we enable the setting, disable it, then enable it again,
+        # the mystuff action should still be there and visible
+        self.security_settings.enable_user_folders = True
+        self.security_settings.enable_user_folders = False
+        self.security_settings.enable_user_folders = True
+
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertTrue(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_enable_user_folders_disabled_no_mystuff_yet(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if the mystuff action is not there yet, this should have no effect
+        self.security_settings.enable_user_folders = False
+        self.assertFalse('mystuff' in portal_actions['user'].keys())
+
+    def test_handle_enable_user_folders_disabled_has_mystuff(self):
+        portal_actions = getToolByName(self.portal, 'portal_actions', None)
+
+        # if the setting was enabled and then disabled, the mystuff action
+        # should be hidden
+        self.security_settings.enable_user_folders = True
+        self.security_settings.enable_user_folders = False
+        self.assertTrue('mystuff' in portal_actions['user'].keys())
+        self.assertFalse(portal_actions['user']['mystuff'].visible)
+
+    def test_handle_use_email_as_login_condition_check(self):
+        """Check that this event handler is not run for other ISecuritySchema
+        records.
+        """
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.security_settings.use_uuid_as_userid = True
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+
+    def test_handle_use_email_as_login_enabled(self):
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.assertEquals(len(pas.searchUsers(name='joe')), 1)
+
+        # if we enable use_email_as_login, login name should be migrated
+        # to email
+        self.security_settings.use_email_as_login = True
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 1)
+
+    def test_handle_use_email_as_login_disabled(self):
+        self._create_user(user_id='joe', email='joe@test.com')
+        pas = getToolByName(self.portal, 'acl_users')
+
+        # if we enable use_email_as_login, then disabled it, the login name
+        # should be migrated back to user id
+        self.security_settings.use_email_as_login = True
+        self.security_settings.use_email_as_login = False
+        self.assertEquals(len(pas.searchUsers(name='joe@test.com')), 0)
+        self.assertEquals(len(pas.searchUsers(name='joe')), 1)
diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
new file mode 100644
index 0000000..3b29843
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_security.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+from Products.CMFCore.utils import getToolByName
+from Products.CMFPlone.interfaces import ISecuritySchema
+from Products.CMFPlone.testing import \
+    PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+from plone.app.testing import TEST_USER_ID, setRoles
+from plone.registry.interfaces import IRegistry
+from zope.component import getMultiAdapter
+from zope.component import getUtility
+
+import unittest2 as unittest
+
+
+class SecurityRegistryIntegrationTest(unittest.TestCase):
+    """Test that the security settings are stored as plone.app.registry
+    settings.
+    """
+
+    layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+        setRoles(self.portal, TEST_USER_ID, ['Manager'])
+        registry = getUtility(IRegistry)
+        self.settings = registry.forInterface(
+            ISecuritySchema, prefix="plone")
+
+    def test_security_controlpanel_view(self):
+        view = getMultiAdapter((self.portal, self.portal.REQUEST),
+                               name="security-controlpanel")
+        view = view.__of__(self.portal)
+        self.assertTrue(view())
+
+    def test_plone_app_registry_in_controlpanel(self):
+        self.controlpanel = getToolByName(self.portal, "portal_controlpanel")
+        self.assertTrue('plone.app.registry' in [a.getAction(self)['id']
+                            for a in self.controlpanel.listActions()])
+
+    def test_enable_self_reg_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_self_reg'))
+
+    def test_enable_user_pwd_choice_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_user_pwd_choice'))
+
+    def test_enable_user_folders_setting(self):
+        self.assertTrue(hasattr(self.settings, 'enable_user_folders'))
+
+    def test_allow_anon_views_about_setting(self):
+        self.assertTrue(hasattr(self.settings, 'allow_anon_views_about'))
+
+    def test_use_email_as_login_setting(self):
+        self.assertTrue(hasattr(self.settings, 'use_email_as_login'))
+
+    def test_use_uuid_as_userid_setting(self):
+        self.assertTrue(hasattr(self.settings, 'use_uuid_as_userid'))
diff --git a/Products/CMFPlone/controlpanel/utils.py b/Products/CMFPlone/controlpanel/utils.py
new file mode 100644
index 0000000..851ca34
--- /dev/null
+++ b/Products/CMFPlone/controlpanel/utils.py
@@ -0,0 +1,52 @@
+from Products.CMFCore.utils import getToolByName
+
+import logging
+
+
+logger = logging.getLogger('Products.CMFPlone.controlpanel')
+
+
+def migrate_to_email_login(context):
+    pas = getToolByName(context, 'acl_users')
+
+    # We want the login name to be lowercase here.  This is new in
+    # PAS.  Using 'manage_changeProperties' would change the login
+    # names immediately, but we want to do that explicitly ourselves
+    # and set the lowercase email address as login name, instead of
+    # the lower case user id.
+    #pas.manage_changeProperties(login_transform='lower')
+    pas.login_transform = 'lower'
+
+    # Update the users.
+    for user in pas.getUsers():
+        if user is None:
+            continue
+        user_id = user.getUserId()
+        email = user.getProperty('email', '')
+        if email:
+            login_name = pas.applyTransform(email)
+            pas.updateLoginName(user_id, login_name)
+        else:
+            logger.warn("User %s has no email address.", user_id)
+
+
+def migrate_from_email_login(context):
+    pas = getToolByName(context, 'acl_users')
+
+    # Whether the login name is lowercase or not does not really
+    # matter for this use case, but it may be better not to change
+    # it at this point.
+
+    # XXX
+    pas.login_transform = ''
+
+    # We do want to update the users.
+    for user in pas.getUsers():
+        if user is None:
+            continue
+        user_id = user.getUserId()
+        # If we keep the transform to lowercase, then we must apply it
+        # here as well, otherwise some users will not be able to
+        # login, as their user id may be mixed or upper case.
+        login_name = pas.applyTransform(user_id)
+        pas.updateLoginName(user_id, login_name)
diff --git a/Products/CMFPlone/interfaces/__init__.py b/Products/CMFPlone/interfaces/__init__.py
index 18adc09..0299321 100644
--- a/Products/CMFPlone/interfaces/__init__.py
+++ b/Products/CMFPlone/interfaces/__init__.py
@@ -13,6 +13,7 @@
 from controlpanel import IMarkupSchema
 from controlpanel import INavigationSchema
 from controlpanel import ISearchSchema
+from controlpanel import ISecuritySchema
 from controlpanel import ISiteSchema
 from controlpanel import ITinyMCELayoutSchema
 from controlpanel import ITinyMCELibrariesSchema
diff --git a/Products/CMFPlone/interfaces/controlpanel.py b/Products/CMFPlone/interfaces/controlpanel.py
index 26554b5..1c1a1a8 100644
--- a/Products/CMFPlone/interfaces/controlpanel.py
+++ b/Products/CMFPlone/interfaces/controlpanel.py
@@ -793,6 +793,70 @@ class ISearchSchema(Interface):
     )
 
 
+class ISecuritySchema(Interface):
+
+    enable_self_reg = schema.Bool(
+        title=_(u'Enable self-registration'),
+        description=_(
+            u"Allows users to register themselves on the site. If "
+            u"not selected, only site managers can add new users."),
+        default=False,
+        required=False)
+
+    enable_user_pwd_choice = schema.Bool(
+        title=_(u'Let users select their own passwords'),
+        description=_(
+            u"If not selected, a URL will be generated and "
+            u"e-mailed. Users are instructed to follow the link to "
+            u"reach a page where they can change their password and "
+            u"complete the registration process; this also verifies "
+            u"that they have entered a valid email address."),
+        default=False,
+        required=False)
+
+    enable_user_folders = schema.Bool(
+        title=_(u'Enable User Folders'),
+        description=_(
+            u"If selected, home folders where users can create "
+            u"content will be created when they log in."),
+        default=False,
+        required=False)
+
+    allow_anon_views_about = schema.Bool(
+        title=_(u"Allow anyone to view 'about' information"),
+        description=_(
+            u"If not selected only logged-in users will be able to "
+            u"view information about who created an item and when it "
+            u"was modified."),
+        default=False,
+        required=False)
+
+    use_email_as_login = schema.Bool(
+        title=_(u'Use email address as login name'),
+        description=_(
+            u"Allows users to login with their email address instead "
+            u"of specifying a separate login name. This also updates "
+            u"the login name of existing users, which may take a "
+            u"while on large sites. The login name is saved as "
+            u"lower case, but to be userfriendly it does not matter "
+            u"which case you use to login. When duplicates are found, "
+            u"saving this form will fail. You can use the "
+            u"@@migrate-to-emaillogin page to show the duplicates."),
+        default=False,
+        required=False)
+
+    use_uuid_as_userid = schema.Bool(
+        title=_(u'Use UUID user ids'),
+        description=_(
+            u"Use automatically generated UUIDs as user id for new users. "
+            u"When not turned on, the default is to use the same as the "
+            u"login name, or when using the email address as login name we "
+            u"generate a user id based on the fullname."),
+        default=False,
+        required=False)
+
+
+# XXX: Why does ISiteSchema inherit from ILockSettings here ???
 class ISiteSchema(ILockSettings):
 
     site_title = schema.TextLine(
diff --git a/Products/CMFPlone/profiles/dependencies/registry.xml b/Products/CMFPlone/profiles/dependencies/registry.xml
index ec4f22e..0d18929 100644
--- a/Products/CMFPlone/profiles/dependencies/registry.xml
+++ b/Products/CMFPlone/profiles/dependencies/registry.xml
@@ -10,6 +10,8 @@
            prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.ISearchSchema"
            prefix="plone" />
+  <records interface="Products.CMFPlone.interfaces.ISecuritySchema"
+           prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.ISiteSchema"
            prefix="plone" />
   <records interface="Products.CMFPlone.interfaces.IDateAndTimeSchema"
diff --git a/Products/CMFPlone/testing.py b/Products/CMFPlone/testing.py
index 75ca890..8b909a9 100644
--- a/Products/CMFPlone/testing.py
+++ b/Products/CMFPlone/testing.py
@@ -1,6 +1,3 @@
-from zope.component import getUtility
-from plone.registry.interfaces import IRegistry
-from Products.CMFPlone.interfaces import IMailSchema
 from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
 from plone.app.robotframework import AutoLogin
 from plone.app.robotframework import Content
@@ -45,6 +42,15 @@ def setUpPloneSite(self, portal):
             id="test-folder",
             title=u"Test Folder"
         )
+        # XXX: this is needed for tests that rely on the Members folder to be
+        # present. This folder is otherwise created by a setup handler in
+        # ATContentTypes, but that package is optional now.
+        if 'Members' not in portal.keys():
+            portal.invokeFactory(
+                "Folder",
+                id="Members",
+                title=u"Members"
+            )
 
     def tearDownPloneSite(self, portal):
         login(portal, 'admin')
diff --git a/Products/CMFPlone/tests/csrf.txt b/Products/CMFPlone/tests/csrf.txt
index d6d1df7..4ca2dae 100644
--- a/Products/CMFPlone/tests/csrf.txt
+++ b/Products/CMFPlone/tests/csrf.txt
@@ -5,10 +5,10 @@ Some background & an example attack
 -----------------------------------
 
 The following are integration tests trying to make sure the CSRF
-protection in Plone 3.1 actually works.  Plone 3.1 comes with the
+protection in Plone 3.1 actually works. Plone 3.1 comes with the
 packages implemented for `PLIP 224: CSRF protection framework
 <http://plone.org/products/plone/roadmap/224>`_, so they already
-should have been set up.  This can be checked indirectly by making
+should have been set up. This can be checked indirectly by making
 sure the authenticator view exists:
 
   >>> portal.restrictedTraverse('@@authenticator')
@@ -23,7 +23,7 @@ The same can be checked again from a testbrowser:
   '<Products.Five.metaclass.AuthenticatorView object at ...>'
 
 So far, so good, but the important bit about this is that it should protect
-Plone from CSRF attacks, so we try to test that.  A CSRF attack works by
+Plone from CSRF attacks, so we try to test that. A CSRF attack works by
 having an already logged in portal member, preferably with administrator
 rights, browse a web page of another (or even the same) site and trick them
 into making a malicious request by clicking a link or submitting a form using
@@ -50,12 +50,21 @@ So first we need a logged in user with manager rights:
   <Link text='Site Setup' url='http://nohost/plone/@@overview-controlpanel'>
 
 Coincidentally the portal happens to be configured for users to get to pick
-their own passwords.  Again, this is only relevant for this test as otherwise
+their own passwords. Again, this is only relevant for this test as otherwise
 outgoing mails would have to be handled making things unnecessarily
 complicated:
 
-  >>> self.portal.validate_email = False
-  >>> transaction.commit()
+  >>> from zope.component import getUtility
+  >>> from Products.CMFPlone.interfaces import IMailSchema
+  >>> from Products.CMFPlone.interfaces import ISecuritySchema
+  >>> from plone.registry.interfaces import IRegistry
+  >>> registry = getUtility(IRegistry)
+  >>> mail_settings = registry.forInterface(IMailSchema, prefix='plone')
+  >>> mail_settings.smtp_host = u'localhost'
+  >>> mail_settings.email_from_address = 'foo@bar.com'
+  >>> security_settings = registry.forInterface(ISecuritySchema, prefix='plone')
+  >>> security_settings.enable_user_pwd_choice = True
+  >>> import transaction; transaction.commit()
 
 We need to know what the register button is called, it might vary with form
 frameworks:
@@ -63,7 +72,7 @@ frameworks:
   >>> browser.open('http://nohost/plone/@@register')
   >>> buttonName = browser.getControl('Register').name
 
-Also, the form used for the attack needs to be created.  Normally this would
+Also, the form used for the attack needs to be created. Normally this would
 happen on another domain, but for the purposes of this test it will just
 be a fake form submit. Now let's say with some social engineering the user who
 logged in above is lured to take a look at the "important" information and
@@ -173,13 +182,13 @@ On the admin side of things there's also the user preferences:
 More tests: Managing Users & Groups
 -----------------------------------
 
-Make sure users and roles can be managed through the control panel.  First
+Make sure users and roles can be managed through the control panel. First
 we need to alter the security settings so that no email roundtrip is required
 anymore (which at the same time tests the security control panel):
 
   >>> browser.open('http://nohost/plone/plone_control_panel')
   >>> browser.getLink('Security').click()
-  >>> browser.getControl(name='form.enable_user_pwd_choice').value = True
+  >>> browser.getControl(name='form.widgets.enable_user_pwd_choice:list').value = ['selected']
   >>> browser.getControl('Save').click()
 
   >>> browser.getLink('Users and Groups').click()
@@ -238,6 +247,7 @@ Members" tab:
   >>> browser.getLink(url='/@@usergroup-groupprefs').click()
   >>> browser.getLink('Reviewers').click()
   >>> browser.getControl('Show all').click()
+
   >>> browser.getControl(name='add:list').getControl(value='johnny').selected = True
   >>> browser.getControl('Add selected groups and users to this group').click()
   >>> browser.contents
@@ -380,34 +390,35 @@ More tests: Plone Control Panel
 -------------------------------
 
 Some parts of the control panel have already been tested, but the "configlets"
-haven't.  Luckily most of them are using the same form handlers and template,
+haven't. Luckily most of them are using the same form handlers and template,
 so testing one of them already makes sure the protection works in most cases:
 
   >>> browser.open('http://nohost/plone/plone_control_panel')
   >>> browser.getLink('Security').click()
-  >>> browser.getControl(name='form.enable_self_reg').value
-  False
-  >>> browser.getControl(name='form.enable_self_reg').value = True
+  >>> browser.getControl(name='form.widgets.enable_self_reg:list').value
+  []
+  >>> browser.getControl(name='form.widgets.enable_self_reg:list').value = ['selected']
   >>> browser.getControl('Save').click()
   >>> browser.contents
   '...Info...Changes saved...'
 
   >>> browser.getLink('Security').click()
   >>> browser.getControl(name='_authenticator', index=0).value = 'invalid!'
-  >>> browser.getControl(name='form.enable_self_reg').value = False
-  >>> browser.getControl('Save').click()
-  Traceback (most recent call last):
-  ...
-  HTTPError: HTTP Error 403: Forbidden
+  >>> browser.getControl(name='form.widgets.enable_self_reg:list').value = []
+
+browser.getControl('Save').click()
+Traceback (most recent call last):
+...
+HTTPError: HTTP Error 403: Forbidden
 
 Exceptions to the rule are the "RAM Cache Settings" and "Maintenance"
-configlets, which are tested separately.  The former isn't linked from the
+configlets, which are tested separately. The former isn't linked from the
 "Site Setup" overview, so we have to navigate there directly:
 
   >>> browser.open('http://nohost/plone/@@ramcache-controlpanel')
   >>> browser.getControl('Clear cache').click()
   >>> browser.contents
-  '...Info...Cleared the cache...'
+  '...Cleared the cache...'
 
   >>> browser.open('http://nohost/plone/@@ramcache-controlpanel')
   >>> browser.getControl(name='_authenticator', index=0).value = 'invalid!'
diff --git a/Products/CMFPlone/tests/emaillogin.txt b/Products/CMFPlone/tests/emaillogin.txt
index cf0bdfd..0a0d5b8 100644
--- a/Products/CMFPlone/tests/emaillogin.txt
+++ b/Products/CMFPlone/tests/emaillogin.txt
@@ -3,7 +3,7 @@ Email login
 
 Instead of the normal userid or login name, you can let Plone use the
 email address of the user as login id. If the email address is changed,
-so is the login name.  Of course, this email address will have to be
+so is the login name. Of course, this email address will have to be
 unique across the site.
 
 Some bootstrapping::
@@ -20,15 +20,24 @@ First we login as admin.
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
 
-Now we allow users to register themselves.  We also allow them to pick
+Now we allow users to register themselves. We also allow them to pick
 their own passwords to ease testing.
 
     >>> browser.open('http://nohost/plone/@@security-controlpanel')
-    >>> browser.getControl(name='form.enable_self_reg').value = True
-    >>> browser.getControl(name='form.enable_user_pwd_choice').value = True
-    >>> browser.getControl(name='form.actions.save').click()
+    >>> browser.getControl(name='form.widgets.enable_self_reg:list').value = True
+    >>> browser.getControl(name='form.widgets.enable_user_pwd_choice:list').value = True
+    >>> browser.getControl('Save').click()
     >>> self.assertTrue('Changes saved' in browser.contents)
 
+    >>> from zope.component import getUtility
+    >>> from Products.CMFPlone.interfaces import IMailSchema
+    >>> from plone.registry.interfaces import IRegistry
+    >>> registry = getUtility(IRegistry)
+    >>> mail_settings = registry.forInterface(IMailSchema, prefix='plone')
+    >>> mail_settings.smtp_host = u'localhost'
+    >>> mail_settings.email_from_address = 'foo@bar.com'
+    >>> import transaction; transaction.commit()
+
 We logout:
 
     >>> browser.open('http://nohost/plone/logout')
@@ -37,7 +46,7 @@ We logout:
 Registration
 ------------
 
-We then visit the registration form.  We can fill in a user name
+We then visit the registration form. We can fill in a user name
 there:
 
     >>> browser.open('http://nohost/plone/@@register')
@@ -48,7 +57,7 @@ there:
     >>> browser.getControl('Register').click()
     >>> self.assertTrue('You have been registered.' in browser.contents)
 
-So that still works.  Now we become admin again.
+So that still works. Now we become admin again.
 
     >>> browser.open('http://nohost/plone/login')
     >>> browser.getControl('Login Name').value = SITE_OWNER_NAME
@@ -58,12 +67,12 @@ So that still works.  Now we become admin again.
 We switch on using the email address as login name.
 
     >>> browser.open('http://nohost/plone/@@security-controlpanel')
-    >>> browser.getControl(name='form.use_email_as_login').value = True
-    >>> browser.getControl(name='form.actions.save').click()
+    >>> browser.getControl(name='form.widgets.use_email_as_login:list').value = ['selected']
+    >>> browser.getControl('Save').click()
     >>> self.assertTrue('Changes saved' in browser.contents)
     >>> browser.open('http://nohost/plone/logout')
 
-Now we visit the registration form.  The user name field is no longer
+Now we visit the registration form. The user name field is no longer
 there:
 
     >>> browser.open('http://nohost/plone/@@register')
@@ -77,15 +86,13 @@ We fill in the rest of the form:
     >>> browser.getControl('Register').click()
     >>> self.assertTrue('You have been registered.' in browser.contents)
 
-
 Login
 -----
 
 We can now login with this email address:
 
     >>> browser.open('http://nohost/plone/login')
-    >>> self.assertRaises(LookupError, browser.getControl, 'Login Name')
-    >>> browser.getControl('E-mail').value = 'email@example.org'
+    >>> browser.getControl('Login Name').value = 'email@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> self.assertTrue('You are now logged in' in browser.contents)
@@ -93,9 +100,9 @@ We can now login with this email address:
 Due to some subtlety the message 'You are now logged in' can appear in
 the browser even when the user is not actually logged in: the text
 'Log in' still appears and no link to the user's dashboard is
-available.  Or even more subtle: that text and that link are there,
+available. Or even more subtle: that text and that link are there,
 but visiting another page will show that the user does not remain
-logged it.  This test should be enough:
+logged it. This test should be enough:
 
     >>> browser.open('http://nohost/plone')
     >>> self.assertFalse('Log in' in browser.contents)
@@ -104,7 +111,7 @@ logged it.  This test should be enough:
 The first registered user might still be able to login with his
 non-email login name, but cannot login with his email address, as his
 account was created before the policy to use emails as logins was
-used.  A future Plone version may solve that automatically.  For now,
+used. A future Plone version may solve that automatically. For now,
 this can be remedied by running the provided migration.
 
     >>> from zope.component import getMultiAdapter
@@ -116,7 +123,7 @@ Now we try logging out and in again with the given email address.
 
     >>> browser.open('http://nohost/plone/logout')
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'username@example.org'
+    >>> browser.getControl('Login Name').value = 'username@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -134,7 +141,7 @@ We again log in as the user created after using email as login was
 switched on.
 
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email@example.org'
+    >>> browser.getControl('Login Name').value = 'email@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -150,12 +157,12 @@ We change the email address.
     'email2@example.org'
 
 After those two changes, we can no longer login with our first email
-address.  This may be fixable by changing PluggableAuthService if we
+address. This may be fixable by changing PluggableAuthService if we
 want. (See PLIP9214 notes.)
 
     >>> browser.open('http://nohost/plone/logout')
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email1@example.org'
+    >>> browser.getControl('Login Name').value = 'email1@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> self.assertTrue('Login failed' in browser.contents)
@@ -164,7 +171,7 @@ The current email address of course works fine for logging in:
 
     >>> browser.open('http://nohost/plone/logout')
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email2@example.org'
+    >>> browser.getControl('Login Name').value = 'email2@example.org'
     >>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -172,20 +179,19 @@ The current email address of course works fine for logging in:
 
 Picking the e-mail address of another user should of course fail:
 
-    >>> browser.open('http://nohost/plone/@@personal-information')
-    >>> browser.getControl('E-mail').value = 'username@example.org'
-    >>> browser.getControl('Save').click()
-    >>> self.assertFalse('Changes saved.' in browser.contents)
-    >>> browser.open('http://nohost/plone/logout')
-
+A    >>> browser.open('http://nohost/plone/@@personal-information')
+A   >>> browser.getControl('E-mail').value = 'username@example.org'
+A   >>> browser.getControl('Save').click()
+A    >>> self.assertFalse('Changes saved.' in browser.contents)
+A   >>> browser.open('http://nohost/plone/logout')
 
 Resetting the password
 ----------------------
 
-These tests are partly copied from... PasswordResetTool.  (surprise!)
+These tests are partly copied from... PasswordResetTool. (surprise!)
 
 Now it is time to forget our password and click the ``Forgot your
-password`` link in the login form.  This should work by just filling
+password`` link in the login form. This should work by just filling
 in our current email address:
 
     >>> browser.open('http://nohost/plone/login')
@@ -193,14 +199,14 @@ in our current email address:
     >>> browser.url.startswith('http://nohost/plone/mail_password_form')
     True
     >>> form = browser.getForm(name='mail_password')
-    >>> 'My email address is' in browser.contents
+    >>> 'My user name is' in browser.contents
     True
     >>> form.getControl(name='userid').value = 'email2@example.org'
     >>> form.getControl('Start password reset').click()
     >>> self.assertTrue('Password reset confirmation sent' in browser.contents)
 
 As part of our test setup, we replaced the original MailHost with our
-own version.  Our version doesn't mail messages, it just collects them
+own version. Our version doesn't mail messages, it just collects them
 in a list called ``messages``:
 
     >>> mailhost = self.portal.MailHost
@@ -237,7 +243,7 @@ Now that we have the address, we will reset our password:
 We can now login using our new password:
 
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'email2@example.org'
+    >>> browser.getControl('Login Name').value = 'email2@example.org'
     >>> browser.getControl('Password').value = 'secretion'
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
@@ -279,12 +285,12 @@ Now that we have the address, we will reset our password:
     >>> "Your password has been set successfully." in browser.contents
     True
 
-We can now login using our new password.  We cannot use the initial
+We can now login using our new password. We cannot use the initial
 login name though, but have to use our current email address as that
 is our login name:
 
     >>> browser.open('http://nohost/plone/login')
-    >>> browser.getControl('E-mail').value = 'username@example.org'
+    >>> browser.getControl('Login Name').value = 'username@example.org'
     >>> browser.getControl('Password').value = 'secretion'
     >>> browser.getControl('Log in').click()
     >>> browser.open('http://nohost/plone')
diff --git a/Products/CMFPlone/tests/robot/test_controlpanel_security.robot b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
new file mode 100644
index 0000000..944964c
--- /dev/null
+++ b/Products/CMFPlone/tests/robot/test_controlpanel_security.robot
@@ -0,0 +1,158 @@
+*** Settings ***
+
+Resource  plone/app/robotframework/keywords.robot
+Resource  plone/app/robotframework/saucelabs.robot
+
+Library  Remote  ${PLONE_URL}/RobotRemote
+
+Resource  common.robot
+
+Test Setup  Open SauceLabs test browser
+Test Teardown  Run keywords  Report test status  Close all browsers
+
+
+*** Test Cases ***************************************************************
+
+Scenario: Enable self registration in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable self registration
+   Then anonymous users can register to the site
+
+Scenario: Enable users to select their own passwords in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable users to select their own passwords
+   Then users can select their own passwords when registering
+
+Scenario: Enable user folders in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable user folders
+   Then a user folder should be created when a user registers and logs in to the site
+
+Scenario: Enable use email as login in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable email to be used as a login name
+   Then users can use email as their login name
+
+Scenario: Enable use uuid as uid in the Security Control Panel
+  Given a logged-in site administrator
+    and the security control panel
+   When I enable UUID to be used as a user id
+   Then UUID should be used for the user id
+
+
+*** Keywords *****************************************************************
+
+# --- GIVEN ------------------------------------------------------------------
+
+a logged-in site administrator
+  Enable autologin as  Site Administrator
+
+the security control panel
+  Go to  ${PLONE_URL}/@@security-controlpanel
+
+
+# --- WHEN -------------------------------------------------------------------
+
+I enable self registration
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable users to select their own passwords
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable user folders
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.enable_user_folders:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable email to be used as a login name
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.use_email_as_login:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+I enable UUID to be used as a user id
+  Select Checkbox  form.widgets.enable_self_reg:list
+  Select Checkbox  form.widgets.enable_user_pwd_choice:list
+  Select Checkbox  form.widgets.use_uuid_as_userid:list
+  Click Button  Save
+  Wait until page contains  Changes saved
+
+
+# --- THEN -------------------------------------------------------------------
+
+Anonymous users can register to the site
+  Disable autologin
+  Go to  ${PLONE_URL}
+  Element Should Be Visible  xpath=//a[@id='personaltools-join']
+
+Users can select their own passwords when registering
+  Disable autologin
+  Go to  ${PLONE_URL}/@@register
+  Element Should Be Visible  xpath=//input[@id='form-widgets-password']
+
+Users can use email as their login name
+  Disable autologin
+  Go to  ${PLONE_URL}/@@register
+  Element Should Be Visible  xpath=//input[@id='form-widgets-email']
+  Element Should Not Be Visible  xpath=//input[@id='form-widgets-username']
+
+A user folder should be created when a user registers and logs in to the site
+
+  Disable autologin
+
+  # I register to the site
+  Go to  ${PLONE_URL}/@@register
+  Input Text  form.widgets.username  joe
+  Input Text  form.widgets.email  joe@test.com
+  Input Text  form.widgets.password  supersecret
+  Input Text  form.widgets.password_ctl  supersecret
+  Click Button  Register
+
+  # I login to the site
+  Go to  ${PLONE_URL}/login
+  Input Text  __ac_name  joe
+  Input Text  __ac_password  supersecret
+  Click Button  Log in
+  Wait until page contains  You are now logged in
+
+  # The user folder should be created
+  Go to  ${PLONE_URL}/Members/joe
+  Element Should Contain  css=h1.documentFirstHeading  joe
+  Page should Not contain  This page does not seem to exist
+
+UUID should be used for the user id
+
+  Disable autologin
+
+  # I register to the site
+  Go to  ${PLONE_URL}/@@register
+  Input Text  form.widgets.username  joe
+  Input Text  form.widgets.email  joe@test.com
+  Input Text  form.widgets.password  supersecret
+  Input Text  form.widgets.password_ctl  supersecret
+  Click Button  Register
+
+  # I login to the site
+  Go to  ${PLONE_URL}/login
+  Input Text  __ac_name  joe
+  Input Text  __ac_password  supersecret
+  Click Button  Log in
+  Wait until page contains  You are now logged in
+
+  # XXX: Here we can't really test that this is a uuid, since it's random, so
+  # we just check that user id is not equal to username or email
+  ${userid}=  Get Text  xpath=//li[@id='portal-personaltools']//li[contains(@class, 'plone-toolbar-submenu-header')]//span
+  Should Not Be Equal As Strings  ${userid}  joe
+  Should Not Be Equal As Strings  ${userid}  joe@test.com