Commit b45f325f authored by Ivan Tyagov's avatar Ivan Tyagov

Add key authentication plugin (preliminary work of FX) and extend tests to...

Add key authentication plugin (preliminary work of FX) and extend tests to basic cover it. This is a work in progress and needs improvements (see XXX) and better test coverage.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@34434 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent f8179b18
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2002 Nexedi SARL and Contributors. All Rights Reserved.
# Francois-Xavier Algrain <fxalgrain@tiolive.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 base64 import encodestring, decodestring
from urllib import quote, unquote
from DateTime import DateTime
from zLOG import LOG, PROBLEM
from Globals import InitializeClass
try:
from zope.interface import Interface
except ImportError:
from Products.PluggableAuthService.utils import Interface
from AccessControl import ClassSecurityInfo
from AccessControl.SecurityManagement import getSecurityManager,\
newSecurityManager,\
setSecurityManager
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PluggableAuthService.interfaces import plugins
from Products.PluggableAuthService.utils import classImplements
from Products.PluggableAuthService.permissions import ManageUsers
from Products.PluggableAuthService.plugins.BasePlugin import BasePlugin
from Products.PluggableAuthService.plugins.CookieAuthHelper import CookieAuthHelper
from Products.ERP5Type.Cache import CachingMethod
from Products.ERP5Security.ERP5UserManager import ERP5UserManager,\
SUPER_USER,\
_AuthenticationFailure
class ILoginEncryptionPlugin(Interface):
"""Contract for possible ERP5 Key Auth Plugin"""
def encrypt(self,login):
"""Encrypt the login"""
def decrypt(self,crypted_login):
"""Decrypt string and return the login"""
#Form for new plugin in ZMI
manage_addERP5KeyAuthPluginForm = PageTemplateFile(
'www/ERP5Security_addERP5KeyAuthPlugin', globals(),
__name__='manage_addERP5KeyAuthPluginForm')
def addERP5KeyAuthPlugin(dispatcher, id,title=None,\
encryption_key='',cookie_name='',\
default_cookie_name='',REQUEST=None):
""" Add a ERP5KeyAuthPlugin to a Pluggable Auth Service. """
plugin = ERP5KeyAuthPlugin( id, title,encryption_key,cookie_name,default_cookie_name)
dispatcher._setObject(plugin.getId(), plugin)
if REQUEST is not None:
REQUEST['RESPONSE'].redirect(
'%s/manage_workspace'
'?manage_tabs_message='
'ERP5KeyAuthPlugin+added.'
% dispatcher.absolute_url())
class ERP5KeyAuthPlugin(ERP5UserManager, CookieAuthHelper):
"""
Key authentification PAS plugin which support key authentication in URL.
<ERP5_Root>/web_page_module/1/view?__ac_key=207221200213146153166
where value of __ac_key contains (encrypted):
- proxied (i.e. granting user) username
- PAS plugin encryption key
XXX: improve encrypt & decrypt part to use PAS encryption_key with a true
python encryption library (reuse of public / private key architecture)!
"""
meta_type = "ERP5 Key Authentication"
login_path = 'login_form'
security = ClassSecurityInfo()
block_length = 3
cookie_name = "__ac_key"
default_cookie_name = "__ac"
encryption_key = ''
encrypted_key = ''
manage_options = ( ( { 'label': 'Edit',
'action': 'manage_editERP5KeyAuthPluginForm', }
,
)
+ BasePlugin.manage_options[:]
#+ CookieAuthHelper.manage_options[:] //don't need folder option today
)
_properties = ( ( { 'id':'default_cookie_name',
'type':'string',
'mode':'w',
'label':'Default Cookie Name'
},
)
+ BasePlugin._properties[:]
+ CookieAuthHelper._properties[:]
)
def __init__(self, id, title=None, encryption_key='', cookie_name='', default_cookie_name=''):
#Check parameters
if cookie_name is None or cookie_name == '':
cookie_name = id
if encryption_key is None or encryption_key == '':
encryption_key = id
if "__ac_key" in [cookie_name, default_cookie_name]:
raise ValueError, "Cookie name must be different of __ac_key"
#Register value
self._setId(id)
self.title = title
self.cookie_name = cookie_name
self.default_cookie_name = default_cookie_name
self.encryption_key = encryption_key
self.encrypted_key = self.transformKey(self.encryption_key);
################################
# ILoginEncryptionPlugin #
################################
security.declarePrivate('transformKey')
def transformKey(self,key):
"""Transform the key to number for encryption"""
encrypt_key = []
for letter in key:
encrypt_key.append(ord(letter))
return encrypt_key
security.declarePublic('encrypt')
def encrypt(self,login):
"""Encrypt the login"""
crypted_login = ''
key_length = len(self.encrypted_key)
for i in range(0,len(login)):
delta = i % key_length
crypted_letter = str(ord(login[i]) + self.encrypted_key[delta])
#ord is the inverse of chr() for 8-bit (1111 1111 = 256)
#so crypted_letter max id 512
#we ajust lenght to be able to decrypt by block
crypted_letter = crypted_letter.rjust(self.block_length, '0')
crypted_login += crypted_letter
return crypted_login
security.declarePrivate('decrypt')
def decrypt(self, crypted_login):
"""Decrypt string and return the login"""
login = ''
#check lenght of the string
clogin_length = len(crypted_login)
if clogin_length % self.block_length != 0:
raise ValueError, "Lenght is not good"
#decrypt block per block
position = 0
key_length = len(self.encrypted_key)
for block in range(0, clogin_length, self.block_length):
delta = position % key_length
crypted_letter = crypted_login[block:block + self.block_length]
crypted_letter = int(crypted_letter) - self.encrypted_key[delta]
letter = chr(crypted_letter)
login += letter
position += 1
return login
####################################
#ILoginPasswordHostExtractionPlugin#
####################################
security.declarePrivate('extractCredentials')
def extractCredentials(self, request):
""" Extract credentials from cookie or 'request'. """
try:
creds = {}
#Search __ac_key
key = request.get('__ac_key', None)
if key is not None:
creds['key'] = key
#Save this in cookie
self.updateCredentials(request,request["RESPONSE"],None,None)
else:
# Look in the request for the names coming from the login form
#It's default method
login_pw = request._authUserPW()
if login_pw is not None:
name, password = login_pw
creds[ 'login' ] = name
creds[ 'password' ] = password
#Save this in cookie
self.updateCredentials(request,request["RESPONSE"],name,password)
else:
#search in cookies
cookie = request.get(self.cookie_name, None)
if cookie is not None:
#Cookie is found
cookie_val = unquote(cookie)
creds['key'] = cookie_val
else:
#Default cookie if needed
default_cookie = request.get(self.default_cookie_name, None)
if default_cookie is not None:
#Cookie is found
cookie_val = decodestring(unquote(default_cookie))
if cookie_val is not None:
login, password = cookie_val.split(':')
creds['login'] = login
creds['password'] = password
#Complete credential with some informations
if creds:
creds['remote_host'] = request.get('REMOTE_HOST', '')
try:
creds['remote_address'] = request.getClientAddr()
except AttributeError:
creds['remote_address'] = request.get('REMOTE_ADDR', '')
except StandardError,e:
#Log standard error to check error
LOG('ERP5KeyAuthPlugin.extractCredentials', PROBLEM, str(e))
return creds
################################
# ICredentialsUpdatePlugin #
################################
security.declarePrivate('updateCredentials')
def updateCredentials(self, request, response, login, new_password):
""" Respond to change of credentials"""
#Update credential for key auth or standard of.
#Remove conflict between both systems
cookie_val = request.get("__ac_key",None)
if cookie_val is not None:
#expires = (DateTime() + 365).toZone('GMT').rfc822()
cookie_val = cookie_val.rstrip()
response.setCookie(self.cookie_name, quote(cookie_val), path='/')#,expires=expires)
response.expireCookie(self.default_cookie_name, path='/')
elif login is not None and new_password is not None:
cookie_val = encodestring('%s:%s' % (login, new_password))
cookie_val = cookie_val.rstrip()
response.setCookie(self.default_cookie_name, quote(cookie_val), path='/')
response.expireCookie(self.cookie_name, path='/')
################################
# ICredentialsResetPlugin #
################################
security.declarePrivate('resetCredentials')
def resetCredentials(self, request, response):
"""Expire cookies of authentification """
response.expireCookie(self.cookie_name, path='/')
response.expireCookie(self.default_cookie_name, path='/')
################################
# IAuthenticationPlugin #
################################
security.declarePrivate('authenticateCredentials')
def authenticateCredentials( self, credentials ):
"""Authentificate with credentials"""
key = credentials.get('key', None)
if key != None:
login = self.decrypt(key)
# Forbidden the usage of the super user.
if login == SUPER_USER:
return None
#Function to allow cache
def _authenticateCredentials(login):
if not login:
return None
#Search the user by his login
user_list = self.getUserByLogin(login)
if len(user_list) != 1:
raise _AuthenticationFailure()
user = user_list[0]
#We need to be super_user
sm = getSecurityManager()
if sm.getUser().getId() != SUPER_USER:
newSecurityManager(self, self.getUser(SUPER_USER))
try:
portal = self.getPortalObject()
# get assignment list
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)
# validate
if len(valid_assignment_list) > 0:
return (login,login)
finally:
setSecurityManager(sm)
raise _AuthenticationFailure()
#Cache Method for best performance
_authenticateCredentials = CachingMethod(_authenticateCredentials,
id='ERP5KeyAuthPlugin_authenticateCredentials',
cache_factory='erp5_content_short')
try:
return _authenticateCredentials(
login=login)
except _AuthenticationFailure:
return None
except StandardError,e:
#Log standard error
LOG('ERP5KeyAuthPlugin.authenticateCredentials', PROBLEM, str(e))
return None
################################
# Properties for ZMI managment #
################################
#'Edit' option form
manage_editERP5KeyAuthPluginForm = PageTemplateFile(
'www/ERP5Security_editERP5KeyAuthPlugin',
globals(),
__name__='manage_editERP5KeyAuthPluginForm' )
security.declareProtected( ManageUsers, 'manage_editKeyAuthPlugin' )
def manage_editKeyAuthPlugin(self, encryption_key,cookie_name,default_cookie_name, RESPONSE=None):
"""Edit the object"""
error_message = ''
#Test paramaeters
if "__ac_key" in [cookie_name, default_cookie_name]:
raise ValueError, "Cookie name must be different of __ac_key"
#Save key
if encryption_key == '' or encryption_key is None:
error_message += 'Invalid key value '
else:
self.encryption_key = encryption_key
self.encrypted_key = self.transformKey(self.encryption_key);
#Save cookie name
if cookie_name == '' or cookie_name is None:
error_message += 'Invalid cookie name '
else:
self.cookie_name = cookie_name
#Save default_cookie_name
if default_cookie_name == '' or default_cookie_name is None:
error_message += 'Invalid default cookie name '
else:
self.default_cookie_name = default_cookie_name
#Redirect
if RESPONSE is not None:
if error_message != '':
self.REQUEST.form['manage_tabs_message'] = error_message
return self.manage_editERP5KeyAuthPluginForm(RESPONSE)
else:
message = "Updated"
RESPONSE.redirect( '%s/manage_editERP5KeyAuthPluginForm'
'?manage_tabs_message=%s'
% ( self.absolute_url(), message )
)
#List implementation of class
classImplements(ERP5KeyAuthPlugin,
ILoginEncryptionPlugin,
plugins.IAuthenticationPlugin,
plugins.ILoginPasswordHostExtractionPlugin,
plugins.ICredentialsResetPlugin,
plugins.ICredentialsUpdatePlugin)
InitializeClass(ERP5KeyAuthPlugin)
\ No newline at end of file
......@@ -25,6 +25,7 @@ import ERP5UserManager
import ERP5GroupManager
import ERP5RoleManager
import ERP5UserFactory
import ERP5KeyAuthPlugin
def mergedLocalRoles(object):
"""Returns a merging of object and its ancestors'
......@@ -58,6 +59,7 @@ registerMultiPlugin(ERP5UserManager.ERP5UserManager.meta_type)
registerMultiPlugin(ERP5GroupManager.ERP5GroupManager.meta_type)
registerMultiPlugin(ERP5RoleManager.ERP5RoleManager.meta_type)
registerMultiPlugin(ERP5UserFactory.ERP5UserFactory.meta_type)
registerMultiPlugin(ERP5KeyAuthPlugin.ERP5KeyAuthPlugin.meta_type)
def initialize(context):
......@@ -97,3 +99,12 @@ def initialize(context):
, icon='www/portal.gif'
)
context.registerClass( ERP5KeyAuthPlugin.ERP5KeyAuthPlugin
, permission=ManageUsers
, constructors=(
ERP5KeyAuthPlugin.manage_addERP5KeyAuthPluginForm,
ERP5KeyAuthPlugin.addERP5KeyAuthPlugin, )
, visibility=None
, icon='www/portal.gif'
)
......@@ -619,6 +619,56 @@ class TestLocalRoleManagement(ERP5TypeTestCase):
basic='guest:guest')
self.assertEqual(response.getStatus(), 401)
def testKeyAuthentication(self):
"""
Make sure that we can grant security using a key.
"""
# add key authentication PAS plugin
portal = self.portal
uf = portal.acl_users
uf.manage_addProduct['ERP5Security'].addERP5KeyAuthPlugin(
id="erp5_auth_key", \
title="ERP5 Auth key",\
encryption_key='fdgfhkfjhltylutyu',
cookie_name='__key',\
default_cookie_name='__ac')
erp5_auth_key_plugin = getattr(uf, "erp5_auth_key")
erp5_auth_key_plugin.manage_activateInterfaces(
interfaces=['IExtractionPlugin',
'IAuthenticationPlugin',
'ICredentialsUpdatePlugin',
'ICredentialsResetPlugin'])
self.stepTic()
reference = 'UserReferenceTextWhichShouldBeHardToGeneratedInAnyHumanOrComputerLanguage'
loginable_person = self.getPersonModule().newContent(portal_type='Person',
reference=reference,
password='guest')
assignment = loginable_person.newContent(portal_type='Assignment',
function='another_subcat')
assignment.open()
self.stepTic()
# encrypt & decrypt works
key = erp5_auth_key_plugin.encrypt(reference)
self.assertNotEquals(reference, key)
self.assertEquals(reference, erp5_auth_key_plugin.decrypt(key))
base_url = '%s/view' %portal.absolute_url(relative=1)
# without key we are Anonymous User so we should be redirected with proper HTML
# status code to login_form
response = self.publish(base_url)
self.assertEqual(response.getStatus(), 302)
self.assertTrue('location' in response.headers.keys())
self.assertTrue(response.headers['location'].endswith('login_form'))
# view front page we should be logged in if we use authentication key
response = self.publish('%s?__ac_key=%s' %(base_url, key))
self.assertEqual(response.getStatus(), 200)
self.assertTrue(reference in response.getBody())
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestUserManagement))
......
<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
<h2 tal:define="form_title string:Add ERP5 Key Authentication PAS"
tal:replace="structure context/manage_form_title">FORM TITLE</h2>
<p class="form-help">Please input the configuration</p>
<form action="addERP5KeyAuthPlugin" 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-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Encryption Key
</div>
</td>
<td align="left" valign="top">
<input type="text" name="encryption_key" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Cookie Name
</div>
</td>
<td align="left" valign="top">
<input type="text" name="cookie_name"
value="__key" size="40" />
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Default Cookie Name
</div>
</td>
<td align="left" valign="top">
<input type="text" name="default_cookie_name"
value="__ac" size="40" />
</td>
</tr>
<tr>
<td colspan="2"> <input type="submit" value="add plugin"/>
</td>
</tr>
</table>
</form>
<h1 tal:replace="structure context/manage_page_footer">PAGE FOOTER</h1>
<h1 tal:replace="structure context/manage_page_header">PAGE HEADER</h1>
<h2 tal:replace="structure here/manage_tabs"> TABS </h2>
<h2 tal:define="form_title string:Edit ERP5 Key Authentification Plugin"
tal:replace="structure context/manage_form_title">FORM TITLE</h2>
<p class="form-help">Please input the configuration for the radius host</p>
<form action="manage_editKeyAuthPlugin" method="POST">
<table tal:define="encryption_key request/encryption_key|context/encryption_key|string:;
default_cookie_name request/default_cookie_name|context/default_cookie_name|string:;
cookie_name request/cookie_name|context/cookie_name|string:;">
<tr>
<td> Encryption Key </td>
<td>
<input type="text" name="encryption_key" value=""
tal:attributes="value encryption_key;" />
</td>
</tr>
<tr>
<td> Cookie Name </td>
<td>
<input type="text" name="cookie_name" value=""
tal:attributes="value cookie_name;" />
</td>
</tr>
<tr>
<td> Default Cookie Name </td>
<td>
<input type="text" name="default_cookie_name" value=""
tal:attributes="value default_cookie_name;" />
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="save"/>
</td>
</tr>
</table>
</form>
<h1 tal:replace="structure context/manage_page_footer">PAGE 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