Commit 65610e6b authored by Ivan Tyagov's avatar Ivan Tyagov

Use system events bt5 for storing authentication failures & password

change events.
Adjust test accordingly.
parent 5ac7ee53
...@@ -50,9 +50,7 @@ ...@@ -50,9 +50,7 @@
</item> </item>
<item> <item>
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[ <value> <string>"""\n
"""\n
Form validator which will check if password is valid for the user.\n Form validator which will check if password is valid for the user.\n
"""\n """\n
from Products.ERP5Type.Document import newTempBase\n from Products.ERP5Type.Document import newTempBase\n
...@@ -69,11 +67,12 @@ message_dict = { 0: \'Unknown error\',\n ...@@ -69,11 +67,12 @@ message_dict = { 0: \'Unknown error\',\n
\n \n
def doValidation(person, password):\n def doValidation(person, password):\n
# raise so Formulator shows proper message\n # raise so Formulator shows proper message\n
result = person.Person_isPasswordValid(password)\n result_code_list = person.Person_analyzePassword(password)\n
if result<=0:\n if result_code_list!=[]:\n
message = context.Base_translateString(message_dict[result])\n translateString = context.Base_translateString\n
message = \' \'.join([translateString(message_dict[x]) for x in result_code_list])\n
raise ValidationError(\'external_validator_failed\', context, error_text=message)\n raise ValidationError(\'external_validator_failed\', context, error_text=message)\n
return result\n return 1\n
\n \n
user_login = request.get(\'field_user_login\', None)\n user_login = request.get(\'field_user_login\', None)\n
# find Person object (or authenticated member) and validate it on it (password recovered for an existing account)\n # find Person object (or authenticated member) and validate it on it (password recovered for an existing account)\n
...@@ -90,9 +89,7 @@ kw = {\'title\': \'%s %s\' %(first_name, last_name),\n ...@@ -90,9 +89,7 @@ kw = {\'title\': \'%s %s\' %(first_name, last_name),\n
person = newTempBase(portal, kw[\'title\'], **kw)\n person = newTempBase(portal, kw[\'title\'], **kw)\n
\n \n
return doValidation(person, password)\n return doValidation(person, password)\n
</string> </value>
]]></string> </value>
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
......
<?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>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
"""\n
Returns if password is valid or not. \n
If not valid return a negative code to indicate failure.\n
"""\n
from Products.Formulator.Errors import ValidationError\n
from DateTime import DateTime\n
import re\n
\n
MARKER = [\'\', None]\n
\n
portal = context.getPortalObject()\n
request = context.REQUEST\n
is_temp_object = context.isTempObject()\n
result_code_list = []\n
min_password_length = portal.portal_preferences.getPreferredMinPasswordLength()\n
\n
# not long enough\n
if min_password_length is not None:\n
if len(password) < min_password_length:\n
result_code_list.append(-1)\n
\n
# password contain X out of following Y regular expression groups ?\n
regular_expression_list = portal.portal_preferences.getPreferredRegularExpressionGroupList()\n
min_regular_expression_group_number = portal.portal_preferences.getPreferredMinRegularExpressionGroupNumber()\n
if regular_expression_list:\n
group_counter = 0\n
for re_expr in regular_expression_list:\n
mo = re.search(re_expr, password)\n
if mo is not None and len(mo.groups()):\n
group_counter+=1\n
#context.log(\'%s %s %s %s\' %(password, group_counter, min_regular_expression_group_number, regular_expression_list))\n
if group_counter < min_regular_expression_group_number:\n
# not enough groups match\n
result_code_list.append(-2)\n
\n
if not is_temp_object:\n
# not changed in last period ?\n
now = DateTime()\n
one_hour = 1/24.0\n
min_password_lifetime_duration = portal.portal_preferences.getPreferredMinPasswordLifetimeDuration()\n
#last_password_modification_date = context.getLastPasswordModificationDate()\n
last_password_modification_date = None\n
last_password_event = portal.portal_catalog.getResultValue(\n
portal_type = \'Password Event\',\n
default_destination_uid = context.getUid(),\n
sort_on = ((\'creation_date\', \'DESC\',),))\n
if last_password_event is not None:\n
last_password_modification_date = last_password_event.getCreationDate()\n
\n
if last_password_modification_date is not None and \\\n
min_password_lifetime_duration is not None and \\\n
(last_password_modification_date + min_password_lifetime_duration*one_hour) > now:\n
# too early to change password\n
result_code_list.append(-3)\n
\n
# not already used before ?\n
preferred_number_of_last_password_to_check = portal.portal_preferences.getPreferredNumberOfLastPasswordToCheck()\n
if preferred_number_of_last_password_to_check not in [None, 0]:\n
if context.isPasswordAlreadyUsed(password):\n
result_code_list.append(-4)\n
\n
# not contain the full name of the user in password or any parts of it (i.e. last and / or first name)\n
if portal.portal_preferences.isPrefferedForceUsernameCheckInPassword():\n
lower_password = password.lower()\n
if not is_temp_object:\n
# real object\n
first_name = context.getFirstName()\n
last_name = context.getLastName()\n
else:\n
# temporary object\n
first_name = getattr(context, \'first_name\', None)\n
last_name = getattr(context, \'last_name\', None)\n
\n
if first_name not in MARKER:\n
first_name = first_name.lower()\n
if last_name not in MARKER:\n
last_name = last_name.lower()\n
\n
if (first_name not in MARKER and first_name in lower_password) or \\\n
(last_name not in MARKER and last_name in lower_password):\n
# user\'s name must not be contained in password\n
result_code_list.append(-5)\n
\n
return result_code_list\n
]]></string> </value>
</item>
<item>
<key> <string>_params</string> </key>
<value> <string>password, request={}</string> </value>
</item>
<item>
<key> <string>_proxy_roles</string> </key>
<value>
<tuple>
<string>Manager</string>
</tuple>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Person_analyzePassword</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
"""\n """\n
\n \n
from DateTime import DateTime\n from DateTime import DateTime\n
from Products.ZSQLCatalog.SQLCatalog import Query\n
\n \n
request = context.REQUEST\n request = context.REQUEST\n
portal = context.getPortalObject()\n portal = context.getPortalObject()\n
...@@ -68,25 +69,35 @@ if not portal_preferences.isAuthenticationPolicyEnabled():\n ...@@ -68,25 +69,35 @@ if not portal_preferences.isAuthenticationPolicyEnabled():\n
\n \n
now = DateTime()\n now = DateTime()\n
one_second = 1/24.0/60.0/60.0\n one_second = 1/24.0/60.0/60.0\n
key = \'authentication_failure_list\'\n
session_id = context.Person_getAuthenticationSessionId()\n
# session might not be initialized yet\n
session = portal.portal_sessions[session_id]\n
authentication_failure_list = session.get(key, [])\n
\n
check_duration = portal_preferences.getPreferredAuthenticationFailureCheckDuration()\n check_duration = portal_preferences.getPreferredAuthenticationFailureCheckDuration()\n
block_duration = portal_preferences.getPreferredAuthenticationFailureBlockDuration()\n block_duration = portal_preferences.getPreferredAuthenticationFailureBlockDuration()\n
max_authentication_failures = portal_preferences.getPreferredMaxAuthenticationFailure()\n max_authentication_failures = portal_preferences.getPreferredMaxAuthenticationFailure()\n
\n
check_time = now - check_duration*one_second\n check_time = now - check_duration*one_second\n
failures_for_period = [x for x in authentication_failure_list if x >= check_time]\n
\n \n
#context.log(\'%s %s %s\' %(authentication_failure_list, failures_for_period, max_authentication_failures))\n # some failures might be still unindexed\n
if len(failures_for_period)>= max_authentication_failures:\n tag = \'authentication_event_%s\' %context.getReference()\n
# block login as too many authentication failure for given time interval back\n unindexed_failures = portal.portal_activities.countMessageWithTag(tag)\n
block_timeout = failures_for_period[-1] + block_duration*one_second\n \n
#context.log(\'check=%s block=%s release=%s-> %s\' %(check_duration, block_duration, block_timeout, failures_for_period))\n if unindexed_failures >= max_authentication_failures:\n
if block_timeout > now:\n # no need to check further\n
return 1\n
\n
# some are already indexed\n
kw = {\'portal_type\': \'Authentication Event\',\n
\'default_destination_uid\': context.getUid(),\n
\'creation_date\': Query(creation_date = check_time,\n
range=\'min\'),\n
\'validation_state\' : \'!=acknowledged\',\n
\'sort_on\' : ((\'creation_date\', \'ASC\',),),\n
\'limit\': max_authentication_failures\n
}\n
indexed_failure_list = portal.portal_catalog(**kw)\n
indexed_failures = len(indexed_failure_list)\n
\n
if (indexed_failures + unindexed_failures) >= max_authentication_failures:\n
last_authentication_failure = indexed_failure_list[-1].getObject()\n
block_timeout = last_authentication_failure.getCreationDate() + block_duration*one_second\n
if block_timeout >= now:\n
context.log(\'block %s\' %context.getReference())\n context.log(\'block %s\' %context.getReference())\n
request.set(\'is_user_account_blocked\', True)\n request.set(\'is_user_account_blocked\', True)\n
return 1\n return 1\n
......
...@@ -67,29 +67,32 @@ def _isPasswordExpired():\n ...@@ -67,29 +67,32 @@ def _isPasswordExpired():\n
now = DateTime()\n now = DateTime()\n
max_password_lifetime_duration = portal.portal_preferences.getPreferredMaxPasswordLifetimeDuration()\n max_password_lifetime_duration = portal.portal_preferences.getPreferredMaxPasswordLifetimeDuration()\n
password_lifetime_expire_warning_duration = portal.portal_preferences.getPreferredPasswordLifetimeExpireWarningDuration()\n password_lifetime_expire_warning_duration = portal.portal_preferences.getPreferredPasswordLifetimeExpireWarningDuration()\n
last_password_modification_date = context.getLastPasswordModificationDate()\n last_password_event = portal.portal_catalog.getResultValue(\n
early_warning = 0 #False\n portal_type = \'Password Event\',\n
if last_password_modification_date is not None:\n default_destination_uid = context.getUid(),\n
sort_on = ((\'creation_date\', \'DESC\',),))\n
expire_date_warning = 0 \n
if last_password_event is not None:\n
last_password_modification_date = last_password_event.getCreationDate()\n
expire_date = last_password_modification_date + max_password_lifetime_duration*one_hour \n expire_date = last_password_modification_date + max_password_lifetime_duration*one_hour \n
if password_lifetime_expire_warning_duration not in (0, None,):\n if password_lifetime_expire_warning_duration not in (0, None,):\n
# calculate early warning period\n # calculate early warning period\n
#context.log( \'%s %s\' %(now, (expire_date - password_lifetime_expire_warning_duration*one_hour)))\n if now > expire_date - password_lifetime_expire_warning_duration*one_hour and \\\n
if now > expire_date - password_lifetime_expire_warning_duration*one_hour \\\n expire_date > now:\n
and expire_date > now:\n expire_date_warning = expire_date\n
early_warning = int(round((expire_date - now))) # return number of hours till expire password moment\n
if expire_date < now:\n if expire_date < now:\n
# password is expired\n # password is expired\n
#context.log(\'expired %s\' %context.getReference())\n context.log(\'expired %s\' %context.getReference())\n
return True, early_warning\n return True, expire_date_warning\n
return False, early_warning\n return False, expire_date_warning\n
\n \n
_isPasswordExpired = CachingMethod(_isPasswordExpired,\n _isPasswordExpired = CachingMethod(_isPasswordExpired,\n
id=\'Person_isPasswordExpired\',\n id=\'Person_isPasswordExpired\',\n
cache_factory=\'erp5_content_short\')\n cache_factory=\'erp5_content_short\')\n
is_password_expired, is_user_account_password_expired_warning_on = _isPasswordExpired()\n is_password_expired, expire_date = _isPasswordExpired()\n
\n \n
request.set(\'is_user_account_password_expired\', is_password_expired)\n request.set(\'is_user_account_password_expired\', is_password_expired)\n
request.set(\'is_user_account_password_expired_warning_on\', is_user_account_password_expired_warning_on)\n request.set(\'is_user_account_password_expired_expire_date\', expire_date)\n
\n \n
return is_password_expired\n return is_password_expired\n
......
...@@ -50,9 +50,7 @@ ...@@ -50,9 +50,7 @@
</item> </item>
<item> <item>
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[ <value> <string>"""\n
"""\n
File a failed authentication attempt.\n File a failed authentication attempt.\n
"""\n """\n
from DateTime import DateTime\n from DateTime import DateTime\n
...@@ -63,32 +61,13 @@ if not portal_preferences.isAuthenticationPolicyEnabled():\n ...@@ -63,32 +61,13 @@ if not portal_preferences.isAuthenticationPolicyEnabled():\n
# no policy, no sense to file failure\n # no policy, no sense to file failure\n
return 0\n return 0\n
\n \n
key = \'authentication_failure_list\'\n activate_kw = {\'tag\': \'authentication_event_%s\' %context.getReference()}\n
session_id = context.Person_getAuthenticationSessionId()\n authentication_event = portal.system_event_module.newContent(\n
session = portal.portal_sessions[session_id]\n portal_type = "Authentication Event",\n
\n activate_kw = activate_kw)\n
if key not in session.keys():\n authentication_event.setDestinationValue(context)\n
# init it only once\n return authentication_event\n
session[key] = []\n </string> </value>
\n
authentication_failure_list = session[key]\n
authentication_failure_list.append(DateTime())\n
\n
# we care for only recent failures, no need to save all so purge old one\n
max_authentication_failures = portal.portal_preferences.getPreferredMaxAuthenticationFailure()\n
if len(authentication_failure_list)> max_authentication_failures:\n
authentication_failure_list.reverse()\n
authentication_failure_list = authentication_failure_list[0:max_authentication_failures]\n
authentication_failure_list.reverse()\n
\n
# update backend\n
session[key] = authentication_failure_list\n
\n
#context.log(\'notify login failure %s %s %s\' %(session_id, session, len(session[key])))\n
return session[key]\n
]]></string> </value>
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
......
...@@ -50,24 +50,46 @@ ...@@ -50,24 +50,46 @@
</item> </item>
<item> <item>
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string>portal = context.getPortalObject()\n <value> <string>from Products.ZSQLCatalog.SQLCatalog import Query\n
\n
portal = context.getPortalObject()\n
portal_preferences = portal.portal_preferences\n
\n \n
if not portal.portal_preferences.isAuthenticationPolicyEnabled():\n if not portal.portal_preferences.isAuthenticationPolicyEnabled():\n
# no policy, no sense to block account\n # no policy, no sense to unblock account\n
return 0\n return 0\n
\n \n
key = \'authentication_failure_list\'\n now = DateTime()\n
session_id = context.Person_getAuthenticationSessionId()\n one_second = 1/24.0/60.0/60.0\n
session = portal.portal_sessions[session_id]\n check_duration = portal_preferences.getPreferredAuthenticationFailureCheckDuration()\n
session[key] = []\n block_duration = portal_preferences.getPreferredAuthenticationFailureBlockDuration()\n
max_authentication_failures = portal_preferences.getPreferredMaxAuthenticationFailure()\n
check_time = now - check_duration*one_second\n
\n
# acknowledge last authentication events for user\n
kw = {\'portal_type\': \'Authentication Event\',\n
\'default_destination_uid\': context.getUid(),\n
\'creation_date\': Query(creation_date = check_time,\n
range=\'min\'),\n
\'validation_state\' : \'!=acknowledged\',\n
\'sort_on\' : ((\'creation_date\', \'ASC\',),),\n
}\n
\n
authentication_event_list = [x.getObject() for x in portal.portal_catalog(**kw)]\n
\n
for authentication_event in authentication_event_list:\n
authentication_event.activate().acknowledge(comment=\'User account unblocked.\')\n
\n
if not batch_mode:\n if not batch_mode:\n
message = context.Base_translateString(\'User Login unblocked.\')\n message = context.Base_translateString(\'User Login unblocked.\')\n
context.Base_redirect(form_id=form_id, keep_items={\'portal_status_message\': message})\n context.Base_redirect(form_id=form_id, keep_items={\'portal_status_message\': message})\n
\n
return\n
</string> </value> </string> </value>
</item> </item>
<item> <item>
<key> <string>_params</string> </key> <key> <string>_params</string> </key>
<value> <string>form_id, batch_mode=False</string> </value> <value> <string>form_id="view", batch_mode=False</string> </value>
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
......
...@@ -75,15 +75,15 @@ ...@@ -75,15 +75,15 @@
</tal:block>\n </tal:block>\n
</tal:block>\n </tal:block>\n
<tal:block tal:condition="not: isAnon"\n <tal:block tal:condition="not: isAnon"\n
tal:define="is_user_account_password_expired_warning_on python:request.get(\'is_user_account_password_expired_warning_on\', 0);">\n tal:define="is_user_account_password_expired_expire_date python:request.get(\'is_user_account_password_expired_expire_date\', 0);">\n
\n \n
<!-- Password will expire soon just warn user ? -->\n <!-- Password will expire soon just warn user ? -->\n
<tal:block tal:condition="is_user_account_password_expired_warning_on">\n <tal:block tal:condition="is_user_account_password_expired_expire_date">\n
<tal:block tal:define="came_from python: request.get(\'came_from\') or here.absolute_url();\n <tal:block tal:define="came_from python: request.get(\'came_from\') or here.absolute_url();\n
dummy python: response.redirect(\'%s/ERP5Site_viewNewPersonCredentialUpdateDialog?portal_status_message=%s&amp;cancel_url=%s\' %(came_from, here.Base_translateString(\'Your password will expire in %s hours. You are advised to change it as soon as possible.\' %is_user_account_password_expired_warning_on), came_from));" />\n dummy python: response.redirect(\'%s/ERP5Site_viewNewPersonCredentialUpdateDialog?portal_status_message=%s&amp;cancel_url=%s\' %(came_from, here.Base_translateString(\'Your password will expire at %s. You are advised to change it as soon as possible.\' %context.Base_FormatDate(is_user_account_password_expired_expire_date, hour_minute=1)), came_from));" />\n
</tal:block>\n </tal:block>\n
\n \n
<tal:block tal:condition="not: is_user_account_password_expired_warning_on">\n <tal:block tal:condition="not: is_user_account_password_expired_expire_date">\n
<tal:block tal:define="came_from python: request.get(\'came_from\') or here.absolute_url();\n <tal:block tal:define="came_from python: request.get(\'came_from\') or here.absolute_url();\n
dummy python: response.redirect(came_from);" />\n dummy python: response.redirect(came_from);" />\n
</tal:block>\n </tal:block>\n
......
...@@ -59,14 +59,12 @@ number_of_last_password_to_check = portal.portal_preferences.getPreferredNumberO ...@@ -59,14 +59,12 @@ number_of_last_password_to_check = portal.portal_preferences.getPreferredNumberO
\n \n
if number_of_last_password_to_check is not None and number_of_last_password_to_check:\n if number_of_last_password_to_check is not None and number_of_last_password_to_check:\n
# save password and modification date\n # save password and modification date\n
person.setLastPasswordModificationDate(DateTime())\n
old_password_list = person.getLastChangedPasswordValueList()\n
current_password = person.getPassword()\n current_password = person.getPassword()\n
if current_password is not None and current_password not in old_password_list:\n if current_password is not None:\n
# we care only if password is set\n password_event = portal.system_event_module.newContent(portal_type = \'Password Event\',\n
old_password_list.append(current_password)\n source_value = person,\n
person.setLastChangedPasswordValueList(old_password_list)\n destination_value = person,\n
context.log(\'%s %s %s\' %(person.getPassword(), person.getLastPasswordModificationDate(), old_password_list))\n password = current_password)\n
</string> </value> </string> </value>
</item> </item>
<item> <item>
......
erp5_credential erp5_credential
\ No newline at end of file erp5_system_event
\ No newline at end of file
12 13
\ No newline at end of file \ No newline at end of file
AuthenticationPolicyPreference AuthenticationPolicyPreference
LoginAccountProvider \ No newline at end of file
\ No newline at end of file
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