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 ...@@ -22,6 +22,7 @@ from Products.PluggableAuthService.PluggableAuthService import registerMultiPlug
from Products.PluggableAuthService.permissions import ManageGroups from Products.PluggableAuthService.permissions import ManageGroups
import ERP5UserManager import ERP5UserManager
import ERP5LoginUserManager
import ERP5GroupManager import ERP5GroupManager
import ERP5RoleManager import ERP5RoleManager
import ERP5UserFactory import ERP5UserFactory
...@@ -61,6 +62,7 @@ def mergedLocalRoles(object): ...@@ -61,6 +62,7 @@ def mergedLocalRoles(object):
return deepcopy(merged) return deepcopy(merged)
registerMultiPlugin(ERP5UserManager.ERP5UserManager.meta_type) registerMultiPlugin(ERP5UserManager.ERP5UserManager.meta_type)
registerMultiPlugin(ERP5LoginUserManager.ERP5LoginUserManager.meta_type)
registerMultiPlugin(ERP5GroupManager.ERP5GroupManager.meta_type) registerMultiPlugin(ERP5GroupManager.ERP5GroupManager.meta_type)
registerMultiPlugin(ERP5RoleManager.ERP5RoleManager.meta_type) registerMultiPlugin(ERP5RoleManager.ERP5RoleManager.meta_type)
registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type) registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type)
...@@ -83,6 +85,15 @@ def initialize(context): ...@@ -83,6 +85,15 @@ def initialize(context):
, icon='www/portal.gif' , 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 context.registerClass( ERP5GroupManager.ERP5GroupManager
, permission=ManageGroups , permission=ManageGroups
, constructors=( , 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