diff --git a/frappe/auth.py b/frappe/auth.py index 7fb9bb58de..bd510b9fcd 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -17,13 +17,12 @@ from frappe.translate import get_lang_code from frappe.utils.password import check_password from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log from frappe.utils.background_jobs import enqueue -from twofactor import should_run_2fa, authenticate_for_2factor, \ - confirm_otp_token,get_cached_user_pass - +from twofactor import (should_run_2fa, authenticate_for_2factor, + confirm_otp_token, get_cached_user_pass) from six.moves.urllib.parse import quote -import pyotp,base64,os +import pyotp, base64, os class HTTPRequest: def __init__(self): @@ -68,7 +67,7 @@ class HTTPRequest: def validate_csrf_token(self): if frappe.local.request and frappe.local.request.method=="POST": - if not frappe.local.session:return + if not frappe.local.session: return if not frappe.local.session.data.csrf_token \ or frappe.local.session.data.device=="mobile" \ or frappe.conf.get('ignore_csrf', None): @@ -95,7 +94,7 @@ class HTTPRequest: def connect(self, ac_name = None): """connect to db, from ac_name or db_name""" frappe.local.db = frappe.database.Database(user = self.get_db_name(), \ - password = getattr(conf,'db_password', '')) + password = getattr(conf, 'db_password', '')) class LoginManager: def __init__(self): @@ -105,7 +104,7 @@ class LoginManager: self.user_type = None if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": - if self.login()==False:return + if self.login()==False: return self.resume = False # run login triggers @@ -120,20 +119,17 @@ class LoginManager: self.make_session() self.set_user_info() - def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) - user,pwd = get_cached_user_pass() - self.authenticate(user=user,pwd=pwd) + user, pwd = get_cached_user_pass() + self.authenticate(user=user, pwd=pwd) if should_run_2fa(self.user): authenticate_for_2factor(self.user) if not confirm_otp_token(self): return False self.post_login() - - def post_login(self): self.run_trigger('on_login') self.validate_ip_address() @@ -198,7 +194,7 @@ class LoginManager: if not (user and pwd): user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') if not (user and pwd): - self.fail('Incomplete login details', user=user) + self.fail(_('Incomplete login details'), user=user) if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")): user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user @@ -220,7 +216,9 @@ class LoginManager: except frappe.AuthenticationError: self.fail('Incorrect password', user=user) - def fail(self, message, user="NA"): + def fail(self, message, user=None): + if not user: + user = _('Unknown User') frappe.local.response['message'] = message add_authentication_log(message, user, status="Failed") frappe.db.commit() diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 3305d2c0e2..6405a275bf 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -778,11 +778,8 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "OTP App", - "depends_on": "", - "description": "Choose authentication method to be used by all users", - "fieldname": "two_factor_method", - "fieldtype": "Select", + "fieldname": "column_break_13", + "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -790,10 +787,8 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Two Factor Authentication method", "length": 0, "no_copy": 0, - "options": "OTP App\nSMS\nEmail", "permlevel": 0, "precision": "", "print_hide": 0, @@ -812,10 +807,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:doc.two_factor_method == \"OTP App\"", - "description": "Time in seconds to retain QR code image on server. Min:240", - "fieldname": "lifespan_qrcode_image", - "fieldtype": "Int", + "description": "Note: Multiple sessions will be allowed in case of mobile device", + "fieldname": "deny_multiple_sessions", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -823,7 +817,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Expiry time of QR Code Image Page", + "label": "Allow only one session per user", "length": 0, "no_copy": 0, "permlevel": 0, @@ -844,10 +838,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "Frappe Framework", - "depends_on": "", - "fieldname": "otp_issuer_name", - "fieldtype": "Data", + "description": "User can login using Email id or Mobile number", + "fieldname": "allow_login_using_mobile_number", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -855,10 +848,9 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "OTP Issuer Name", + "label": "Allow Login using Mobile Number", "length": 0, "no_copy": 0, - "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -877,8 +869,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "column_break_13", - "fieldtype": "Column Break", + "default": "1", + "description": "", + "fieldname": "allow_error_traceback", + "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -886,6 +880,37 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, + "label": "Show Full Error and Allow Reporting of Issues to the Developer", + "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, @@ -906,8 +931,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "Note: Multiple sessions will be allowed in case of mobile device", - "fieldname": "deny_multiple_sessions", + "fieldname": "enable_two_factor_auth", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -916,7 +940,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow only one session per user", + "label": "Enable Two Factor Auth", "length": 0, "no_copy": 0, "permlevel": 0, @@ -937,9 +961,11 @@ "bold": 0, "collapsible": 0, "columns": 0, - "description": "User can login using Email id or Mobile number", - "fieldname": "allow_login_using_mobile_number", - "fieldtype": "Check", + "default": "OTP App", + "depends_on": "", + "description": "Choose authentication method to be used by all users", + "fieldname": "two_factor_method", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -947,9 +973,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Allow Login using Mobile Number", + "label": "Two Factor Authentication method", "length": 0, "no_copy": 0, + "options": "OTP App\nSMS\nEmail", "permlevel": 0, "precision": "", "print_hide": 0, @@ -968,10 +995,10 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "1", - "description": "", - "fieldname": "allow_error_traceback", - "fieldtype": "Check", + "depends_on": "eval:doc.two_factor_method == \"OTP App\"", + "description": "Time in seconds to retain QR code image on server. Min:240", + "fieldname": "lifespan_qrcode_image", + "fieldtype": "Int", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -979,7 +1006,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Show Full Error and Allow Reporting of Issues to the Developer", + "label": "Expiry time of QR Code Image Page", "length": 0, "no_copy": 0, "permlevel": 0, @@ -994,6 +1021,39 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Frappe Framework", + "depends_on": "enable_two_factor_auth", + "fieldname": "otp_issuer_name", + "fieldtype": "Data", + "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": "OTP Issuer Name", + "length": 0, + "no_copy": 0, + "options": "", + "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, @@ -1126,7 +1186,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-08-04 12:05:08.054099", + "modified": "2017-08-07 23:29:18.858797", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index f7ecfc00bb..cd7edc6a53 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -9,6 +9,7 @@ from frappe.model import no_value_fields from frappe.translate import set_default_language from frappe.utils import cint from frappe.utils.momentjs import get_all_timezones +from frappe.twofactor import toggle_two_factor_auth class SystemSettings(Document): def validate(self): @@ -25,6 +26,12 @@ class SystemSettings(Document): if len(parts)!=2 or not (cint(parts[0]) or cint(parts[1])): frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm")) + if self.enable_two_factor_auth: + if self.two_factor_method=='SMS': + if not frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'): + frappe.throw(_('Please setup SMS before setting it as an authentication method, via SMS Settings')) + toggle_two_factor_auth(True, roles=['All']) + def on_update(self): for df in self.meta.get("fields"): if df.fieldtype not in no_value_fields: diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 1b8252477f..5b6b530dc1 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -80,7 +80,7 @@ def update_system_settings(args): 'backup_limit': 3 # Default for downloadable backups }) if args.get("twofactor_enable") == 1: - toggle_two_factor_auth(True,roles=['All']) + toggle_two_factor_auth(True, roles=['All']) system_settings.two_factor_method = args.get('twofactor_method') system_settings.save() diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index 95bd9edd0e..6e33918c6c 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -507,6 +507,7 @@ li { border-top: 1px solid #EBEFF2; } .page_content { + padding-top: 30px; padding-bottom: 30px; } .carousel-control .icon { @@ -554,6 +555,9 @@ li { .panel-body { padding-left: 15px; } +.page-head { + margin-bottom: -30px; +} .page-head h1, .page-head h2 { margin-top: 0px; @@ -813,6 +817,9 @@ a.active { padding: 30px; padding-left: 40px; } +.page-content.without-sidebar { + padding-top: 30px; +} .your-account-info { margin-top: 30px; } diff --git a/frappe/public/less/website.less b/frappe/public/less/website.less index 189ee6275f..87f5065e66 100644 --- a/frappe/public/less/website.less +++ b/frappe/public/less/website.less @@ -125,6 +125,7 @@ li { } .page_content { + padding-top: 30px; padding-bottom: 30px; } @@ -181,6 +182,7 @@ li { } .page-head { + margin-bottom: -30px; h1, h2 { margin-top: 0px; } @@ -504,6 +506,10 @@ a.active { padding-left: 40px; } +.page-content.without-sidebar { + padding-top: 30px; +} + .your-account-info { margin-top: 30px; } diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index cd65cdb250..69e1199254 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -267,7 +267,7 @@ var request_otp = function(r){ var continue_otp_app = function(setup, qrcode){ request_otp(); - var qrcode_div = $('
').attr({'id':'qrcode_div','style':'text-align:center;padding-bottom:15px;'}); + var qrcode_div = $('
'); if (setup){ direction = $('
').attr('id','qr_info').text('Enter Code displayed in OTP App.'); @@ -282,7 +282,7 @@ var continue_otp_app = function(setup, qrcode){ var continue_sms = function(setup, prompt){ request_otp(); - var sms_div = $('
').attr({'id':'sms_div','style':'padding-bottom:15px;text-align:center;'}); + var sms_div = $('
'); if (setup){ sms_div.append(prompt) @@ -296,7 +296,7 @@ var continue_sms = function(setup, prompt){ var continue_email = function(setup, prompt){ request_otp(); - var email_div = $('
').attr({'id':'email_div','style':'padding-bottom:15px;text-align:center;'}); + var email_div = $('
'); if (setup){ email_div.append(prompt) diff --git a/frappe/templates/web.html b/frappe/templates/web.html index cb7a1aa09e..44b5c1cb8c 100644 --- a/frappe/templates/web.html +++ b/frappe/templates/web.html @@ -11,7 +11,7 @@ {% include "templates/includes/web_sidebar.html" %}
{% endif %} -
+
diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 44e3fb24df..3e0fbf69de 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -5,31 +5,27 @@ from __future__ import unicode_literals import frappe from frappe import _ -import pyotp,base64,os +import pyotp, os from frappe.utils.background_jobs import enqueue from jinja2 import Template from pyqrcode import create as qrcreate from StringIO import StringIO -from base64 import b64encode,b32encode +from base64 import b64encode, b32encode from frappe.utils import get_url, get_datetime, time_diff_in_seconds -from frappe.installer import update_site_config +class ExpiredLoginException(Exception): pass -class ExpiredLoginException(Exception):pass - - -def toggle_two_factor_auth(state,roles=[]): +def toggle_two_factor_auth(state, roles=[]): '''Enable or disable 2FA in site_config and roles''' - update_site_config('enable_two_factor_auth',state) + frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 1) for role in roles: - role = frappe.get_doc('Role',{'role_name':role}) + role = frappe.get_doc('Role', {'role_name': role}) role.two_factor_auth = state role.save(ignore_permissions=True) - def two_factor_is_enabled(user=None): '''Returns True if 2FA is enabled.''' - enabled = frappe.local.conf.get('enable_two_factor_auth',False) + enabled = frappe.db.get_value('System Settings', None, 'enable_two_factor_auth') if not user or not enabled: return enabled return two_factor_is_enabled_for_(user) @@ -38,7 +34,6 @@ def should_run_2fa(user): '''Check if 2fa should run.''' return two_factor_is_enabled(user=user) - def get_cached_user_pass(): '''Get user and password if set.''' user = pwd = None @@ -46,23 +41,22 @@ def get_cached_user_pass(): if tmp_id: user = frappe.cache().get(tmp_id+'_usr') pwd = frappe.cache().get(tmp_id+'_pwd') - return (user,pwd) - + return (user, pwd) def authenticate_for_2factor(user): '''Authenticate two factor for enabled user before login.''' - if frappe.form_dict.get('otp'):return + if frappe.form_dict.get('otp'): + return otp_secret = get_otpsecret_for_(user) - verification_method = frappe.db.get_value('System Settings', None, 'two_factor_method') token = int(pyotp.TOTP(otp_secret).now()) tmp_id = frappe.generate_hash(length=8) - cache_2fa_data(user,token,otp_secret,tmp_id) - verification_obj = get_verification_obj(user,token,otp_secret) + cache_2fa_data(user, token, otp_secret, tmp_id) + verification_obj = get_verification_obj(user, token, otp_secret) # Save data in local frappe.local.response['verification'] = verification_obj frappe.local.response['tmp_id'] = tmp_id -def cache_2fa_data(user,token,otp_secret,tmp_id): +def cache_2fa_data(user, token, otp_secret, tmp_id): '''Cache and set expiry for data.''' pwd = frappe.form_dict.get('pwd') verification_method = get_verification_method() @@ -74,20 +68,24 @@ def cache_2fa_data(user,token,otp_secret,tmp_id): frappe.cache().expire(tmp_id + '_token', expiry_time) else: expiry_time = 180 - for k,v in {'_usr':user,'_pwd':pwd,'_otp_secret':otp_secret}.iteritems(): - frappe.cache().set("{0}{1}".format(tmp_id,k),v) - frappe.cache().expire("{0}{1}".format(tmp_id,k),expiry_time) + for k, v in {'_usr': user, '_pwd': pwd, '_otp_secret': otp_secret}.iteritems(): + frappe.cache().set("{0}{1}".format(tmp_id, k), v) + frappe.cache().expire("{0}{1}".format(tmp_id, k), expiry_time) def two_factor_is_enabled_for_(user): '''Check if 2factor is enabled for user.''' - if isinstance(user,basestring): - user = frappe.get_doc('User',user) - if user.roles: - query = """select name from `tabRole` where two_factor_auth=1 - and name in ("All",{0});""".format(', '.join('\"{}\"'.format(i.role) for \ - i in user.roles)) - if len(frappe.db.sql(query)) > 0: - return True + if isinstance(user, basestring): + user = frappe.get_doc('User', user) + + roles = [frappe.db.escape(d.role) for d in user.roles or []] + roles.append('All') + + query = """select name from `tabRole` where two_factor_auth=1 + and name in ({0}) limit 1""".format(', '.join('\"{}\"'.format(i) for \ + i in roles)) + if len(frappe.db.sql(query)) > 0: + return True + return False def get_otpsecret_for_(user): @@ -102,9 +100,7 @@ def get_otpsecret_for_(user): def get_verification_method(): return frappe.db.get_value('System Settings', None, 'two_factor_method') - - -def confirm_otp_token(login_manager,otp=None,tmp_id=None): +def confirm_otp_token(login_manager, otp=None, tmp_id=None): '''Confirm otp matches.''' if not otp: otp = frappe.form_dict.get('otp') @@ -119,7 +115,7 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None): if not otp_secret: raise ExpiredLoginException(_('Login session expired, refresh page to retry')) hotp = pyotp.HOTP(otp_secret) - if hotp_token: + if hotp_token: if hotp.verify(otp, int(hotp_token)): frappe.cache().delete(tmp_id + '_token') return True @@ -137,35 +133,37 @@ def confirm_otp_token(login_manager,otp=None,tmp_id=None): login_manager.fail(_('Incorrect Verification code'), login_manager.user) -def get_verification_obj(user,token,otp_secret): +def get_verification_obj(user, token, otp_secret): otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') verification_method = get_verification_method() verification_obj = None if verification_method == 'SMS': - verification_obj = process_2fa_for_sms(user,token,otp_secret) + verification_obj = process_2fa_for_sms(user, token, otp_secret) elif verification_method == 'OTP App': #check if this if the first time that the user is trying to login. If so, send an email if not frappe.db.get_default(user + '_otplogin'): - verification_obj = process_2fa_for_email(user,token,otp_secret,otp_issuer,method='OTP App') + verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer, method='OTP App') else: - verification_obj = process_2fa_for_otp_app(user,otp_secret,otp_issuer) + verification_obj = process_2fa_for_otp_app(user, otp_secret, otp_issuer) elif verification_method == 'Email': - verification_obj = process_2fa_for_email(user,token,otp_secret,otp_issuer) + verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer) return verification_obj -def process_2fa_for_sms(user,token,otp_secret): +def process_2fa_for_sms(user, token, otp_secret): '''Process sms method for 2fa.''' - phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1) + phone = frappe.db.get_value('User', user, ['phone', 'mobile_no'], as_dict=1) phone = phone.mobile_no or phone.phone - status = send_token_via_sms(otp_secret,token=token, phone_no=phone) - verification_obj = {'token_delivery': status, - 'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]), - 'method': 'SMS', - 'setup': status} + status = send_token_via_sms(otp_secret, token=token, phone_no=phone) + verification_obj = { + 'token_delivery': status, + 'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]), + 'method': 'SMS', + 'setup': status + } return verification_obj -def process_2fa_for_otp_app(user,otp_secret,otp_issuer): +def process_2fa_for_otp_app(user, otp_secret, otp_issuer): '''Process OTP App method for 2fa.''' totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) if frappe.db.get_default(user + '_otplogin'): @@ -173,13 +171,15 @@ def process_2fa_for_otp_app(user,otp_secret,otp_issuer): else: otp_setup_completed = False - verification_obj = {'totp_uri': totp_uri, - 'method': 'OTP App', - 'qrcode': get_qr_svg_code(totp_uri), - 'setup': otp_setup_completed } + verification_obj = { + 'totp_uri': totp_uri, + 'method': 'OTP App', + 'qrcode': get_qr_svg_code(totp_uri), + 'setup': otp_setup_completed + } return verification_obj -def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='Email'): +def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'): '''Process Email method for 2fa.''' subject = None message = None @@ -188,51 +188,53 @@ def process_2fa_for_email(user,token,otp_secret,otp_issuer,method='Email'): if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'): '''Sending one-time email for OTP App''' totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) - qrcode_link = get_link_for_qrcode(user,totp_uri) - message = get_email_body_for_qr_code({'qrcode_link':qrcode_link}) - subject = get_email_subject_for_qr_code({'qrcode_link':qrcode_link}) - 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!!' + qrcode_link = get_link_for_qrcode(user, totp_uri) + message = get_email_body_for_qr_code({'qrcode_link': qrcode_link}) + subject = get_email_subject_for_qr_code({'qrcode_link': qrcode_link}) + 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.') else: '''Sending email verification''' - prompt = 'Verification code has been sent to your registered email address.' - status = send_token_via_email(user,token,otp_secret,otp_issuer,subject=subject,message=message) - verification_obj = {'token_delivery': status, - 'prompt': status and prompt, - 'method': 'Email', - 'setup': status} + prompt = _('Verification code has been sent to your registered email address.') + status = send_token_via_email(user, token, otp_secret, otp_issuer, subject=subject, message=message) + verification_obj = { + 'token_delivery': status, + 'prompt': status and prompt, + 'method': 'Email', + 'setup': status + } return verification_obj def get_email_subject_for_2fa(kwargs_dict): '''Get email subject for 2fa.''' - subject_template = 'Verification Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) - subject = render_string_template(subject_template,kwargs_dict) + subject_template = _('Login Verification Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) + subject = render_string_template(subject_template, kwargs_dict) return subject def get_email_body_for_2fa(kwargs_dict): '''Get email body for 2fa.''' - body_template = 'Use this token to login
{{otp}}' - body = render_string_template(body_template,kwargs_dict) + body_template = 'Enter this code to complete your login:

{{otp}}' + body = render_string_template(body_template, kwargs_dict) return body def get_email_subject_for_qr_code(kwargs_dict): '''Get QRCode email subject.''' - subject_template = 'OTP Registration Code from {}'.format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) - subject = render_string_template(subject_template,kwargs_dict) + subject_template = _('One Time Password (OTP) Registration Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')) + subject = render_string_template(subject_template, kwargs_dict) return subject def get_email_body_for_qr_code(kwargs_dict): '''Get QRCode email body.''' - body_template = 'Please click on the following link and follow the instructions on the page.
{{qrcode_link}}' - body = render_string_template(body_template,kwargs_dict) + body_template = 'Please click on the following link and follow the instructions on the page.

{{qrcode_link}}' + body = render_string_template(body_template, kwargs_dict) return body -def render_string_template(_str,kwargs_dict): +def render_string_template(_str, kwargs_dict): '''Render string with jinja.''' s = Template(_str) s = s.render(**kwargs_dict) return s -def get_link_for_qrcode(user,totp_uri): +def get_link_for_qrcode(user, totp_uri): '''Get link to temporary page showing QRCode.''' key = frappe.generate_hash(length=20) key_user = "{}_user".format(key) @@ -240,8 +242,8 @@ def get_link_for_qrcode(user,totp_uri): lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')) if lifespan<=0: lifespan = 240 - frappe.cache().set_value(key_uri,totp_uri,expires_in_sec=lifespan) - frappe.cache().set_value(key_user,user,expires_in_sec=lifespan) + frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan) + frappe.cache().set_value(key_user, user, expires_in_sec=lifespan) return get_url('/qrcode?k={}'.format(key)) def send_token_via_sms(otpsecret, token=None, phone_no=None): @@ -258,7 +260,7 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None): ss = frappe.get_doc('SMS Settings', 'SMS Settings') if not ss.sms_gateway_url: return False - + hotp = pyotp.HOTP(otpsecret) args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer} for d in ss.get("parameters"): @@ -266,28 +268,35 @@ def send_token_via_sms(otpsecret, token=None, phone_no=None): args[ss.receiver_parameter] = phone_no - sms_args = {'gateway_url':ss.sms_gateway_url,'params':args} + sms_args = {'gateway_url': ss.sms_gateway_url, 'params': args} enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args) return True -def send_token_via_email(user, token, otp_secret, otp_issuer,subject=None,message=None): +def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None): '''Send token to user as email.''' user_email = frappe.db.get_value('User', user, 'email') if not user_email: return False hotp = pyotp.HOTP(otp_secret) otp = hotp.at(int(token)) - template_args = {'otp':otp,'otp_issuer':otp_issuer} + template_args = {'otp': otp, 'otp_issuer': otp_issuer} if not subject: subject = get_email_subject_for_2fa(template_args) if not message: message = get_email_body_for_2fa(template_args) - email_args = { - 'recipients':user_email, 'sender':None, 'subject':subject, - 'message':message, - 'delayed':False, 'retry':3 } - enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) + email_args = { + 'recipients': user_email, + 'sender': None, + 'subject': subject, + 'message': message, + 'header': [_('Verfication Code'), 'blue'], + 'delayed': False, + 'retry':3 + } + + enqueue(method=frappe.sendmail, queue='short', + timeout=300, event=None, async=True, job_name=None, now=False, **email_args) return True def get_qr_svg_code(totp_uri): @@ -297,62 +306,62 @@ def get_qr_svg_code(totp_uri): stream = StringIO() try: url.svg(stream, scale=4, background="#eee", module_color="#222") - svg = stream.getvalue().replace('\n','') + svg = stream.getvalue().replace('\n', '') svg = b64encode(bytes(svg)) finally: stream.close() return svg -def qrcode_as_png(user,totp_uri): +def qrcode_as_png(user, totp_uri): '''Save temporary Qrcode to server.''' from frappe.utils.file_manager import save_file folder = create_barcode_folder() png_file_name = '{}.png'.format(frappe.generate_hash(length=20)) - file_obj = save_file(png_file_name,png_file_name,'User',user,folder=folder) + file_obj = save_file(png_file_name, png_file_name, 'User', user, folder=folder) frappe.db.commit() file_url = get_url(file_obj.file_url) - file_path = os.path.join(frappe.get_site_path('public', 'files'),file_obj.file_name) - url = qrcreate(totp_uri) - with open(file_path,'w') as png_file: - url.png(png_file,scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc]) + file_path = os.path.join(frappe.get_site_path('public', 'files'), file_obj.file_name) + url = qrcreate(totp_uri) + with open(file_path, 'w') as png_file: + url.png(png_file, scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc]) return file_url def create_barcode_folder(): '''Get Barcodes folder.''' folder_name = 'Barcodes' - folder = frappe.db.exists('File',{'file_name':folder_name}) + folder = frappe.db.exists('File', {'file_name': folder_name}) if folder: return folder folder = frappe.get_doc({ - 'doctype':'File', - 'file_name':folder_name, + 'doctype': 'File', + 'file_name': folder_name, 'is_folder':1, - 'folder':'Home' + 'folder': 'Home' }) folder.insert(ignore_permissions=True) return folder.name -def delete_qrimage(user,check_expiry=False): +def delete_qrimage(user, check_expiry=False): '''Delete Qrimage when user logs in.''' - user_barcodes = frappe.get_all('File',{'attached_to_doctype':'User', - 'attached_to_name':user,'folder':'Home/Barcodes'}) + user_barcodes = frappe.get_all('File', {'attached_to_doctype': 'User', + 'attached_to_name': user, 'folder': 'Home/Barcodes'}) for barcode in user_barcodes: - if check_expiry and not should_remove_barcode_image(barcode):continue - barcode = frappe.get_doc('File',barcode.name) - frappe.delete_doc('File',barcode.name,ignore_permissions=True) + if check_expiry and not should_remove_barcode_image(barcode): continue + barcode = frappe.get_doc('File', barcode.name) + frappe.delete_doc('File', barcode.name, ignore_permissions=True) def delete_all_barcodes_for_users(): '''Task to delete all barcodes for user.''' - users = frappe.get_all('User',{'enabled':1}) + users = frappe.get_all('User', {'enabled':1}) for user in users: - delete_qrimage(user.name,check_expiry=True) + delete_qrimage(user.name, check_expiry=True) def should_remove_barcode_image(barcode): '''Check if it's time to delete barcode image from server. ''' if isinstance(barcode, basestring): - barcode = frappe.get_doc('File',barcode) + barcode = frappe.get_doc('File', barcode) lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image') - if time_diff_in_seconds(get_datetime(),barcode.creation) > int(lifespan): + if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan): return True return False diff --git a/frappe/website/router.py b/frappe/website/router.py index 6bf75c8f46..fa79d807dd 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -35,7 +35,6 @@ def get_page_context(path): page_context = make_page_context(path) if can_cache(page_context.no_cache): page_context_cache[frappe.local.lang] = page_context - frappe.cache().hset("page_context", path, page_context_cache) return page_context diff --git a/frappe/website/utils.py b/frappe/website/utils.py index c4f167f2bc..7a80d03f85 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -24,7 +24,11 @@ def find_first_image(html): return None def can_cache(no_cache=False): - return not (frappe.conf.disable_website_cache or getattr(frappe.local, "no_cache", False) or no_cache) + if frappe.conf.disable_website_cache or frappe.conf.developer_mode: + return False + if getattr(frappe.local, "no_cache", False): + return False + return not no_cache def get_comment_list(doctype, name): return frappe.db.sql("""select diff --git a/frappe/www/qrcode.html b/frappe/www/qrcode.html index 3b9c81c531..4cbedb1060 100644 --- a/frappe/www/qrcode.html +++ b/frappe/www/qrcode.html @@ -1,31 +1,27 @@ {% extends "templates/web.html" %} -{% block title %}Register OTP Secret{% endblock %} +{% block title %}{{ _("QR Code") }}{% endblock %} {% block page_content %} -
-
- - - - - -
-
-

- Hi {{qr_code_user.first_name}}, please perform the following actions: -

  • Open your authentication app on your mobile phone, -
  • Scan the QR Code and enter the resulting code displayed -
  • Return to the Verification screen and enter the code displayed by your authentication app -

    -

    Examples of Authentication Apps you can use are Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile. -

    -
  • -
    -
    - -
    -
    +

    {{ _("QR Code for Login Verification") }}

    +
    +
    +

    {{ _("Hi {0}").format(qr_code_user.first_name) }},

    + +

    {{ _("Steps to verify your login") }}:

    +
      +
    1. {{ _("Open your authentication app on your mobile phone.") }} +
    2. {{ _("Scan the QR Code and enter the resulting code displayed.") }} +
    3. {{ _("Return to the Verification screen and enter the code displayed by your authentication app") }} +
    +

    +
    +

    {{ _("Authentication Apps you can use are: ") }} + Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile. +

    +
    +
    +
    {% endblock %} \ No newline at end of file diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py index 636eabac35..bf7d79236e 100644 --- a/frappe/www/qrcode.py +++ b/frappe/www/qrcode.py @@ -8,10 +8,8 @@ from frappe import _ from urlparse import parse_qs from frappe.twofactor import get_qr_svg_code -no_cache = 1 - - def get_context(context): + context.no_cache = 1 context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache() def get_query_key():