Resolved merge conflicts
This commit is contained in:
commit
176f3b6a15
17 changed files with 312 additions and 25 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -83,4 +83,4 @@ class ImplicitCommitError(ValidationError): pass
|
|||
class RetryBackgroundJobError(Exception): pass
|
||||
class DocumentLockedError(ValidationError): pass
|
||||
class CircularLinkingError(ValidationError): pass
|
||||
|
||||
class SecurityException(Exception): pass
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
8
frappe/patches/v10_0/set_default_locking_time.py
Normal file
8
frappe/patches/v10_0/set_default_locking_time.py
Normal 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)
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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=';')
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue