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.
197 lines
6.9 KiB
Python
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
|