Commit cc4f6f3a authored by Vincent Pelletier's avatar Vincent Pelletier

ERP5Security: Add a PAS plugin for ERP5 Login authentication.

In addition to ERP5 Login-based authentication and enumeration support,
reserve special Zope users.
parent 430596e5
......@@ -38,6 +38,7 @@ from Products.ERP5.mixin.login_account_provider import LoginAccountProviderMixin
try:
from Products import PluggableAuthService
from Products.ERP5Security.ERP5UserManager import ERP5UserManager
from Products.ERP5Security.ERP5LoginUserManager import ERP5LoginUserManager
except ImportError:
PluggableAuthService = None
......@@ -128,7 +129,7 @@ class Person(Node, LoginAccountProviderMixin, EncryptedPasswordMixin):
- we want to apply a different permission
- we want to prevent duplicated user ids, but only when
PAS _AND_ ERP5UserManager are used
PAS _AND_ (ERP5UserManager or ERP5LoginUserManager) are used
"""
activate_kw = {}
portal = self.getPortalObject()
......@@ -143,7 +144,7 @@ class Person(Node, LoginAccountProviderMixin, EncryptedPasswordMixin):
plugin_list = acl_users.plugins.listPlugins(
PluggableAuthService.interfaces.plugins.IUserEnumerationPlugin)
for plugin_name, plugin_value in plugin_list:
if isinstance(plugin_value, ERP5UserManager):
if isinstance(plugin_value, (ERP5UserManager, ERP5LoginUserManager)):
user_list = acl_users.searchUsers(id=value,
exact_match=True)
if len(user_list) > 0:
......
##############################################################################
#
# Copyright (c) 2016 Nexedi SARL and Contributors. All Rights Reserved.
# Vincent Pelletier <vincent@nexedi.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.
#
##############################################################################
from functools import partial
from Products.ERP5Type.Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.AuthEncoding import pw_validate
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
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 DateTime import DateTime
from Products import ERP5Security
from AccessControl import SpecialUsers
# To prevent login thieves
SPECIAL_USER_NAME_SET = (
ERP5Security.SUPER_USER,
SpecialUsers.nobody.getUserName(),
SpecialUsers.system.getUserName(),
# Note: adding emergency_user is pointless as its login is variable, so no
# way to prevent another user from stealing its login.
)
manage_addERP5LoginUserManagerForm = PageTemplateFile(
'www/ERP5Security_addERP5LoginUserManager', globals(),
__name__='manage_addERP5LoginUserManagerForm' )
def addERP5LoginUserManager(dispatcher, id, title=None, RESPONSE=None):
""" Add a ERP5LoginUserManager to a Pluggable Auth Service. """
eum = ERP5LoginUserManager(id, title)
dispatcher._setObject(eum.getId(), eum)
if RESPONSE is not None:
RESPONSE.redirect(eum.absolute_url() + '/manage_main')
class ERP5LoginUserManager(BasePlugin):
""" PAS plugin for managing users in ERP5
"""
meta_type = 'ERP5 Login User Manager'
login_portal_type = 'ERP5 Login'
security = ClassSecurityInfo()
def __init__(self, id, title=None):
self._id = self.id = id
self.title = title
#
# IAuthenticationPlugin implementation
#
security.declarePrivate('authenticateCredentials')
def authenticateCredentials(self, credentials):
login_portal_type = credentials.get(
'login_portal_type',
self.login_portal_type,
)
if 'external_login' in credentials:
# External plugin: extractor plugin can validate credential validity.
# Our job is to locate the actual user and check related documents
# (assignments...).
check_password = False
login_value = self._getLoginValueFromLogin(
credentials.get('external_login'),
login_portal_type=login_portal_type,
)
elif 'login_relative_url' in credentials:
# Path-based login: extractor plugin can validate credential validity and
# directly locate the login document. Our job is to check related
# documents (assignments...).
check_password = False
login_value = self.getPortalObject().unrestrictedTraverse(
credentials.get("login_relative_url"),
)
else:
# Traditional login: find login document from credentials, check password
# and check related documents (assignments...).
check_password = True
login_value = self._getLoginValueFromLogin(
credentials.get('login'),
login_portal_type=login_portal_type,
)
if login_value is None:
return
user_value = login_value.getParentValue()
if not user_value.hasReference():
return
if user_value.getValidationState() == 'deleted':
return
now = DateTime()
for assignment in user_value.contentValues(portal_type="Assignment"):
if assignment.getValidationState() == "open" and (
not assignment.hasStartDate() or assignment.getStartDate() <= now
) and (
not assignment.hasStopDate() or assignment.getStopDate() >= now
):
break
else:
return
is_authentication_policy_enabled = self.getPortalObject().portal_preferences.isAuthenticationPolicyEnabled()
if check_password:
password = credentials.get('password')
if not password or not pw_validate(
login_value.getPassword(),
password,
):
if is_authentication_policy_enabled:
login_value.notifyLoginFailure()
return
if is_authentication_policy_enabled:
if login_value.isPasswordExpired():
login_value.notifyPasswordExpire()
return
if login_value.isLoginBlocked():
return
return (user_value.getReference(), login_value.getReference())
def _getLoginValueFromLogin(self, login, login_portal_type=None):
user_list = self.enumerateUsers(
login=login,
exact_match=True,
login_portal_type=login_portal_type,
)
if not user_list:
return
single_user, = user_list
single_login, = single_user['login_list']
path = single_login['path']
if path is None:
return
return self.getPortalObject().unrestrictedTraverse(path)
#
# IUserEnumerationPlugin implementation
#
security.declarePrivate('enumerateUsers')
def enumerateUsers(self, id=None, login=None, exact_match=False,
sort_by=None, max_results=None, login_portal_type=None, **kw):
""" See IUserEnumerationPlugin.
"""
portal = self.getPortalObject()
if login_portal_type is None:
login_portal_type = portal.getPortalLoginTypeList()
unrestrictedSearchResults = portal.portal_catalog.unrestrictedSearchResults
searchUser = lambda **kw: unrestrictedSearchResults(
select_list=('reference', ),
portal_type='Person',
**kw
).dictionaries()
searchLogin = lambda **kw: unrestrictedSearchResults(
select_list=('parent_uid', 'reference'),
validation_state='validated',
**kw
).dictionaries()
if login_portal_type is not None:
searchLogin = partial(searchLogin, portal_type=login_portal_type)
special_user_name_set = set()
if login is None:
# Only search by id if login is not given. Same logic as in
# PluggableAuthService.searchUsers.
if isinstance(id, str):
id = (id, )
if id:
if exact_match:
requested = set(id).__contains__
else:
requested = lambda x: True
user_list = [
x for x in searchUser(
reference={
'query': id,
'key': 'ExactMatch' if exact_match else 'Keyword',
},
limit=max_results,
)
if requested(x['reference'])
]
else:
user_list = []
login_dict = {}
if user_list:
for login in searchLogin(parent_uid=[x['uid'] for x in user_list]):
login_dict.setdefault(login['parent_uid'], []).append(login)
else:
if isinstance(login, str):
login = (login, )
login_list = []
for user_login in login:
if user_login in SPECIAL_USER_NAME_SET:
special_user_name_set.add(user_login)
else:
login_list.append(user_login)
login_dict = {}
if exact_match:
requested = set(login_list).__contains__
else:
requested = lambda x: True
if login_list:
for login in searchLogin(
reference={
'query': login_list,
'key': 'ExactMatch' if exact_match else 'Keyword',
},
limit=max_results,
):
if requested(login['reference']):
login_dict.setdefault(login['parent_uid'], []).append(login)
if login_dict:
user_list = searchUser(uid=list(login_dict))
else:
user_list = []
plugin_id = self.getId()
result = [
{
'id': user['reference'],
# Note: PAS forbids us from returning more than one entry per given id,
# so take any available login.
'login': login_dict.get(user['uid'], [{'reference': None}])[0]['reference'],
'pluginid': plugin_id,
# Extra properties, specific to ERP5
'path': user['path'],
'uid': user['uid'],
'login_list': [
{
'reference': login['reference'],
'path': login['path'],
'uid': login['uid'],
}
for login in login_dict.get(user['uid'], [])
],
}
for user in user_list if user['reference']
]
for special_user_name in special_user_name_set:
# Note: special users are a bastard design in Zope: they are expected to
# have a user name (aka, a login), but no id (aka, they do not exist as
# users). This is likely done to prevent them from having any local role
# (ownership or otherwise). In reality, they should have an id (they do
# exist, and user ids are some internal detail where it is easy to avoid
# such magic strings) and no login (because nobody should ever be able to
# log in as a special user, and logins are exposed to users (duh !) and
# hence magic values are impossible to avoid with ad-hoc code peppered
# everywhere). To avoid such ad-hoc code, this plugin will find magic
# users so code checking if a user login exists before allowing it to be
# reused, preventing misleading logins from being misused.
result.append({
'id': None,
'login': special_user_name,
'pluginid': plugin_id,
'path': None,
'uid': None,
'login_list': [
{
'reference': special_user_name,
'path': None,
'uid': None,
}
]
})
return tuple(result)
classImplements(ERP5LoginUserManager, IAuthenticationPlugin, IUserEnumerationPlugin)
InitializeClass(ERP5LoginUserManager)
......@@ -54,6 +54,7 @@ def mergedLocalRoles(object):
def initialize(context):
import ERP5UserManager
import ERP5LoginUserManager
import ERP5GroupManager
import ERP5RoleManager
import ERP5UserFactory
......@@ -65,6 +66,7 @@ def initialize(context):
import ERP5DumbHTTPExtractionPlugin
registerMultiPlugin(ERP5UserManager.ERP5UserManager.meta_type)
registerMultiPlugin(ERP5LoginUserManager.ERP5LoginUserManager.meta_type)
registerMultiPlugin(ERP5GroupManager.ERP5GroupManager.meta_type)
registerMultiPlugin(ERP5RoleManager.ERP5RoleManager.meta_type)
registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type)
......@@ -86,6 +88,15 @@ def initialize(context):
, icon='www/portal.gif'
)
context.registerClass( ERP5LoginUserManager.ERP5LoginUserManager
, permission=ManageUsers
, constructors=(
ERP5LoginUserManager.manage_addERP5LoginUserManagerForm,
ERP5LoginUserManager.addERP5LoginUserManager, )
, visibility=None
, icon='www/portal.gif'
)
context.registerClass( ERP5GroupManager.ERP5GroupManager
, permission=ManageGroups
, constructors=(
......
<h1 tal:replace="structure here/manage_page_header">Header</h1>
<h2 tal:define="form_title string:Add ERP5 User Manager"
tal:replace="structure here/manage_form_title">Form Title</h2>
<p class="form-help">
ERP5 User Manager applys the users managed in ERP5 person moduel
to the Pluggable Authentication Service
</p>
<form action="addERP5LoginUserManager" method="post">
<table cellspacing="0" cellpadding="2" border="0">
<tr>
<td align="left" valign="top">
<div class="form-label">
Id
</div>
</td>
<td align="left" valign="top">
<input type="text" name="id" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-optional">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" name="submit"
value=" Add " />
</div>
</td>
</tr>
</table>
</form>
<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment