Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

388 linhas
13 KiB

  1. # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe
  5. from frappe import _
  6. import pyotp, os
  7. from frappe.utils.background_jobs import enqueue
  8. from jinja2 import Template
  9. from pyqrcode import create as qrcreate
  10. from six import BytesIO
  11. from base64 import b64encode, b32encode
  12. from frappe.utils import get_url, get_datetime, time_diff_in_seconds
  13. from six import iteritems, string_types
  14. class ExpiredLoginException(Exception): pass
  15. def toggle_two_factor_auth(state, roles=[]):
  16. '''Enable or disable 2FA in site_config and roles'''
  17. for role in roles:
  18. role = frappe.get_doc('Role', {'role_name': role})
  19. role.two_factor_auth = state
  20. role.save(ignore_permissions=True)
  21. def two_factor_is_enabled(user=None):
  22. '''Returns True if 2FA is enabled.'''
  23. enabled = int(frappe.db.get_value('System Settings', None, 'enable_two_factor_auth') or 0)
  24. if enabled:
  25. bypass_two_factor_auth = int(frappe.db.get_value('System Settings', None, 'bypass_2fa_for_retricted_ip_users') or 0)
  26. if bypass_two_factor_auth:
  27. restrict_ip = frappe.db.get_value("User", filters={"name": user}, fieldname="restrict_ip")
  28. if restrict_ip and bypass_two_factor_auth:
  29. enabled = False
  30. if not user or not enabled:
  31. return enabled
  32. return two_factor_is_enabled_for_(user)
  33. def should_run_2fa(user):
  34. '''Check if 2fa should run.'''
  35. return two_factor_is_enabled(user=user)
  36. def get_cached_user_pass():
  37. '''Get user and password if set.'''
  38. user = pwd = None
  39. tmp_id = frappe.form_dict.get('tmp_id')
  40. if tmp_id:
  41. user = frappe.cache().get(tmp_id+'_usr')
  42. pwd = frappe.cache().get(tmp_id+'_pwd')
  43. return (user, pwd)
  44. def authenticate_for_2factor(user):
  45. '''Authenticate two factor for enabled user before login.'''
  46. if frappe.form_dict.get('otp'):
  47. return
  48. otp_secret = get_otpsecret_for_(user)
  49. token = int(pyotp.TOTP(otp_secret).now())
  50. tmp_id = frappe.generate_hash(length=8)
  51. cache_2fa_data(user, token, otp_secret, tmp_id)
  52. verification_obj = get_verification_obj(user, token, otp_secret)
  53. # Save data in local
  54. frappe.local.response['verification'] = verification_obj
  55. frappe.local.response['tmp_id'] = tmp_id
  56. def cache_2fa_data(user, token, otp_secret, tmp_id):
  57. '''Cache and set expiry for data.'''
  58. pwd = frappe.form_dict.get('pwd')
  59. verification_method = get_verification_method()
  60. # set increased expiry time for SMS and Email
  61. if verification_method in ['SMS', 'Email']:
  62. expiry_time = 300
  63. frappe.cache().set(tmp_id + '_token', token)
  64. frappe.cache().expire(tmp_id + '_token', expiry_time)
  65. else:
  66. expiry_time = 180
  67. for k, v in iteritems({'_usr': user, '_pwd': pwd, '_otp_secret': otp_secret}):
  68. frappe.cache().set("{0}{1}".format(tmp_id, k), v)
  69. frappe.cache().expire("{0}{1}".format(tmp_id, k), expiry_time)
  70. def two_factor_is_enabled_for_(user):
  71. '''Check if 2factor is enabled for user.'''
  72. if user == "Administrator":
  73. return False
  74. if isinstance(user, string_types):
  75. user = frappe.get_doc('User', user)
  76. roles = [frappe.db.escape(d.role) for d in user.roles or []]
  77. roles.append('All')
  78. query = """select name from `tabRole` where two_factor_auth=1
  79. and name in ({0}) limit 1""".format(', '.join('\"{}\"'.format(i) for i in roles))
  80. if len(frappe.db.sql(query)) > 0:
  81. return True
  82. return False
  83. def get_otpsecret_for_(user):
  84. '''Set OTP Secret for user even if not set.'''
  85. otp_secret = frappe.db.get_default(user + '_otpsecret')
  86. if not otp_secret:
  87. otp_secret = b32encode(os.urandom(10)).decode('utf-8')
  88. frappe.db.set_default(user + '_otpsecret', otp_secret)
  89. frappe.db.commit()
  90. return otp_secret
  91. def get_verification_method():
  92. return frappe.db.get_value('System Settings', None, 'two_factor_method')
  93. def confirm_otp_token(login_manager, otp=None, tmp_id=None):
  94. '''Confirm otp matches.'''
  95. if not otp:
  96. otp = frappe.form_dict.get('otp')
  97. if not otp:
  98. if two_factor_is_enabled_for_(login_manager.user):
  99. return False
  100. return True
  101. if not tmp_id:
  102. tmp_id = frappe.form_dict.get('tmp_id')
  103. hotp_token = frappe.cache().get(tmp_id + '_token')
  104. otp_secret = frappe.cache().get(tmp_id + '_otp_secret')
  105. if not otp_secret:
  106. raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
  107. hotp = pyotp.HOTP(otp_secret)
  108. if hotp_token:
  109. if hotp.verify(otp, int(hotp_token)):
  110. frappe.cache().delete(tmp_id + '_token')
  111. return True
  112. else:
  113. login_manager.fail(_('Incorrect Verification code'), login_manager.user)
  114. totp = pyotp.TOTP(otp_secret)
  115. if totp.verify(otp):
  116. # show qr code only once
  117. if not frappe.db.get_default(login_manager.user + '_otplogin'):
  118. frappe.db.set_default(login_manager.user + '_otplogin', 1)
  119. delete_qrimage(login_manager.user)
  120. return True
  121. else:
  122. login_manager.fail(_('Incorrect Verification code'), login_manager.user)
  123. def get_verification_obj(user, token, otp_secret):
  124. otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
  125. verification_method = get_verification_method()
  126. verification_obj = None
  127. if verification_method == 'SMS':
  128. verification_obj = process_2fa_for_sms(user, token, otp_secret)
  129. elif verification_method == 'OTP App':
  130. #check if this if the first time that the user is trying to login. If so, send an email
  131. if not frappe.db.get_default(user + '_otplogin'):
  132. verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer, method='OTP App')
  133. else:
  134. verification_obj = process_2fa_for_otp_app(user, otp_secret, otp_issuer)
  135. elif verification_method == 'Email':
  136. verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer)
  137. return verification_obj
  138. def process_2fa_for_sms(user, token, otp_secret):
  139. '''Process sms method for 2fa.'''
  140. phone = frappe.db.get_value('User', user, ['phone', 'mobile_no'], as_dict=1)
  141. phone = phone.mobile_no or phone.phone
  142. status = send_token_via_sms(otp_secret, token=token, phone_no=phone)
  143. verification_obj = {
  144. 'token_delivery': status,
  145. 'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]),
  146. 'method': 'SMS',
  147. 'setup': status
  148. }
  149. return verification_obj
  150. def process_2fa_for_otp_app(user, otp_secret, otp_issuer):
  151. '''Process OTP App method for 2fa.'''
  152. totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
  153. if frappe.db.get_default(user + '_otplogin'):
  154. otp_setup_completed = True
  155. else:
  156. otp_setup_completed = False
  157. verification_obj = {
  158. 'totp_uri': totp_uri,
  159. 'method': 'OTP App',
  160. 'qrcode': get_qr_svg_code(totp_uri),
  161. 'setup': otp_setup_completed
  162. }
  163. return verification_obj
  164. def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'):
  165. '''Process Email method for 2fa.'''
  166. subject = None
  167. message = None
  168. status = True
  169. prompt = ''
  170. if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'):
  171. '''Sending one-time email for OTP App'''
  172. totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
  173. qrcode_link = get_link_for_qrcode(user, totp_uri)
  174. message = get_email_body_for_qr_code({'qrcode_link': qrcode_link})
  175. subject = get_email_subject_for_qr_code({'qrcode_link': qrcode_link})
  176. prompt = _('Please check your registered email address for instructions on how to proceed. Do not close this window as you will have to return to it.')
  177. else:
  178. '''Sending email verification'''
  179. prompt = _('Verification code has been sent to your registered email address.')
  180. status = send_token_via_email(user, token, otp_secret, otp_issuer, subject=subject, message=message)
  181. verification_obj = {
  182. 'token_delivery': status,
  183. 'prompt': status and prompt,
  184. 'method': 'Email',
  185. 'setup': status
  186. }
  187. return verification_obj
  188. def get_email_subject_for_2fa(kwargs_dict):
  189. '''Get email subject for 2fa.'''
  190. subject_template = _('Login Verification Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
  191. subject = render_string_template(subject_template, kwargs_dict)
  192. return subject
  193. def get_email_body_for_2fa(kwargs_dict):
  194. '''Get email body for 2fa.'''
  195. body_template = 'Enter this code to complete your login:<br><br> <b>{{otp}}</b>'
  196. body = render_string_template(body_template, kwargs_dict)
  197. return body
  198. def get_email_subject_for_qr_code(kwargs_dict):
  199. '''Get QRCode email subject.'''
  200. subject_template = _('One Time Password (OTP) Registration Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
  201. subject = render_string_template(subject_template, kwargs_dict)
  202. return subject
  203. def get_email_body_for_qr_code(kwargs_dict):
  204. '''Get QRCode email body.'''
  205. body_template = 'Please click on the following link and follow the instructions on the page.<br><br> {{qrcode_link}}'
  206. body = render_string_template(body_template, kwargs_dict)
  207. return body
  208. def render_string_template(_str, kwargs_dict):
  209. '''Render string with jinja.'''
  210. s = Template(_str)
  211. s = s.render(**kwargs_dict)
  212. return s
  213. def get_link_for_qrcode(user, totp_uri):
  214. '''Get link to temporary page showing QRCode.'''
  215. key = frappe.generate_hash(length=20)
  216. key_user = "{}_user".format(key)
  217. key_uri = "{}_uri".format(key)
  218. lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image'))
  219. if lifespan<=0:
  220. lifespan = 240
  221. frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan)
  222. frappe.cache().set_value(key_user, user, expires_in_sec=lifespan)
  223. return get_url('/qrcode?k={}'.format(key))
  224. def send_token_via_sms(otpsecret, token=None, phone_no=None):
  225. '''Send token as sms to user.'''
  226. try:
  227. from frappe.core.doctype.sms_settings.sms_settings import send_request
  228. except:
  229. return False
  230. if not phone_no:
  231. return False
  232. ss = frappe.get_doc('SMS Settings', 'SMS Settings')
  233. if not ss.sms_gateway_url:
  234. return False
  235. hotp = pyotp.HOTP(otpsecret)
  236. args = {
  237. ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token)))
  238. }
  239. for d in ss.get("parameters"):
  240. args[d.parameter] = d.value
  241. args[ss.receiver_parameter] = phone_no
  242. sms_args = {
  243. 'params': args,
  244. 'gateway_url': ss.sms_gateway_url,
  245. 'use_post': ss.use_post
  246. }
  247. enqueue(method=send_request, queue='short', timeout=300, event=None,
  248. async=True, job_name=None, now=False, **sms_args)
  249. return True
  250. def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None):
  251. '''Send token to user as email.'''
  252. user_email = frappe.db.get_value('User', user, 'email')
  253. if not user_email:
  254. return False
  255. hotp = pyotp.HOTP(otp_secret)
  256. otp = hotp.at(int(token))
  257. template_args = {'otp': otp, 'otp_issuer': otp_issuer}
  258. if not subject:
  259. subject = get_email_subject_for_2fa(template_args)
  260. if not message:
  261. message = get_email_body_for_2fa(template_args)
  262. email_args = {
  263. 'recipients': user_email,
  264. 'sender': None,
  265. 'subject': subject,
  266. 'message': message,
  267. 'header': [_('Verfication Code'), 'blue'],
  268. 'delayed': False,
  269. 'retry':3
  270. }
  271. enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None,
  272. async=True, job_name=None, now=False, **email_args)
  273. return True
  274. def get_qr_svg_code(totp_uri):
  275. '''Get SVG code to display Qrcode for OTP.'''
  276. url = qrcreate(totp_uri)
  277. svg = ''
  278. stream = BytesIO()
  279. try:
  280. url.svg(stream, scale=4, background="#eee", module_color="#222")
  281. svg = stream.getvalue().decode().replace('\n', '')
  282. svg = b64encode(svg.encode())
  283. finally:
  284. stream.close()
  285. return svg
  286. def qrcode_as_png(user, totp_uri):
  287. '''Save temporary Qrcode to server.'''
  288. from frappe.utils.file_manager import save_file
  289. folder = create_barcode_folder()
  290. png_file_name = '{}.png'.format(frappe.generate_hash(length=20))
  291. file_obj = save_file(png_file_name, png_file_name, 'User', user, folder=folder)
  292. frappe.db.commit()
  293. file_url = get_url(file_obj.file_url)
  294. file_path = os.path.join(frappe.get_site_path('public', 'files'), file_obj.file_name)
  295. url = qrcreate(totp_uri)
  296. with open(file_path, 'w') as png_file:
  297. url.png(png_file, scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc])
  298. return file_url
  299. def create_barcode_folder():
  300. '''Get Barcodes folder.'''
  301. folder_name = 'Barcodes'
  302. folder = frappe.db.exists('File', {'file_name': folder_name})
  303. if folder:
  304. return folder
  305. folder = frappe.get_doc({
  306. 'doctype': 'File',
  307. 'file_name': folder_name,
  308. 'is_folder':1,
  309. 'folder': 'Home'
  310. })
  311. folder.insert(ignore_permissions=True)
  312. return folder.name
  313. def delete_qrimage(user, check_expiry=False):
  314. '''Delete Qrimage when user logs in.'''
  315. user_barcodes = frappe.get_all('File', {'attached_to_doctype': 'User',
  316. 'attached_to_name': user, 'folder': 'Home/Barcodes'})
  317. for barcode in user_barcodes:
  318. if check_expiry and not should_remove_barcode_image(barcode):
  319. continue
  320. barcode = frappe.get_doc('File', barcode.name)
  321. frappe.delete_doc('File', barcode.name, ignore_permissions=True)
  322. def delete_all_barcodes_for_users():
  323. '''Task to delete all barcodes for user.'''
  324. if not two_factor_is_enabled():
  325. return
  326. users = frappe.get_all('User', {'enabled':1})
  327. for user in users:
  328. delete_qrimage(user.name, check_expiry=True)
  329. def should_remove_barcode_image(barcode):
  330. '''Check if it's time to delete barcode image from server. '''
  331. if isinstance(barcode, string_types):
  332. barcode = frappe.get_doc('File', barcode)
  333. lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')
  334. if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan):
  335. return True
  336. return False
  337. def disable():
  338. frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 0)