Resolved merge conflicts

This commit is contained in:
Saurabh 2018-07-18 16:20:06 +05:30
commit 176f3b6a15
17 changed files with 312 additions and 25 deletions

View file

@ -17,7 +17,7 @@ from faker import Faker
from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
__version__ = '10.1.39'
__version__ = '10.1.40'
__title__ = "Frappe Framework"
local = Local()

View file

@ -8,13 +8,13 @@ from frappe import _
import frappe
import frappe.database
import frappe.utils
from frappe.utils import cint
from frappe.utils import cint, flt, get_datetime, datetime
import frappe.utils.user
from frappe import conf
from frappe.sessions import Session, clear_sessions, delete_session
from frappe.modules.patch_handler import check_session_stopped
from frappe.translate import get_lang_code
from frappe.utils.password import check_password
from frappe.utils.password import check_password, delete_login_failed_cache
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
confirm_otp_token, get_cached_user_pass)
@ -211,6 +211,10 @@ class LoginManager:
def check_if_enabled(self, user):
"""raise exception if user not enabled"""
doc = frappe.get_doc("System Settings")
if doc.allow_consecutive_login_attempts > 0:
check_consecutive_login_attempts(user, doc)
if user=='Administrator': return
if not cint(frappe.db.get_value('User', user, 'enabled')):
self.fail('User disabled or missing', user=user)
@ -221,6 +225,7 @@ class LoginManager:
# returns user in correct case
return check_password(user, pwd)
except frappe.AuthenticationError:
self.update_invalid_login(user)
self.fail('Incorrect password', user=user)
def fail(self, message, user=None):
@ -231,6 +236,15 @@ class LoginManager:
frappe.db.commit()
raise frappe.AuthenticationError
def update_invalid_login(self, user):
last_login_tried = get_last_tried_login_data(user)
failed_count = 0
if last_login_tried > get_datetime():
failed_count = get_login_failed_count(user)
frappe.cache().hset('login_failed_count', user, failed_count + 1)
def run_trigger(self, event='on_login'):
for method in frappe.get_hooks().get(event, []):
frappe.call(frappe.get_attr(method), login_manager=self)
@ -346,3 +360,35 @@ def get_website_user_home_page(user):
return '/' + home_page.strip('/')
else:
return '/me'
def get_last_tried_login_data(user, get_last_login=False):
locked_account_time = frappe.cache().hget('locked_account_time', user)
if get_last_login and locked_account_time:
return locked_account_time
last_login_tried = frappe.cache().hget('last_login_tried', user)
if not last_login_tried or last_login_tried < get_datetime():
last_login_tried = get_datetime() + datetime.timedelta(seconds=60)
frappe.cache().hset('last_login_tried', user, last_login_tried)
return last_login_tried
def get_login_failed_count(user):
return cint(frappe.cache().hget('login_failed_count', user)) or 0
def check_consecutive_login_attempts(user, doc):
login_failed_count = get_login_failed_count(user)
last_login_tried = (get_last_tried_login_data(user, True)
+ datetime.timedelta(seconds=doc.allow_login_after_fail))
if login_failed_count >= cint(doc.allow_consecutive_login_attempts):
locked_account_time = frappe.cache().hget('locked_account_time', user)
if not locked_account_time:
frappe.cache().hset('locked_account_time', user, get_datetime())
if last_login_tried > get_datetime():
frappe.throw(_("Your account has been locked and will resume after {0} seconds")
.format(doc.allow_login_after_fail), frappe.SecurityException)
else:
delete_login_failed_cache(user)

View file

@ -5,10 +5,11 @@ from __future__ import unicode_literals
import frappe
import unittest
import time
from frappe.auth import LoginManager, CookieManager
class TestActivityLog(unittest.TestCase):
def test_activity_log(self):
from frappe.auth import LoginManager, CookieManager
# test user login log
frappe.local.form_dict = frappe._dict({
@ -44,4 +45,44 @@ class TestActivityLog(unittest.TestCase):
name = names[0]
auth_log = frappe.get_doc('Activity Log', name)
return auth_log
return auth_log
def test_brute_security(self):
update_system_settings({
'allow_consecutive_login_attempts': 3,
'allow_login_after_fail': 5
})
frappe.local.form_dict = frappe._dict({
'cmd': 'login',
'sid': 'Guest',
'pwd': 'admin',
'usr': 'Administrator'
})
frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager()
auth_log = self.get_auth_log()
self.assertEquals(auth_log.status, 'Success')
# test user logout log
frappe.local.login_manager.logout()
auth_log = self.get_auth_log(operation='Logout')
self.assertEquals(auth_log.status, 'Success')
# test invalid login
frappe.form_dict.update({ 'pwd': 'password' })
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.AuthenticationError, LoginManager)
self.assertRaises(frappe.SecurityException, LoginManager)
time.sleep(5)
self.assertRaises(frappe.AuthenticationError, LoginManager)
frappe.local.form_dict = frappe._dict()
def update_system_settings(args):
doc = frappe.get_doc('System Settings')
doc.update(args)
doc.save()

View file

@ -94,9 +94,8 @@ def notify_mentions(doc):
subject = _("{0} mentioned you in a comment").format(sender_fullname)
recipients = [frappe.db.get_value("User", {"enabled": 1, "username": username, "user_type": "System User"})
for username in mentions]
recipients = [frappe.db.get_value("User", {"enabled": 1, "name": name, "user_type": "System User"})
for name in mentions]
frappe.sendmail(
recipients=recipients,
sender=frappe.session.user,

View file

@ -927,6 +927,157 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "brute_force_security",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Brute Force Security",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow_consecutive_login_attempts",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Consecutive Login Attempts ",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_34",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "60",
"description": "In seconds",
"fieldname": "allow_login_after_fail",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Allow Login After Fail",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "two_factor_authentication",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Two Factor Authentication",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -1405,7 +1556,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2018-05-23 16:45:46.261765",
"modified": "2018-07-06 16:33:49.222058",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -12,6 +12,7 @@ from frappe.limits import update_limits, clear_limit
from frappe.utils import get_url
from frappe.core.doctype.user.user import get_total_users
from frappe.core.doctype.user.user import MaxUsersReachedError, test_password_strength
from frappe.core.doctype.user.user import extract_mentions
test_records = frappe.get_test_records('User')
@ -266,5 +267,13 @@ class TestUser(unittest.TestCase):
result = test_password_strength("Eastern_43A1W")
self.assertEqual(result['feedback']['password_policy_validation_passed'], True)
def test_comment_mentions(self):
user_name = "@test.comment@example.com"
self.assertEqual(extract_mentions(user_name)[0], "test.comment@example.com")
user_name = "Testing comment, @test-user please check."
self.assertEqual(extract_mentions(user_name)[0], "test-user")
user_name = "Testing comment, @test.user@example.com please check."
self.assertEqual(extract_mentions(user_name)[0], "test.user@example.com")
def delete_contact(user):
frappe.db.sql("delete from tabContact where email_id='%s'" % frappe.db.escape(user))

View file

@ -927,9 +927,9 @@ def notify_admin_access_to_system_manager(login_manager=None):
)
def extract_mentions(txt):
"""Find all instances of @username in the string.
"""Find all instances of @name in the string.
The mentions will be separated by non-word characters or may appear at the start of the string"""
return re.findall(r'(?:[^\w]|^)@([\w]*)', txt)
return re.findall(r'(?:[^\w\.\-\@]|^)@([\w\.\-\@]*)', txt)
def handle_password_test_fail(result):

View file

@ -8,31 +8,43 @@ from frappe.utils import cstr, unique, cint
from frappe.permissions import has_permission
from frappe import _
from six import string_types
import re
def sanitize_searchfield(searchfield):
blacklisted_keywords = ['select', 'delete', 'drop', 'update', 'case', 'and', 'or', 'like']
def _raise_exception():
frappe.throw(_('Invalid Search Field'), frappe.DataError)
def _raise_exception(searchfield):
frappe.throw(_('Invalid Search Field {0}').format(searchfield), frappe.DataError)
if len(searchfield) == 1:
# do not allow special characters to pass as searchfields
regex = re.compile('^.*[=;*,\'"$\-+%#@()_].*')
if regex.match(searchfield):
_raise_exception(searchfield)
if len(searchfield) >= 3:
# to avoid 1=1
if '=' in searchfield:
_raise_exception()
_raise_exception(searchfield)
# in mysql -- is used for commenting the query
elif ' --' in searchfield:
_raise_exception()
_raise_exception(searchfield)
# to avoid and, or and like
elif any(' {0} '.format(keyword) in searchfield.split() for keyword in blacklisted_keywords):
_raise_exception()
_raise_exception(searchfield)
# to avoid select, delete, drop, update and case
elif any(keyword in searchfield.split() for keyword in blacklisted_keywords):
_raise_exception()
_raise_exception(searchfield)
else:
regex = re.compile('^.*[=;*,\'"$\-+%#@()].*')
if any(regex.match(f) for f in searchfield.split()):
_raise_exception(searchfield)
# this is called by the Link Field
@frappe.whitelist()

View file

@ -83,4 +83,4 @@ class ImplicitCommitError(ValidationError): pass
class RetryBackgroundJobError(Exception): pass
class DocumentLockedError(ValidationError): pass
class CircularLinkingError(ValidationError): pass
class SecurityException(Exception): pass

View file

@ -222,4 +222,4 @@ frappe.patches.v11_0.delete_duplicate_user_permissions
frappe.patches.v11_0.replicate_old_user_permissions
frappe.patches.v11_0.set_dropbox_file_backup
frappe.patches.v11_0.get_docs_apps_if_not_present
frappe.patches.v10_0.set_default_locking_time

View file

@ -0,0 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
def execute():
frappe.reload_doc("core", "doctype", "system_settings")
frappe.db.set_value('System Settings', None, "allow_login_after_fail", 60)

View file

@ -17,7 +17,7 @@ frappe.ui.form.Timeline = Class.extend({
this.comment_area = new frappe.ui.CommentArea({
parent: this.wrapper.find('.timeline-head'),
mentions: this.get_usernames_for_mentions(),
mentions: this.get_names_for_mentions(),
on_submit: (val) => {
val && this.insert_comment(
"Comment", val, this.comment_area.button);
@ -99,7 +99,7 @@ frappe.ui.form.Timeline = Class.extend({
this.editing_area = new frappe.ui.CommentArea({
parent: this.$editing_area,
mentions: this.get_usernames_for_mentions(),
mentions: this.get_names_for_mentions(),
no_wrapper: true
});
@ -666,11 +666,11 @@ frappe.ui.form.Timeline = Class.extend({
return last_email;
},
get_usernames_for_mentions: function() {
get_names_for_mentions: function() {
var valid_users = Object.keys(frappe.boot.user_info)
.filter(user => !["Administrator", "Guest"].includes(user));
return valid_users.map(user => frappe.boot.user_info[user].username || frappe.boot.user_info[user].name);
return valid_users.map(user => frappe.boot.user_info[user].name);
},
setup_comment_like: function() {

View file

@ -146,6 +146,7 @@ frappe.views.GridReport = Class.extend({
},
setup_filters: function() {
var me = this;
$.each(me.filter_inputs, function(i, v) {
var opts = v.get(0).opts;
if(opts.fieldtype == "Select" && in_list(me.doctypes, opts.link)) {
@ -173,7 +174,7 @@ frappe.views.GridReport = Class.extend({
this.page.add_menu_item(__("Print"), function() {
frappe.ui.get_print_settings(false, function(print_settings) {
frappe.render_grid({grid: me.grid, title: me.page.title, print_settings: print_settings });
frappe.render_grid({grid: me.grid, title: me.page.title, print_settings: print_settings, report: me});
});
}, true);

View file

@ -88,7 +88,7 @@ frappe.render_grid = function(opts) {
// build context
if(opts.grid) {
opts.columns = opts.grid.getColumns();
if(opts.report) {
if(opts.report && opts.report.dataView) {
opts.data = frappe.slickgrid_tools.get_filtered_items(opts.report.dataView);
} else if(opts.grid) {
opts.data = opts.grid.getData().getItems();

View file

@ -26,3 +26,15 @@ class TestSearch(unittest.TestCase):
self.assertRaises(frappe.DataError,
search_link, 'DocType', 'Customer', query=None, filters=None,
page_length=20, searchfield='name or (select * from tabSessions)')
self.assertRaises(frappe.DataError,
search_link, 'DocType', 'Customer', query=None, filters=None,
page_length=20, searchfield='*')
self.assertRaises(frappe.DataError,
search_link, 'DocType', 'Customer', query=None, filters=None,
page_length=20, searchfield=';')
self.assertRaises(frappe.DataError,
search_link, 'DocType', 'Customer', query=None, filters=None,
page_length=20, searchfield=';')

View file

@ -549,6 +549,8 @@ def in_words(integer, in_million=True):
ret = num2words(integer, lang=locale)
except NotImplementedError:
ret = num2words(integer, lang='en')
except OverflowError:
ret = num2words(integer, lang='en')
return ret.replace('-', ' ')
def is_html(text):

View file

@ -67,12 +67,18 @@ def check_password(user, pwd, doctype='User', fieldname='password'):
# lettercase agnostic
user = auth[0].name
delete_login_failed_cache(user)
if not passlibctx.needs_update(auth[0].password):
update_password(user, pwd, doctype, fieldname)
return user
def delete_login_failed_cache(user):
frappe.cache().hdel('last_login_tried', user)
frappe.cache().hdel('login_failed_count', user)
frappe.cache().hdel('locked_account_time', user)
def update_password(user, pwd, doctype='User', fieldname='password', logout_all_sessions=False):
'''
Update the password for the User