diff --git a/Products/CMFPlone/controlpanel/bbb/configure.zcml b/Products/CMFPlone/controlpanel/bbb/configure.zcml index b8b95617b1..e4510d3544 100644 --- a/Products/CMFPlone/controlpanel/bbb/configure.zcml +++ b/Products/CMFPlone/controlpanel/bbb/configure.zcml @@ -8,6 +8,7 @@ + diff --git a/Products/CMFPlone/controlpanel/bbb/security.py b/Products/CMFPlone/controlpanel/bbb/security.py new file mode 100644 index 0000000000..2d051606a7 --- /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 c5383ab72d..b90a6f21ca 100644 --- a/Products/CMFPlone/controlpanel/browser/configure.zcml +++ b/Products/CMFPlone/controlpanel/browser/configure.zcml @@ -65,6 +65,23 @@ permission="plone.app.controlpanel.Search" /> + + + + + + + + + + +

+ Find duplicate login names +

+ +

+ Switching the email login setting in the + Security settings + 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. +

+ +
+
+

+ The following login names would be used by more than one account: +

+
    +
      + : + +
    +
+
+
+

+ No login names found that are used by more than one account. +

+
+
+ +
+
+ + +
+ +
+
+ +
+ + diff --git a/Products/CMFPlone/controlpanel/browser/security.py b/Products/CMFPlone/controlpanel/browser/security.py new file mode 100644 index 0000000000..1026f27dfb --- /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 1b49edab07..93d5b4998b 100644 --- a/Products/CMFPlone/controlpanel/configure.zcml +++ b/Products/CMFPlone/controlpanel/configure.zcml @@ -7,4 +7,6 @@ + + diff --git a/Products/CMFPlone/controlpanel/events.py b/Products/CMFPlone/controlpanel/events.py index 14a7ee05c3..4e5a96e0b5 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 0000000000..665959b897 --- /dev/null +++ b/Products/CMFPlone/controlpanel/events.zcml @@ -0,0 +1,5 @@ + + + + + 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 0000000000..c2cb8e93ff --- /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 0000000000..098cd47c5f --- /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 0000000000..b5ff729fbd --- /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 0000000000..3b2984356d --- /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 0000000000..851ca34b68 --- /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 18adc09005..029932119f 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 26554b5a7b..1c1a1a836f 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 ec4f22e47d..0d189296a6 100644 --- a/Products/CMFPlone/profiles/dependencies/registry.xml +++ b/Products/CMFPlone/profiles/dependencies/registry.xml @@ -10,6 +10,8 @@ prefix="plone" /> + `_, 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: '' 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: 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 cf0bdfd927..0a0d5b8236 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 0000000000..944964cb95 --- /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