diff --git a/product/ERP5eGovSecurity/EGOVGroupManager.py b/product/ERP5eGovSecurity/EGOVGroupManager.py new file mode 100644 index 0000000000000000000000000000000000000000..8de6030a6cf0ef8414c71201be539c528f52ae79 --- /dev/null +++ b/product/ERP5eGovSecurity/EGOVGroupManager.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2010 Nexedi SARL and Contributors. All Rights Reserved. +# Mohamadou Mbengue <mayoro@gmail.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## +""" Classes: ERP5GroupManager +""" + +from Globals import InitializeClass +from AccessControl.SecurityManagement import newSecurityManager,\ + getSecurityManager, setSecurityManager +from Products.PageTemplates.PageTemplateFile import PageTemplateFile +from AccessControl import ClassSecurityInfo +from Products.PluggableAuthService.PropertiedUser import PropertiedUser +from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin +from Products.PluggableAuthService.utils import classImplements +from Products.PluggableAuthService.interfaces.plugins import IGroupsPlugin +from Products.ERP5Security.ERP5GroupManager import ERP5GroupManager +from Products.ERP5Type.Cache import CachingMethod +from ZODB.POSException import ConflictError +from Products.ERP5Security.ERP5GroupManager import ConsistencyError + +import sys + +from zLOG import LOG, WARNING + +from Products.ERP5Security.ERP5UserManager import SUPER_USER + + +NO_CACHE_MODE = 0 +manage_addEGOVGroupManagerForm = PageTemplateFile( + 'www/ERP5Security_addERP5GroupManager', globals(), + __name__='manage_addERP5GroupManagerForm' ) + +def addEGOVGroupManager( dispatcher, id, title=None, REQUEST=None ): + """ Add a EGOVGroupManager to a Pluggable Auth Service. """ + egm = EGOVGroupManager(id, title) + dispatcher._setObject(egm.getId(), egm) + + if REQUEST is not None: + REQUEST['RESPONSE'].redirect( + '%s/manage_workspace' + '?manage_tabs_message=' + 'EGOVGroupManager+added.' + % dispatcher.absolute_url()) + + +class EGOVGroupManager(ERP5GroupManager): + """ PAS plugin for dynamically adding Groups + this plugin permit to login with evry portal_type + So it's possible to login with organisation + """ + meta_type = 'EGOV Group Manager' + security = ClassSecurityInfo() + + portal_type_list = ('Person',) + + _properties = BasePlugin._properties + ( + {'label' : 'Portal Type List (Experimental)', + 'type' : 'lines', + 'id' : 'portal_type_list', + 'mode' : 'w', + }, + ) + + def __init__(self, id, title=None): + + self._id = self.id = id + self.title = title + + # + # IGroupsPlugin implementation + # + def getGroupsForPrincipal(self, principal, request=None): + """ See IGroupsPlugin. + """ + # If this is the super user, skip the check. + if principal.getId() == SUPER_USER: + return () + + def _getGroupsForPrincipal(user_name, path): + security_category_dict = {} # key is the base_category_list, + # value is the list of fetched categories + security_group_list = [] + security_definition_list = () + + # because we aren't logged in, we have to create our own + # SecurityManager to be able to access the Catalog + sm = getSecurityManager() + if sm.getUser().getId() != SUPER_USER: + newSecurityManager(self, self.getUser(SUPER_USER)) + try: + # To get the complete list of groups, we try to call the + # ERP5Type_getSecurityCategoryMapping which should return a list + # of lists of two elements (script, base_category_list) like : + # ( + # ('script_1', ['base_category_1', 'base_category_2', ...]), + # ('script_2', ['base_category_1', 'base_category_3', ...]) + # ) + # + # else, if the script does not exist, falls back to a list containng + # only one list : + # (('ERP5Type_getSecurityCategoryFromAssignment', + # self.getPortalAssignmentBaseCategoryList() ),) + + mapping_method = getattr(self, + 'ERP5Type_getSecurityCategoryMapping', None) + if mapping_method is None: + security_definition_list = (( + 'ERP5Type_getSecurityCategoryFromAssignment', + self.getPortalAssignmentBaseCategoryList() + ),) + else: + security_definition_list = mapping_method() + + # get the person from its reference - no security check needed + catalog_result = self.portal_catalog.unrestrictedSearchResults( + portal_type=self.portal_type_list, reference=user_name) + + if len(catalog_result) != 1: # we won't proceed with groups + if len(catalog_result) > 1: # configuration is screwed + raise ConsistencyError, 'There is more than one Person whose \ + login is %s : %s' % (user_name, + repr([r.getObject() for r in catalog_result])) + else: # no person is linked to this user login + portal = self.getPortalObject() + + # this permit to get the module of the application + # the goal is to work with anonymous applications, even if + # they are not reindexed + + module_id = self.REQUEST.get('anonymous_module', None) + if module_id: + module = getattr(portal, module_id, None) + if module is not None: + result = module._getOb(user_name, None) + if result is not None: + person_object = result + else: + return () + else: + return () + else: + person_object = catalog_result[0].getObject() + person_id = person_object.getId() + + # Fetch category values from defined scripts + for (method_name, base_category_list) in security_definition_list: + base_category_list = tuple(base_category_list) + method = getattr(self, method_name) + security_category_list = security_category_dict.setdefault( + base_category_list, []) + try: + security_category_list.extend( + method(base_category_list, user_name, person_object, '') + ) + except ConflictError: + raise + except: + LOG('EGOVGroupManager', WARNING, + 'could not get security categories from %s' % (method_name,), + error = sys.exc_info()) + + # Get group names from category values + group_id_list_generator = getattr(self, + 'ERP5Type_asSecurityGroupIdList', None) + if group_id_list_generator is None: + group_id_list_generator = getattr(self, 'ERP5Type_asSecurityGroupId') + generator_name = "ERP5Type_asSecurityGroupId" + else: + generator_name = 'ERP5Type_asSecurityGroupIdList' + for base_category_list, category_value_list in \ + security_category_dict.items(): + for category_dict in category_value_list: + try: + group_id_list = group_id_list_generator( + category_order=base_category_list, + **category_dict) + if isinstance(group_id_list, str): + group_id_list = [group_id_list] + security_group_list.extend(group_id_list) + except ConflictError: + raise + except: + LOG('EGOVGroupManager', WARNING, + 'could not get security groups from %s' % + generator_name, + error = sys.exc_info()) + finally: + setSecurityManager(sm) + return tuple(security_group_list) + + if not NO_CACHE_MODE: + _getGroupsForPrincipal = CachingMethod(_getGroupsForPrincipal, + id='EGOVGroupManager_getGroupsForPrincipal', + cache_factory='erp5_content_short') + + return _getGroupsForPrincipal( + user_name=principal.getId(), + path=self.getPhysicalPath()) + + +classImplements( EGOVGroupManager + , IGroupsPlugin + ) + +InitializeClass(EGOVGroupManager) diff --git a/product/ERP5eGovSecurity/EGOVUserManager.py b/product/ERP5eGovSecurity/EGOVUserManager.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3d6d300b860300765ae7595c42aa70865c6df0 --- /dev/null +++ b/product/ERP5eGovSecurity/EGOVUserManager.py @@ -0,0 +1,311 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2010 Nexedi SARL and Contributors. All Rights Reserved. +# Mohamadou Mbengue <mayoro@gmail.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## +""" Classes: ERP5UserManager +""" + +from Globals import InitializeClass +from AccessControl import ClassSecurityInfo +from AccessControl.SecurityManagement import getSecurityManager,\ + setSecurityManager, newSecurityManager +from Products.PageTemplates.PageTemplateFile import PageTemplateFile +from Products.PluggableAuthService.PluggableAuthService import \ + _SWALLOWABLE_PLUGIN_EXCEPTIONS +from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin +from Products.PluggableAuthService.utils import classImplements +from Products.PluggableAuthService.interfaces.plugins import IAuthenticationPlugin +from Products.PluggableAuthService.interfaces.plugins import IUserEnumerationPlugin +from Products.ERP5Type.Cache import CachingMethod +from Products.ERP5Security.ERP5UserManager import ERP5UserManager +from ZODB.POSException import ConflictError +import sys +from DateTime import DateTime +from zLOG import LOG, PROBLEM + +try : + from AccessControl.AuthEncoding import pw_validate +except ImportError: + pw_validate = lambda reference, attempt: reference == attempt + +# This user is used to bypass all security checks. +SUPER_USER = '__erp5security-=__' + +manage_addEGOVUserManagerForm = PageTemplateFile( + 'www/ERP5Security_addERP5UserManager', globals(), + __name__='manage_addERP5UserManagerForm' ) + + +def addEGOVUserManager(dispatcher, id, title=None, REQUEST=None): + """ Add a EGOVUserManager to a Pluggable Auth Service. """ + eum = EGOVUserManager(id, title) + dispatcher._setObject(eum.getId(), eum) + if REQUEST is not None: + REQUEST['RESPONSE'].redirect( + '%s/manage_workspace' + '?manage_tabs_message=' + 'EGOVUserManager+added.' + % dispatcher.absolute_url()) + +class EGOVUserManager(ERP5UserManager): + """ PAS plugin for managing users in ERP5 + this plugin permit to login with evry portal_type + So it's possible to login with organisation + """ + + meta_type = 'EGOV User Manager' + + security = ClassSecurityInfo() + + portal_type_list = ('Person',) + + _properties = BasePlugin._properties + ( + {'label' : 'Portal Type List (Experimental)', + 'type' : 'lines', + 'id' : 'portal_type_list', + 'mode' : 'w', + }, + ) + + def __init__(self, id, title=None): + + self._id = self.id = id + self.title = title + + # + # IAuthenticationPlugin implementation + # + security.declarePrivate( 'authenticateCredentials' ) + def authenticateCredentials(self, credentials): + """ See IAuthenticationPlugin. + + o We expect the credentials to be those returned by + ILoginPasswordExtractionPlugin. + """ + # Forbidden the usage of the super user. + if credentials.get('login') == SUPER_USER: + return None + + def _authenticateCredentials(login, password, path): + if not login or not password: + return None + + user_list = self.getUserByLogin((login,)) + + if not user_list: + return None + + user = user_list[0] + user_portal_type = user.getPortalType() + + sm = getSecurityManager() + if sm.getUser().getId() != SUPER_USER: + newSecurityManager(self, self.getUser(SUPER_USER)) + + # search for assignment only on person entity + if user_portal_type == 'Person': + try: + # get assignment + assignment_list = [x for x in \ + user.contentValues(portal_type="Assignment") if \ + x.getValidationState() == "open"] + valid_assignment_list = [] + # check dates if exist + login_date = DateTime() + for assignment in assignment_list: + if assignment.getStartDate() is not None and \ + assignment.getStartDate() > login_date: + continue + if assignment.getStopDate() is not None and \ + assignment.getStopDate() < login_date: + continue + valid_assignment_list.append(assignment) + + if pw_validate(user.getPassword(), password) and \ + len(valid_assignment_list): #user.getCareerRole() == 'internal': + return login, login # use same for user_id and login + finally: + setSecurityManager(sm) + else: + if pw_validate(user.getPassword(), password): + return login, login # use same for user_id and login + + return None + + _authenticateCredentials = CachingMethod(_authenticateCredentials, + id='ERP5UserManager_authenticateCredentials', + cache_factory='erp5_content_short') + return _authenticateCredentials( + login=credentials.get('login'), + password=credentials.get('password'), + path=self.getPhysicalPath()) + + # + # IUserEnumerationPlugin implementation + # + security.declarePrivate( 'enumerateUsers' ) + def enumerateUsers(self, id=None, login=None, exact_match=False, + sort_by=None, max_results=None, **kw): + """ See IUserEnumerationPlugin. + """ + def _enumerateUsers(id_tuple, exact_match, path): + user_info = [] + plugin_id = self.getId() + + id_list = [] + for id in id_tuple: + if SUPER_USER == id: + info = { 'id' : SUPER_USER + , 'login' : SUPER_USER + , 'pluginid' : plugin_id + } + user_info.append(info) + else: + if exact_match: + id_list.append(id) + else: + id_list.append('%%%s%%' % id) + + if id_list: + for user in self.getUserByLogin(tuple(id_list), exact_match=exact_match): + info = { 'id' : user.getReference() + , 'login' : user.getReference() + , 'pluginid' : plugin_id + } + + user_info.append(info) + + return tuple(user_info) + + _enumerateUsers = CachingMethod(_enumerateUsers, + id='ERP5UserManager_enumerateUsers', + cache_factory='erp5_content_short') + + if id is None: + id = login + if isinstance(id, list): + id = tuple(id) + elif not isinstance(id, tuple): + id = (id,) + return _enumerateUsers(id_tuple=id, + exact_match=exact_match, + path=self.getPhysicalPath()) + + def getUserByLogin(self, login, exact_match=True): + # Search the Catalog for login and return a list of person objects + # login can be a string or a list of strings + # (no docstring to prevent publishing) + if not login: + return [] + + portal = self.getPortalObject() + + def _getUserByLogin(login, exact_match): + # because we aren't logged in, we have to create our own + # SecurityManager to be able to access the Catalog + sm = getSecurityManager() + if sm.getUser().getId() != SUPER_USER: + newSecurityManager(self, self.getUser(SUPER_USER)) + + try: + try: + result = portal.portal_catalog.unrestrictedSearchResults( + select_expression='reference', + portal_type=self.portal_type_list, reference=login) + if len(result) != 1: # we won't proceed with groups + if len(result) > 1: # configuration is screwed + raise ConsistencyError, 'There is more than one Person whose \ + login is %s : %s' % (user_name, + repr([r.getObject() for r in catalog_result])) + else: # no person is linked to this user login + # this permit to get the module of the application + # the goal is to work with anonymous applications, even if + # they are not reindexed + module_id = self.REQUEST.get('anonymous_module', None) + if module_id: + module = getattr(portal, module_id, None) + if module is not None: + result = module._getOb(login[0], None) + if result is not None: + return [result.getPath(),] + else: + return [] + else: + return [] + except ConflictError: + raise + except: + LOG('ERP5Security', PROBLEM, 'getUserByLogin failed', error=sys.exc_info()) + # Here we must raise an exception to prevent callers from caching + # a result of a degraded situation. + # The kind of exception does not matter as long as it's catched by + # PAS and causes a lookup using another plugin or user folder. + # As PAS does not define explicitely such exception, we must use + # the _SWALLOWABLE_PLUGIN_EXCEPTIONS list. + raise _SWALLOWABLE_PLUGIN_EXCEPTIONS[0] + finally: + setSecurityManager(sm) + # XXX: Here, we filter catalog result list ALTHOUGH we did pass + # parameters to unrestrictedSearchResults to restrict result set. + # This is done because the following values can match person with + # reference "foo": + # "foo " because of MySQL (feature, PADSPACE collation): + # mysql> SELECT reference as r FROM catalog + # -> WHERE reference="foo "; + # +-----+ + # | r | + # +-----+ + # | foo | + # +-----+ + # 1 row in set (0.01 sec) + # " foo", "foo " and other padding variations because of + # ZSQLCatalog (feature ?): + # (Pdb) print portal.portal_catalog.unrestrictedSearchResults(\ + # portal_type="Person", reference=' foo ', src__=1) + # SELECT DISTINCT + # catalog.path, catalog.uid + # FROM + # catalog AS catalog + # WHERE + # 1 = 1 + # AND (((((catalog.portal_type = 'Person'))))) AND (((((catalog.reference = 'foo'))))) + # LIMIT 1000 + # "bar OR foo" because of ZSQLCatalog tokenizing searched sgtrings + # by default (feature). + return [x.path for x in result if (not exact_match) or x['reference'] in login] + _getUserByLogin = CachingMethod(_getUserByLogin, + id='ERP5UserManager_getUserByLogin', + cache_factory='erp5_content_short') + result = _getUserByLogin(login, exact_match) + return [portal.unrestrictedTraverse(x) for x in result] + +classImplements( EGOVUserManager + , IAuthenticationPlugin + , IUserEnumerationPlugin + ) + +InitializeClass(EGOVUserManager) diff --git a/product/ERP5eGovSecurity/__init__.py b/product/ERP5eGovSecurity/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bf712986b9b6eac38f26a6c1e0a3eb0c2b5b01aa --- /dev/null +++ b/product/ERP5eGovSecurity/__init__.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights +# Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this +# distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## +""" ERP5Security product initialization. +""" + +from copy import deepcopy + +from AccessControl.Permissions import manage_users as ManageUsers +from Products.PluggableAuthService.PluggableAuthService import registerMultiPlugin +from Products.PluggableAuthService.permissions import ManageGroups + +import EGOVUserManager +import EGOVGroupManager +from Products.ERP5Security import ERP5UserFactory +from Products.ERP5Security import ERP5RoleManager + + +def mergedLocalRoles(object): + """Returns a merging of object and its ancestors' + __ac_local_roles__.""" + # Modified to take into account _getAcquireLocalRoles + merged = {} + object = getattr(object, 'aq_inner', object) + while 1: + if getattr(object, '__ac_local_roles__', None) is not None: + roles = object.__ac_local_roles__ or {} + if callable(roles): roles = roles() + for k, v in roles.iteritems(): + merged.setdefault(k, []).extend(v) + # block acquisition + if getattr(object, '_getAcquireLocalRoles', None) is not None: + if not object._getAcquireLocalRoles() is not None: + break + if getattr(object, 'aq_parent', None) is not None: + object = object.aq_parent + object = getattr(object, 'aq_inner', object) + continue + if getattr(object, 'im_self', None) is not None: + object = object.im_self + object = getattr(object, 'aq_inner', object) + continue + break + + return deepcopy(merged) + +registerMultiPlugin(EGOVUserManager.EGOVUserManager.meta_type) +registerMultiPlugin(EGOVGroupManager.EGOVGroupManager.meta_type) + +def initialize(context): + + context.registerClass( EGOVUserManager.EGOVUserManager + , permission=ManageUsers + , constructors=( + EGOVUserManager.manage_addEGOVUserManagerForm, + EGOVUserManager.addEGOVUserManager, ) + , visibility=None + , icon='www/portal.gif' + ) + + context.registerClass( EGOVGroupManager.EGOVGroupManager + , permission=ManageGroups + , constructors=( + EGOVGroupManager.manage_addEGOVGroupManagerForm, + EGOVGroupManager.addEGOVGroupManager, ) + , visibility=None + , icon='www/portal.gif' + )