diff --git a/Products/CMFPlone/controlpanel/bbb/configure.zcml b/Products/CMFPlone/controlpanel/bbb/configure.zcml index dc8d0ba840..c3af9231da 100644 --- a/Products/CMFPlone/controlpanel/bbb/configure.zcml +++ b/Products/CMFPlone/controlpanel/bbb/configure.zcml @@ -7,6 +7,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 1e3a13ee6c..9510e55909 100644 --- a/Products/CMFPlone/controlpanel/browser/configure.zcml +++ b/Products/CMFPlone/controlpanel/browser/configure.zcml @@ -57,6 +57,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 35b0069c13..cd5fcc2ad0 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 01a0816ff1..1a0a7db3aa 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 bd110f342d..279a9f0868 100644 --- a/Products/CMFPlone/profiles/dependencies/registry.xml +++ b/Products/CMFPlone/profiles/dependencies/registry.xml @@ -8,6 +8,8 @@ prefix="plone" /> +