Commit d3e47c4d authored by Vincent Pelletier's avatar Vincent Pelletier Committed by Kazuhiko Shiozaki

ERP5Security: Add a PAS plugin for ERP5 Login authentication.

Simplified rework of ERP5 User Manager without module-level callables and
implementing more of PAS API.
Implement IAuthenticationPlugin API better, so PAS API becomes usable for
ERP5-based authentication.
parent 5d5eea30
##############################################################################
#
# 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.
#
##############################################################################
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
# This user is used to bypass all security checks.
SUPER_USER = '__erp5security-=__'
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 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):
# Forbidden the usage of the super user.
if login == SUPER_USER:
return 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']
return self.getPortalObject().unrestrictedTraverse(
single_login['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.
"""
unrestrictedSearchResults = self.getPortalObject(
).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)
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, )
id_list = []
has_super_user = False
for user_id in id:
if user_id == SUPER_USER:
  • ipdb> pp self.portal.acl_users.searchUsers(id=SUPER_USER)
    ({'id': '__erp5security-=__',
      'login': '__erp5security-=__',
    ...

    But we have == SUPER_USER check only for user_id, i.e. searchUsers(login=SUPER_USER) returns nothing. So we can still create ERP5 Login whose reference is SUPER_USER. Is it fine ?

  • So we can still create ERP5 Login whose reference is SUPER_USER. Is it fine ?

    Yes, all logins should be allowed. Which of course means: no login should magically grant any special permission.

    Edited by Vincent Pelletier
Please register or sign in to reply
has_super_user = True
elif user_id:
id_list.append(user_id)
if id_list:
if exact_match:
requested = set(id_list).__contains__
else:
requested = lambda x: True
user_list = [
x for x in searchUser(
reference={
'query': id_list,
'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)
if has_super_user:
user_list.append({'uid': None, 'reference': SUPER_USER})
login_dict[None] = [{
'reference': SUPER_USER,
'path': None,
'uid': None,
}]
  • ipdb> pp self.portal.acl_users.searchUsers(id=SUPER_USER)
    ({'id': '__erp5security-=__',
      'login': '__erp5security-=__',
      'login_list': [{'path': None,
                      'reference': '__erp5security-=__',
                      'uid': None}],
      'path': None,
      'pluginid': 'erp5_login_users',
      'principal_type': 'user',
      'title': '__erp5security-=__',
      'userid': '__erp5security-=__'},)

    This login_list entry is required ? Can it be 'login_list': [] instead ?

  • Right, this would be more consistent with the new way special accounts are handled.

Please register or sign in to reply
else:
if isinstance(login, str):
login = (login, )
login_dict = {}
if exact_match:
requested = set(login).__contains__
else:
requested = lambda x: True
if login:
for login in searchLogin(
reference={
'query': login,
'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()
return tuple([
{
'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'], [None])[0]['reference'],
'pluginid': plugin_id,
# Extra properties, specific to ERP5
'path': user['path'],
'login_list': [
{
'reference': login['reference'],
'path': login['path'],
'uid': login['uid'],
}
for login in login_dict.get(user['uid'], [])
],
}
for user in user_list
])
classImplements(ERP5LoginUserManager, IAuthenticationPlugin, IUserEnumerationPlugin)
InitializeClass(ERP5LoginUserManager)
......@@ -22,6 +22,7 @@ from Products.PluggableAuthService.PluggableAuthService import registerMultiPlug
from Products.PluggableAuthService.permissions import ManageGroups
import ERP5UserManager
import ERP5LoginUserManager
import ERP5GroupManager
import ERP5RoleManager
import ERP5UserFactory
......@@ -61,6 +62,7 @@ def mergedLocalRoles(object):
return deepcopy(merged)
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)
......@@ -83,6 +85,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