Commit 601e46e9 by Jérome Perrin

Fix AccessToken login with ERP5 Login

Since the introduction of ERP5 Login, authentication by Access Token is broken, and it is only working if `erp5_login.getReference() == person.getUserId()`

The scriptable part of access token changed, now scripts must return a user object - on which the plugin will call `getUserId` (it was not clear what they should return before, maybe login, but they should return a user id, not a login, as the token plays the same role as a login). To make it clear and to intentionally break compatibility as this is now something different, these scripts have been renamed to be `getUserValue` type based methods.

/reviewed-on !838
2 parents d8999426 71aac680
Showing 22 changed files with 213 additions and 617 deletions
......@@ -6,4 +6,7 @@
<item>Reference</item>
<item>Url</item>
</portal_type>
<portal_type id="Template Tool">
<item>TemplateToolERP5AccessTokenExtractionPluginConstraint</item>
</portal_type>
</property_sheet_list>
\ No newline at end of file
......@@ -2,122 +2,65 @@
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
<global name="Property Sheet" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<key> <string>_count</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>
<key> <string>_mt_index</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/>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<key> <string>_tree</string> </key>
<value>
<tuple/>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<key> <string>description</string> </key>
<value>
<tuple/>
<none/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
<key> <string>id</string> </key>
<value> <string>TemplateToolERP5AccessTokenExtractionPluginConstraint</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
<key> <string>portal_type</string> </key>
<value> <string>Property Sheet</string> </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>
<global name="Length" module="BTrees.Length"/>
</pickle>
<pickle> <int>0</int> </pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
<global name="OOBTree" module="BTrees.OOBTree"/>
</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>
<none/>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
<global name="OOBTree" module="BTrees.OOBTree"/>
</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>
<none/>
</pickle>
</record>
</ZopeData>
......@@ -2,61 +2,47 @@
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test Component" module="erp5.portal_type"/>
<global name="Script Constraint" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<key> <string>_identity_criterion</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>
<key> <string>_range_criterion</string> </key>
<value>
<none/>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</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>
<key> <string>categories</string> </key>
<value>
<none/>
<tuple>
<string>constraint_type/post_upgrade</string>
</tuple>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<key> <string>description</string> </key>
<value>
<tuple/>
<none/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
<key> <string>id</string> </key>
<value> <string>ERP5AccessTokenExtractionPlugin_existence_constraint</string> </value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
<key> <string>portal_type</string> </key>
<value> <string>Script Constraint</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
<key> <string>script_id</string> </key>
<value> <string>TemplateTool_checkERP5AccessTokenExtractionPluginExistenceConsistency</string> </value>
</item>
</dictionary>
</pickle>
......@@ -85,39 +71,10 @@
<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>
<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>
......@@ -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,32 @@ 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()
if agent_document.getPortalType() == 'Person':
# if this is a token for a person, only make accept if person has valid
# assignments and a validated login (for compatibility with login/password
# authentication)
if agent_document.getValidationState() == 'deleted':
return None
now = DateTime()
for assignment in agent_document.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 None
user, = context.getPortalObject().acl_users.searchUsers(
exact_match=True,
id=agent_document.Person_getUserId())
if not user['login_list']:
return None
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>
......
alpha = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
random_id = ''
for i in range(0,128):
for _ in range(0, 128):
random_id += random.choice(alpha)
# Define Reference from ID provided by portal_ids
......
acl_users = context.getPortalObject().acl_users
token_extraction_id = "erp5_access_token_plugin"
access_token_plugin_list = [
plugin for plugin in acl_users.objectValues()
if plugin.meta_type == 'ERP5 Access Token Extraction Plugin']
if len(access_token_plugin_list) > 1:
return ["More than one plugin found: %s" % access_token_plugin_list]
error_list = []
if not access_token_plugin_list:
# A dumb http extraction plugin is required as fallback if we use an access token
# since https://github.com/Nexedi/erp5/commit/0bee523da0075c6efe3c06296dddd01d9dd5045a
# we enable it automatically at site creation, but for compatibility with old instances
# make sure it is created if needed
if 'erp5_dumb_http_extraction' not in acl_users.objectIds():
error_list.append("erp5_dumb_http_extraction is missing")
if fixit:
dispacher = acl_users.manage_addProduct['ERP5Security']
dispacher.addERP5DumbHTTPExtractionPlugin('erp5_dumb_http_extraction')
acl_users.erp5_dumb_http_extraction.manage_activateInterfaces(('IExtractionPlugin', ))
error_list.append("erp5_access_token_plugin is missing")
if fixit:
dispacher = acl_users.manage_addProduct['ERP5Security']
dispacher.addERP5AccessTokenExtractionPlugin(token_extraction_id)
access_token_plugin_list = [getattr(acl_users, token_extraction_id)]
if access_token_plugin_list:
access_token_plugin, = access_token_plugin_list
# We only check that our plugin is enabled for IAuthenticationPlugin, this covers both
# cases where plugin was not enabled at all or was enabled only for IExtractionPlugin
IAuthenticationPlugin = [
# Products.PluggableAuthService.interfaces.plugins.IAuthenticationPlugin cannot
# be imported in restricted python but we can get it this way.
x for x in acl_users.plugins.listPluginTypeInfo()
if x['id'] == 'IAuthenticationPlugin'][0]['interface']
if (access_token_plugin.getId()
not in acl_users.plugins.listPluginIds(IAuthenticationPlugin)):
error_list.append("erp5_access_token_plugin is not activated")
if fixit:
access_token_plugin.manage_activateInterfaces((
'IExtractionPlugin',
'IAuthenticationPlugin',))
return error_list
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>Script_magic</string> </key>
<value> <int>3</int> </value>
</item>
<item>
<key> <string>_bind_names</string> </key>
<value>
<object>
<klass>
<global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
</klass>
<tuple/>
<state>
<dictionary>
<item>
<key> <string>_asgns</string> </key>
<value>
<dictionary>
<item>
<key> <string>name_container</string> </key>
<value> <string>container</string> </value>
</item>
<item>
<key> <string>name_context</string> </key>
<value> <string>context</string> </value>
</item>
<item>
<key> <string>name_m_self</string> </key>
<value> <string>script</string> </value>
</item>
<item>
<key> <string>name_subpath</string> </key>
<value> <string>traverse_subpath</string> </value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</state>
</object>
</value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>fixit=False</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TemplateTool_checkERP5AccessTokenExtractionPluginExistenceConsistency</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -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())
# Copyright (c) 2002-2013 Nexedi SA and Contributors. All Rights Reserved.
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
import transaction
class TestERP5AccessTokenSkins(ERP5TypeTestCase):
test_token_extraction_id = 'test_erp5_access_token_extraction'
def getBusinessTemplateList(self):
return ('erp5_base',
'erp5_access_token')
def generateNewId(self):
return str(self.portal.portal_ids.generateNewId(
id_group=('erp5_access_token_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._setupAccessTokenExtraction()
transaction.commit()
self.tic()
def _setupAccessTokenExtraction(self):
pas = self.portal.acl_users
access_extraction_list = [q for q in pas.objectValues() \
if q.meta_type == 'ERP5 Access Token Extraction Plugin']
if len(access_extraction_list) == 0:
dispacher = pas.manage_addProduct['ERP5Security']
dispacher.addERP5AccessTokenExtractionPlugin(self.test_token_extraction_id)
getattr(pas, self.test_token_extraction_id).manage_activateInterfaces(
('IExtractionPlugin',))
elif len(access_extraction_list) == 1:
self.test_token_extraction_id = access_extraction_list[0].getId()
elif len(access_extraction_list) > 1:
raise ValueError
transaction.commit()
def _createPerson(self, new_id):
"""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',
user_id='TESTP-' + new_id)
person.newContent(portal_type = 'Assignment').open()
transaction.commit()
return person
def _getTokenCredential(self, request):
plugin = getattr(self.portal.acl_users, self.test_token_extraction_id)
return plugin.extractCredentials(request)
def _createRestrictedAccessToken(self, new_id, person, method, url_string):
access_token = self.portal.access_token_module.newContent(
portal_type="Restricted Access Token",
url_string=url_string,
url_method=method,
)
if person:
access_token.edit(agent_value=person)
return access_token
def _createOneTimeRestrictedAccessToken(self, new_id, person, method, url_string):
access_token = self.portal.access_token_module.newContent(
portal_type="One Time Restricted Access Token",
url_string=url_string,
url_method=method,
)
if person:
access_token.edit(agent_value=person)
return access_token
def test_working_token(self):
person = self.person = self._createPerson(self.new_id)
access_url = "http://exemple.com/foo"
access_method = "GET"
access_token = self._createRestrictedAccessToken(self.new_id,
person,
access_method,
access_url)
access_token.validate()
self.tic()
self.portal.REQUEST.form["access_token"] = access_token.getId()
self.portal.REQUEST["REQUEST_METHOD"] = access_method
self.portal.REQUEST["ACTUAL_URL"] = access_url
self.portal.REQUEST.form["access_token_secret"] = access_token.getReference()
result = self._getTokenCredential(self.portal.REQUEST)
self.assertEqual(result.get('external_login'), person.Person_getUserId())
def test_bad_token(self):
person = self.person = self._createPerson(self.new_id)
access_url = "http://exemple.com/foo"
access_method = "GET"
access_token = self._createRestrictedAccessToken(self.new_id,
person,
access_method,
access_url)
access_token.validate()
self.tic()
self.portal.REQUEST.form["access_token"] = "XYSYDT-YDTYSD"
self.portal.REQUEST["REQUEST_METHOD"] = access_method
self.portal.REQUEST["ACTUAL_URL"] = access_url
self.portal.REQUEST.form["access_token_secret"] = access_token.getReference()
result = self._getTokenCredential(self.portal.REQUEST)
self.assertEqual(result, {})
def test_RestrictedAccessToken_getExternalLogin(self):
person = self.person = self._createPerson(self.new_id)
access_url = "http://exemple.com/foo"
access_method = "GET"
access_token = self._createRestrictedAccessToken(self.new_id,
person,
access_method,
access_url)
access_token.validate()
self.tic()
self.portal.REQUEST["REQUEST_METHOD"] = access_method
self.portal.REQUEST["ACTUAL_URL"] = access_url
self.portal.REQUEST.form["access_token_secret"] = access_token.getReference()
result = access_token.RestrictedAccessToken_getExternalLogin()
self.assertEqual(result, person.Person_getUserId())
self.assertEqual(access_token.getValidationState(), 'validated')
def test_RestrictedAccessToken_getExternalLogin_access_token_secret(self):
person = self.person = self._createPerson(self.new_id)
access_url = "http://exemple.com/foo"
access_method = "GET"
access_token = self._createRestrictedAccessToken(self.new_id,
person,
access_method,
access_url)
access_token.validate()
self.tic()
self.portal.REQUEST["REQUEST_METHOD"] = access_method
self.portal.REQUEST["ACTUAL_URL"] = access_url
result = access_token.RestrictedAccessToken_getExternalLogin()
self.assertEqual(result, None)
self.portal.REQUEST.form["access_token_secret"] = "XYXYXYXY"
self.assertEqual(result, None)
self.portal.REQUEST.form["access_token_secret"] = access_token.getReference()
result = access_token.RestrictedAccessToken_getExternalLogin()
self.assertEqual(result, person.Person_getUserId())
self.assertEqual(access_token.getValidationState(), 'validated')
def test_RestrictedAccessToken_getExternalLogin_no_agent(self):
access_url = "http://exemple.com/foo"
access_method = "GET"
access_token = self._createRestrictedAccessToken(self.new_id,
None,
access_method,
access_url)
access_token.validate()
self.tic()
self.portal.REQUEST["REQUEST_METHOD"] = access_method
self.portal.REQUEST["ACTUAL_URL"] = access_url
self.portal.REQUEST.form["access_token_secret"] = access_token.getReference()
result = access_token.RestrictedAccessToken_getExternalLogin()
self.assertEqual(result, None)
def test_RestrictedAccessToken_getExternalLogin_wrong_values(self):
person = self.person = self._createPerson(self.new_id)
access_url = "http://exemple.com/foo"
access_method = "GET"
access_token = self._createRestrictedAccessToken(self.new_id,
person,
access_method,
access_url)
self.tic()
result = access_token.RestrictedAccessToken_getExternalLogin()
self.assertEqual(result, None)
access_token.validate()
self.tic()
self.portal.REQUEST["REQUEST_METHOD"] = "POST"
self.portal.REQUEST["ACTUAL_URL"] = access_url
self.portal.REQUEST.form["access_token_secret"] = access_token.getReference()
result = access_token.RestrictedAccessToken_getExternalLogin()
self.assertEqual(result, None)
self.portal.REQUEST