Commit c957099d by Jérome Perrin

ERP5Security: make ERP5AccessTokenExtractionPlugin work with user ids

Because this was broken, we took the liberty to introduce a breaking
change to fix naming, now type based scripts are *_getUserValue and must
return a user document, with a getUserId method returning the user id.

Make this plugin also an IAuthenticationPlugin which does all the job of
returning the user id.
It does not really make sense to delegate this to default authenticator.
A side effect is that token can still authenticate users with no
assignments, since tokens are scriptable, if this is a requirement, it
can be implemented in scripts.

also update test:

 - plugin must be enabled for IAuthenticationPlugin
 - check complete authentication sequence, not just extraction
 - update scripts to new names
 - simplify transaction management
 - don't set self.person, it was not used anywhere
 - update _createPerson to reindex, as said in docstring
 - merge all tests in on test component
1 parent 9c2719b0
Showing 14 changed files with 39 additions and 460 deletions
......@@ -54,7 +54,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>AccessToken_getExternalLogin</string> </value>
<value> <string>AccessToken_getUserValue</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -14,7 +14,7 @@ if access_token_document.getValidationState() == 'validated':
agent_document = access_token_document.getAgentValue()
if agent_document is not None:
result = agent_document.Person_getUserId()
result = agent_document
comment = "Token usage accepted"
access_token_document.invalidate(comment=comment)
......
......@@ -54,7 +54,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>OneTimeRestrictedAccessToken_getExternalLogin</string> </value>
<value> <string>OneTimeRestrictedAccessToken_getUserValue</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -19,9 +19,9 @@ if access_token_document.getValidationState() == 'validated':
# use hmac.compare_digest and not string comparison to avoid timing attacks
if not hmac.compare_digest(access_token_document.getReference(), reference):
return None
agent_document = access_token_document.getAgentValue()
if agent_document is not None:
result = agent_document.Person_getUserId()
result = agent_document
return result
......@@ -54,7 +54,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>RestrictedAccessToken_getExternalLogin</string> </value>
<value> <string>RestrictedAccessToken_getUserValue</string> </value>
</item>
</dictionary>
</pickle>
......
......@@ -14,7 +14,7 @@
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testERP5AccessTokenAlarm</string> </value>
<value> <string>testERP5AccessToken</string> </value>
</item>
<item>
<key> <string>description</string> </key>
......@@ -24,7 +24,7 @@
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testERP5AccessTokenAlarm</string> </value>
<value> <string>test.erp5.testERP5AccessToken</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
......
# Copyright (c) 2002-2013 Nexedi SA and Contributors. All Rights Reserved.
from DateTime import DateTime
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
class TestERP5AccessTokenAlarm(ERP5TypeTestCase):
def getBusinessTemplateList(self):
return ('erp5_base',
'erp5_access_token')
def test_alarm_old_validated_restricted_access_token(self):
access_token = self.portal.access_token_module.newContent(
portal_type="One Time Restricted Access Token",
)
access_token.workflow_history['edit_workflow'] = [{
'comment':'Fake history',
'error_message': '',
'actor': 'ERP5TypeTestCase',
'state': 'current',
'time': DateTime('2012/11/15 11:11'),
'action': 'foo_action'
}]
self.portal.portal_workflow._jumpToStateFor(access_token, 'validated')
self.tic()
self.portal.portal_alarms.\
erp5_garbage_collect_one_time_restricted_access_token.activeSense()
self.tic()
self.assertEqual('invalidated', access_token.getValidationState())
self.assertEqual(
'Unused for 1 day.',
access_token.workflow_history['validation_workflow'][-1]['comment'])
def test_alarm_recent_validated_restricted_access_token(self):
access_token = self.portal.access_token_module.newContent(
portal_type="One Time Restricted Access Token",
)
self.portal.portal_workflow._jumpToStateFor(access_token, 'validated')
self.tic()
self.portal.portal_alarms.\
erp5_garbage_collect_one_time_restricted_access_token.activeSense()
self.tic()
self.assertEqual('validated', access_token.getValidationState())
def test_alarm_old_non_validated_restricted_access_token(self):
access_token = self.portal.access_token_module.newContent(
portal_type="One Time Restricted Access Token",
)
access_token.workflow_history['edit_workflow'] = [{
'comment':'Fake history',
'error_message': '',
'actor': 'ERP5TypeTestCase',
'state': 'current',
'time': DateTime('2012/11/15 11:11'),
'action': 'foo_action'
}]
self.tic()
self.portal.portal_alarms.\
erp5_garbage_collect_one_time_restricted_access_token.activeSense()
self.tic()
self.assertEqual('draft', access_token.getValidationState())
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testERP5AccessTokenSkins</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testERP5AccessTokenSkins</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2015 Nexedi SA and Contributors. All Rights Reserved.
# Tristan Cavelier <tristan.cavelier@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 Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from ZPublisher.HTTPRequest import HTTPRequest
from ZPublisher.HTTPResponse import HTTPResponse
from Products.ERP5Security.ERP5DumbHTTPExtractionPlugin import ERP5DumbHTTPExtractionPlugin
import base64
import transaction
import StringIO
class TestERP5DumbHTTPExtractionPlugin(ERP5TypeTestCase):
test_id = 'test_erp5_dumb_http_extraction'
def getBusinessTemplateList(self):
return ('erp5_base',)
def generateNewId(self):
return str(self.portal.portal_ids.generateNewId(
id_group=('erp5_dumb_http_test_id')))
def afterSetUp(self):
"""
This is ran before anything, used to set the environment
"""
self.portal = self.getPortalObject()
self.new_id = self.generateNewId()
self._setupDumbHTTPExtraction()
transaction.commit()
self.tic()
def do_fake_request(self, request_method, headers=None):
if headers is None:
headers = {}
__version__ = "0.1"
env={}
env['SERVER_NAME']='bobo.server'
env['SERVER_PORT']='80'
env['REQUEST_METHOD']=request_method
env['REMOTE_ADDR']='204.183.226.81 '
env['REMOTE_HOST']='bobo.remote.host'
env['HTTP_USER_AGENT']='Bobo/%s' % __version__
env['HTTP_HOST']='127.0.0.1'
env['SERVER_SOFTWARE']='Bobo/%s' % __version__
env['SERVER_PROTOCOL']='HTTP/1.0 '
env['HTTP_ACCEPT']='image/gif, image/x-xbitmap, image/jpeg, */* '
env['SERVER_HOSTNAME']='bobo.server.host'
env['GATEWAY_INTERFACE']='CGI/1.1 '
env['SCRIPT_NAME']='Main'
env.update(headers)
return HTTPRequest(StringIO.StringIO(), env, HTTPResponse())
def _setupDumbHTTPExtraction(self):
pas = self.portal.acl_users
access_extraction_list = [q for q in pas.objectValues() \
if q.meta_type == 'ERP5 Dumb HTTP Extraction Plugin']
if len(access_extraction_list) == 0:
dispacher = pas.manage_addProduct['ERP5Security']
dispacher.addERP5DumbHTTPExtractionPlugin(self.test_id)
getattr(pas, self.test_id).manage_activateInterfaces(
('IExtractionPlugin',))
elif len(access_extraction_list) == 1:
self.test_id = access_extraction_list[0].getId()
elif len(access_extraction_list) > 1:
raise ValueError
transaction.commit()
def _createPerson(self, new_id, password=None):
"""Creates a person in person module, and returns the object, after
indexing is done. """
person_module = self.getPersonModule()
person = person_module.newContent(portal_type='Person',
reference='TESTP-' + new_id)
if password:
person.setPassword(password)
person.newContent(portal_type = 'Assignment').open()
transaction.commit()
return person
def test_working_authentication(self):
self._createPerson(self.new_id, "test")
request = self.do_fake_request("GET", {"HTTP_AUTHORIZATION": "Basic " + base64.b64encode("%s:test" % self.new_id)})
ret = ERP5DumbHTTPExtractionPlugin("default_extraction").extractCredentials(request)
self.assertEqual(ret, {'login': self.new_id, 'password': 'test', 'remote_host': 'bobo.remote.host', 'remote_address': '204.183.226.81 '})
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>testERP5DumbHTTPExtractionPlugin</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testERP5DumbHTTPExtractionPlugin</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
test.erp5.testERP5AccessTokenAlarm
test.erp5.testERP5AccessTokenSkins
test.erp5.testERP5DumbHTTPExtractionPlugin
\ No newline at end of file
test.erp5.testERP5AccessToken
\ No newline at end of file
......@@ -59,35 +59,38 @@ class ERP5AccessTokenExtractionPlugin(BasePlugin):
#ILoginPasswordHostExtractionPlugin#
####################################
security.declarePrivate('extractCredentials')
@UnrestrictedMethod
def extractCredentials(self, request):
""" Extract CookieHash credentials from the request header. """
""" Extract credentials from the request header. """
creds = {}
# XXX Extract from HTTP Header, URL parameter are hardcoded.
# More flexible way would be to configure on the portal type level
token = request.getHeader("X-ACCESS-TOKEN", None)
if token is None:
token = request.form.get("access_token", None)
if token is not None:
# Extract token from HTTP Header
token = request.getHeader("X-ACCESS-TOKEN", request.form.get("access_token", None))
if token:
creds['erp5_access_token_id'] = token
creds['remote_host'] = request.get('REMOTE_HOST', '')
try:
creds['remote_address'] = request.getClientAddr()
except AttributeError:
creds['remote_address'] = request.get('REMOTE_ADDR', '')
return creds
#######################
#IAuthenticationPlugin#
#######################
security.declarePrivate('authenticateCredentials')
@UnrestrictedMethod
def authenticateCredentials(self, credentials):
""" Map credentials to a user ID. """
if 'erp5_access_token_id' in credentials:
erp5_access_token_id = credentials['erp5_access_token_id']
token_document = self.getPortalObject().access_token_module.\
_getOb(token, None)
# Access Token should be validated
# Check restricted access of URL
# Extract login information
_getOb(erp5_access_token_id, None)
if token_document is not None:
external_login = None
method = token_document._getTypeBasedMethod('getExternalLogin')
method = token_document._getTypeBasedMethod('getUserValue')
if method is not None:
external_login = method()
if external_login is not None:
creds['external_login'] = external_login
creds['remote_host'] = request.get('REMOTE_HOST', '')
try:
creds['remote_address'] = request.getClientAddr()
except AttributeError:
creds['remote_address'] = request.get('REMOTE_ADDR', '')
return creds
user_value = method()
if user_value is not None:
return (user_value.getUserId(), token_document.getRelativeUrl())
#Form for new plugin in ZMI
manage_addERP5AccessTokenExtractionPluginForm = PageTemplateFile(
......@@ -109,6 +112,7 @@ def addERP5AccessTokenExtractionPlugin(dispatcher, id, title=None, REQUEST=None)
#List implementation of class
classImplements(ERP5AccessTokenExtractionPlugin,
plugins.ILoginPasswordHostExtractionPlugin
)
plugins.ILoginPasswordHostExtractionPlugin,
plugins.IAuthenticationPlugin,
)
InitializeClass(ERP5AccessTokenExtractionPlugin)
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!