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 @@
</item>
<item>
<key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
"""\n
<value> <string>"""\n
Form validator which will check if password is valid for the user.\n
"""\n
from Products.ERP5Type.Document import newTempBase\n
......@@ -69,11 +67,12 @@ message_dict = { 0: \'Unknown error\',\n
\n
def doValidation(person, password):\n
# raise so Formulator shows proper message\n
result = person.Person_isPasswordValid(password)\n
if result<=0:\n
message = context.Base_translateString(message_dict[result])\n
result_code_list = person.Person_analyzePassword(password)\n
if result_code_list!=[]:\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
return result\n
return 1\n
\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
......@@ -90,9 +89,7 @@ kw = {\'title\': \'%s %s\' %(first_name, last_name),\n
person = newTempBase(portal, kw[\'title\'], **kw)\n
\n
return doValidation(person, password)\n
]]></string> </value>
</string> </value>
</item>
<item>
<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 @@
"""\n
\n
from DateTime import DateTime\n
from Products.ZSQLCatalog.SQLCatalog import Query\n
\n
request = context.REQUEST\n
portal = context.getPortalObject()\n
......@@ -68,25 +69,35 @@ if not portal_preferences.isAuthenticationPolicyEnabled():\n
\n
now = DateTime()\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
block_duration = portal_preferences.getPreferredAuthenticationFailureBlockDuration()\n
max_authentication_failures = portal_preferences.getPreferredMaxAuthenticationFailure()\n
\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
#context.log(\'%s %s %s\' %(authentication_failure_list, failures_for_period, max_authentication_failures))\n
if len(failures_for_period)>= max_authentication_failures:\n
# block login as too many authentication failure for given time interval back\n
block_timeout = failures_for_period[-1] + block_duration*one_second\n
#context.log(\'check=%s block=%s release=%s-> %s\' %(check_duration, block_duration, block_timeout, failures_for_period))\n
if block_timeout > now:\n
# some failures might be still unindexed\n
tag = \'authentication_event_%s\' %context.getReference()\n
unindexed_failures = portal.portal_activities.countMessageWithTag(tag)\n
\n
if unindexed_failures >= max_authentication_failures:\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
request.set(\'is_user_account_blocked\', True)\n
return 1\n
......
......@@ -67,29 +67,32 @@ def _isPasswordExpired():\n
now = DateTime()\n
max_password_lifetime_duration = portal.portal_preferences.getPreferredMaxPasswordLifetimeDuration()\n
password_lifetime_expire_warning_duration = portal.portal_preferences.getPreferredPasswordLifetimeExpireWarningDuration()\n
last_password_modification_date = context.getLastPasswordModificationDate()\n
early_warning = 0 #False\n
if last_password_modification_date is not 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
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
if password_lifetime_expire_warning_duration not in (0, None,):\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 \\\n
and expire_date > now:\n
early_warning = int(round((expire_date - now))) # return number of hours till expire password moment\n
if now > expire_date - password_lifetime_expire_warning_duration*one_hour and \\\n
expire_date > now:\n
expire_date_warning = expire_date\n
if expire_date < now:\n
# password is expired\n
#context.log(\'expired %s\' %context.getReference())\n
return True, early_warning\n
return False, early_warning\n
context.log(\'expired %s\' %context.getReference())\n
return True, expire_date_warning\n
return False, expire_date_warning\n
\n
_isPasswordExpired = CachingMethod(_isPasswordExpired,\n
id=\'Person_isPasswordExpired\',\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
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
return is_password_expired\n
......
......@@ -50,9 +50,7 @@
</item>
<item>
<key> <string>_body</string> </key>
<value> <string encoding="cdata"><![CDATA[
"""\n
<value> <string>"""\n
File a failed authentication attempt.\n
"""\n
from DateTime import DateTime\n
......@@ -63,32 +61,13 @@ if not portal_preferences.isAuthenticationPolicyEnabled():\n
# no policy, no sense to file failure\n
return 0\n
\n
key = \'authentication_failure_list\'\n
session_id = context.Person_getAuthenticationSessionId()\n
session = portal.portal_sessions[session_id]\n
\n
if key not in session.keys():\n
# init it only once\n
session[key] = []\n
\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>
activate_kw = {\'tag\': \'authentication_event_%s\' %context.getReference()}\n
authentication_event = portal.system_event_module.newContent(\n
portal_type = "Authentication Event",\n
activate_kw = activate_kw)\n
authentication_event.setDestinationValue(context)\n
return authentication_event\n
</string> </value>
</item>
<item>
<key> <string>_params</string> </key>
......
......@@ -50,24 +50,46 @@
</item>
<item>
<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
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
\n
key = \'authentication_failure_list\'\n
session_id = context.Person_getAuthenticationSessionId()\n
session = portal.portal_sessions[session_id]\n
session[key] = []\n
now = DateTime()\n
one_second = 1/24.0/60.0/60.0\n
check_duration = portal_preferences.getPreferredAuthenticationFailureCheckDuration()\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
message = context.Base_translateString(\'User Login unblocked.\')\n
context.Base_redirect(form_id=form_id, keep_items={\'portal_status_message\': message})\n
\n
return\n
</string> </value>
</item>
<item>
<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>
<key> <string>id</string> </key>
......
......@@ -75,15 +75,15 @@
</tal:block>\n
</tal:block>\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
<!-- 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
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
\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
dummy python: response.redirect(came_from);" />\n
</tal:block>\n
......
......@@ -59,14 +59,12 @@ number_of_last_password_to_check = portal.portal_preferences.getPreferredNumberO
\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
person.setLastPasswordModificationDate(DateTime())\n
old_password_list = person.getLastChangedPasswordValueList()\n
current_password = person.getPassword()\n
if current_password is not None and current_password not in old_password_list:\n
# we care only if password is set\n
old_password_list.append(current_password)\n
person.setLastChangedPasswordValueList(old_password_list)\n
context.log(\'%s %s %s\' %(person.getPassword(), person.getLastPasswordModificationDate(), old_password_list))\n
if current_password is not None:\n
password_event = portal.system_event_module.newContent(portal_type = \'Password Event\',\n
source_value = person,\n
destination_value = person,\n
password = current_password)\n
</string> </value>
</item>
<item>
......
erp5_credential
\ No newline at end of file
erp5_credential
erp5_system_event
\ No newline at end of file
12
\ No newline at end of file
13
\ No newline at end of file
AuthenticationPolicyPreference
LoginAccountProvider
\ No newline at end of file
AuthenticationPolicyPreference
\ 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