seitime-frappe/frappe/utils/password.py
Gavin D'souza 3446026555 chore: Update header: license.txt => LICENSE
The license.txt file has been replaced with LICENSE for quite a while
now. INAL but it didn't seem accurate to say "hey, checkout license.txt
although there's no such file". Apart from this, there were
inconsistencies in the headers altogether...this change brings
consistency.
2021-09-03 12:02:59 +05:30

197 lines
6.9 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import string
import frappe
from frappe import _
from frappe.utils import cstr, encode
from cryptography.fernet import Fernet, InvalidToken
from passlib.hash import pbkdf2_sha256, mysql41
from passlib.registry import register_crypt_handler
from passlib.context import CryptContext
from pymysql.constants.ER import DATA_TOO_LONG
from psycopg2.errorcodes import STRING_DATA_RIGHT_TRUNCATION
class LegacyPassword(pbkdf2_sha256):
name = "frappe_legacy"
ident = "$frappel$"
def _calc_checksum(self, secret):
# check if this is a mysql hash
# it is possible that we will generate a false positive if the users password happens to be 40 hex chars proceeded
# by an * char, but this seems highly unlikely
if not (secret[0] == "*" and len(secret) == 41 and all(c in string.hexdigits for c in secret[1:])):
secret = mysql41.hash(secret + self.salt.decode('utf-8'))
return super(LegacyPassword, self)._calc_checksum(secret)
register_crypt_handler(LegacyPassword, force=True)
passlibctx = CryptContext(
schemes=[
"pbkdf2_sha256",
"argon2",
"frappe_legacy",
],
deprecated=[
"frappe_legacy",
],
)
def get_decrypted_password(doctype, name, fieldname='password', raise_exception=True):
auth = frappe.db.sql('''select `password` from `__Auth`
where doctype=%(doctype)s and name=%(name)s and fieldname=%(fieldname)s and encrypted=1''',
{ 'doctype': doctype, 'name': name, 'fieldname': fieldname })
if auth and auth[0][0]:
return decrypt(auth[0][0])
elif raise_exception:
frappe.throw(_('Password not found'), frappe.AuthenticationError)
def set_encrypted_password(doctype, name, pwd, fieldname='password'):
try:
frappe.db.sql("""insert into `__Auth` (doctype, name, fieldname, `password`, encrypted)
values (%(doctype)s, %(name)s, %(fieldname)s, %(pwd)s, 1)
{on_duplicate_update} `password`=%(pwd)s, encrypted=1""".format(
on_duplicate_update=frappe.db.get_on_duplicate_update(['doctype', 'name', 'fieldname'])
), { 'doctype': doctype, 'name': name, 'fieldname': fieldname, 'pwd': encrypt(pwd) })
except frappe.db.DataError as e:
if ((frappe.db.db_type == 'mariadb' and e.args[0] == DATA_TOO_LONG) or
(frappe.db.db_type == 'postgres' and e.pgcode == STRING_DATA_RIGHT_TRUNCATION)):
frappe.throw(_("Most probably your password is too long."), exc=e)
raise e
def remove_encrypted_password(doctype, name, fieldname='password'):
frappe.db.delete("__Auth", {
"doctype": doctype,
"name": name,
"fieldname": fieldname
})
def check_password(user, pwd, doctype='User', fieldname='password', delete_tracker_cache=True):
'''Checks if user and password are correct, else raises frappe.AuthenticationError'''
auth = frappe.db.sql("""select `name`, `password` from `__Auth`
where `doctype`=%(doctype)s and `name`=%(name)s and `fieldname`=%(fieldname)s and `encrypted`=0""",
{'doctype': doctype, 'name': user, 'fieldname': fieldname}, as_dict=True)
if not auth or not passlibctx.verify(pwd, auth[0].password):
raise frappe.AuthenticationError(_('Incorrect User or Password'))
# lettercase agnostic
user = auth[0].name
# TODO: This need to be deleted after checking side effects of it.
# We have a `LoginAttemptTracker` that can take care of tracking related cache.
if delete_tracker_cache:
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
:param user: username
:param pwd: new password
:param doctype: doctype name (for encryption)
:param fieldname: fieldname (in given doctype) (for encryption)
:param logout_all_session: delete all other session
'''
hashPwd = passlibctx.hash(pwd)
frappe.db.multisql({
"mariadb": """INSERT INTO `__Auth`
(`doctype`, `name`, `fieldname`, `password`, `encrypted`)
VALUES (%(doctype)s, %(name)s, %(fieldname)s, %(pwd)s, 0)
ON DUPLICATE key UPDATE `password`=%(pwd)s, encrypted=0""",
"postgres": """INSERT INTO `__Auth`
(`doctype`, `name`, `fieldname`, `password`, `encrypted`)
VALUES (%(doctype)s, %(name)s, %(fieldname)s, %(pwd)s, 0)
ON CONFLICT("name", "doctype", "fieldname") DO UPDATE
SET `password`=%(pwd)s, encrypted=0""",
}, {'doctype': doctype, 'name': user, 'fieldname': fieldname, 'pwd': hashPwd})
# clear all the sessions except current
if logout_all_sessions:
from frappe.sessions import clear_sessions
clear_sessions(user=user, keep_current=True, force=True)
def delete_all_passwords_for(doctype, name):
try:
frappe.db.delete("__Auth", {
"doctype": doctype,
"name": name
})
except Exception as e:
if not frappe.db.is_missing_column(e):
raise
def rename_password(doctype, old_name, new_name):
# NOTE: fieldname is not considered, since the document is renamed
frappe.db.sql("""update `__Auth` set name=%(new_name)s
where doctype=%(doctype)s and name=%(old_name)s""",
{ 'doctype': doctype, 'new_name': new_name, 'old_name': old_name })
def rename_password_field(doctype, old_fieldname, new_fieldname):
frappe.db.sql('''update `__Auth` set fieldname=%(new_fieldname)s
where doctype=%(doctype)s and fieldname=%(old_fieldname)s''',
{ 'doctype': doctype, 'old_fieldname': old_fieldname, 'new_fieldname': new_fieldname })
def create_auth_table():
# same as Framework.sql
frappe.db.create_auth_table()
def encrypt(txt, encryption_key=None):
# Only use Fernet.generate_key().decode() to enter encyption_key value
try:
cipher_suite = Fernet(encode(encryption_key or get_encryption_key()))
except Exception:
# encryption_key is not in 32 url-safe base64-encoded format
frappe.throw(_('Encryption key is in invalid format!'))
cipher_text = cstr(cipher_suite.encrypt(encode(txt)))
return cipher_text
def decrypt(txt, encryption_key=None):
# Only use encryption_key value generated with Fernet.generate_key().decode()
try:
cipher_suite = Fernet(encode(encryption_key or get_encryption_key()))
plain_text = cstr(cipher_suite.decrypt(encode(txt)))
return plain_text
except InvalidToken:
# encryption_key in site_config is changed and not valid
frappe.throw(_('Encryption key is invalid' + '!' if encryption_key else ', please check site_config.json.'))
def get_encryption_key():
from frappe.installer import update_site_config
if 'encryption_key' not in frappe.local.conf:
encryption_key = Fernet.generate_key().decode()
update_site_config('encryption_key', encryption_key)
frappe.local.conf.encryption_key = encryption_key
return frappe.local.conf.encryption_key
def get_password_reset_limit():
return frappe.db.get_single_value("System Settings", "password_reset_limit") or 0