Login Attemps : Warning and Lock out cancel

Hi,
I’m currently trying to do 2 things:

1- When a user is lock out, a notification should be sent to a certain Role. I saw this post: Login attempts Administrator Warning! - #7 by Helio_Jesus, but does a function to warn someone have been implemented yet? Or should we read the log and find the information there?

2- Allow a certain Role to ‘overwrite’ the attempts made by a user. In other word, if a user is lock for an hour, this user can remove that restriction. Is it possible?

Any help/guidance will be appreciated :slight_smile:

1 Like

Will these changes be contributed?

@mel_erp
are you trying to write code to do this or are you trying to configure it and need help with it?

Authentication Logs have these values Logged In, Logged Out, Incorrect Password

1 Like

@rmehta @Not_a_countant for now, I need to do it for one of my project and I didn’t know if those were already implemented (I guess they aren’t). In that case I need to do a work around for now. After that, if you guys think it’s useful for the community, I can try to add those in the settings of ERPNext.

For those still interested, here’s the code to unlock a user. You can set permissions via the Doctype or Permissions.
Lock Out User {‘user’: Link’-> User, ‘logouttime’ → Datetime}
so here’s the code:

def unlock_user(self):
		#reset the number of attempt to 0
		user_roles = frappe.get_roles(frappe.session.user)
	    frappe.cache().hset('login_failed_count', self.user, 0)
	    delete = frappe.delete_doc('Lock Out User', self.user)
        frappe.db.commit()
		frappe.msgprint("The user have been unlocked")

So basically, a user need to go on that document and click on a button and this method gets triggered.

is this a custom script? an app? addition to the core code?

I’ve put in in a doctype, but I get you could put it anywhere as long as you have the email address.
I supposed it would make more sense to put it in the User directly

can you specify for a Dummy what “I’ve put it in a doctype” practically means? did you edit the core code, or did you do this in the UI, or …?

my bad I completly forgot to put the heart of the code :woman_facepalming:! I gave the method without the population of the table!

I created a Doctype called “LockOut User”, which contains a field ‘User’ and a DateTime field.
To populate this table, I’ve created a scheduler that runs every 4 minutes and fetch all the lockout accounts to put them in my LockOut User table. So at this point, I have all the user who have been lockout with the time at which they were lockout. When this script runs, it also removes the account who are no longer blocked (after the time have been reached).

To make it work, I created a schedule in the hook.py ‘all’ that calls ‘max_attempt_reach()’:

from __future__ import unicode_literals
import frappe
import datetime
from datetime import timedelta
from frappe.utils import now_datetime
from frappe.utils import cint
from frappe.email.doctype.email_template.email_template import get_email_template

def max_attempt_reach():
        allow_consecutive_attempts = frappe.db.get_value('System Settings', 'System Setting', 'allow_consecutive_attempts')
        allow_login_after_fail= frappe.db.get_value('System Settings', 'System Setting', 'allow_login_after_fail')
        accounts = frappe.db.get_all('User', None)
        for account in accounts:
		if cint(frappe.cache().hget('login_failed_count', account['name'])) == int(allow_consecutive_attempts ):
                        #Get current time
                        _now = now_datetime()
                        account_info = frappe.db.get_all('Lock Out User', filters={'name':account['name']}, fields=['*'])
			if not account_info:
				lock_account(account['name'])
		    else:
	            account_info = account_info[0]
                diff_time = _now - account_info['lock_out_time'] #difference in time between current time and lock out time
                days, seconds = diff_time.days, diff_time.seconds
                diff_hour = (days * 24 * 60 * 60) + (seconds) #convert time to seconds
                #if diff_hour > allow_login_after_fail, the number of attempt have been reset : ie account in unlock
                if diff_hour > allow_login_after_fail:
	                unlock_account(account['name'])

def lock_account(user):
	frappe.new_doc('Lock Out User').update({
        	"user": user,
            "lock_out_time": now_datetime()
        }).save()
        frappe.db.commit()
	recipient = get_recipient(user)
        lockout_doc = frappe.get_doc('Lock Out User', user)
        frappe.sendmail(
        	recipients = [recipient],
                **get_email_template('User Lock Out', {'doc': lockout_doc})
        )

def get_recipient(user):
		users = frappe.db.get_all('User', None)
		#We want to send the email to a Super Admin which account is unlock
		for u in users:
            user_email = frappe.db.get_value('User', u['name'], 'email')
            user_roles = frappe.get_roles(user_email)
			if 'System Manager' in user_roles:
			    lock_out_email = frappe.db.get_value('Lock Out User', user_email, 'user')
			    if not lock_out_email:
				return user_email
	

def unlock_account(user):
	# reset the number of attempt to 0. 
    # By default, even if an account is unlock, the number of attempt will remains at the maximum attempt (allow_consecutive_attempts)
	frappe.cache().hset('login_failed_count', user, 0)
	delete = frappe.delete_doc('Lock Out User', user)
    frappe.db.commit()

Then, if the admin wants to unlock a user, they must go in ‘LockOut User’ doctype, click on a button (that I created as a field in the doctype JS)

frappe.ui.form.on('Lock Out User', {
	unlock_user_btn: function(frm) {
		frappe.call({
			method: "unlock_user",
			doc: frm.doc,
			args: {
			},
			callback: function() {
			}
		});
	},
});

that calls the methods in your_app.your_app.doctype.lockout_user.lockout_user.py

class LockOutUser(Document):
    def unlock_user(self):
		#reset the number of attempt to 0
		user_roles = frappe.get_roles(frappe.session.user)
	    frappe.cache().hset('login_failed_count', self.user, 0)
	    delete = frappe.delete_doc('Lock Out User', self.user)
        frappe.db.commit()
		frappe.msgprint("The user have been unlocked")

The code can be clean up a bit, I did it a few months ago without knowing much of erpnext. I thought it could be a good idea to add it here.
Ideally, it could be done automatically as soon as the user is lockout and the button to unlock it could be in the ‘User’ doctype. I didn’t feel like modifying the core, so I did it this way.

4 Likes