Просмотр исходного кода

[fix] style and move setup to system settings

version-14
Rushabh Mehta 8 лет назад
Родитель
Сommit
bc4d46a362
13 измененных файлов: 269 добавлений и 185 удалений
  1. +12
    -14
      frappe/auth.py
  2. +93
    -33
      frappe/core/doctype/system_settings/system_settings.json
  3. +7
    -0
      frappe/core/doctype/system_settings/system_settings.py
  4. +1
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  5. +7
    -0
      frappe/public/css/website.css
  6. +6
    -0
      frappe/public/less/website.less
  7. +3
    -3
      frappe/templates/includes/login/login.js
  8. +1
    -1
      frappe/templates/web.html
  9. +113
    -104
      frappe/twofactor.py
  10. +0
    -1
      frappe/website/router.py
  11. +5
    -1
      frappe/website/utils.py
  12. +20
    -24
      frappe/www/qrcode.html
  13. +1
    -3
      frappe/www/qrcode.py

+ 12
- 14
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()


+ 93
- 33
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:<strong>240</strong>",
"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:<strong>240</strong>",
"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",


+ 7
- 0
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:


+ 1
- 1
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()



+ 7
- 0
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;
}


+ 6
- 0
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;
}


+ 3
- 3
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 = $('<div>').attr({'id':'qrcode_div','style':'text-align:center;padding-bottom:15px;'});
var qrcode_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');

if (setup){
direction = $('<div>').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 = $('<div>').attr({'id':'sms_div','style':'padding-bottom:15px;text-align:center;'});
var sms_div = $('<div class="text-muted" style="padding-bottom: 15px;"></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 = $('<div>').attr({'id':'email_div','style':'padding-bottom:15px;text-align:center;'});
var email_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');

if (setup){
email_div.append(prompt)


+ 1
- 1
frappe/templates/web.html Просмотреть файл

@@ -11,7 +11,7 @@
{% include "templates/includes/web_sidebar.html" %}
</div>
{% endif %}
<div class="{% if show_sidebar %}page-content with-sidebar{% else %} page-content {% endif %}">
<div class="{% if show_sidebar %}page-content with-sidebar{% else %}page-content without-sidebar{% endif %}">
<div class="page-content-wrapper">
<div class="row page-head">
<div class='col-sm-12'>


+ 113
- 104
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 <br> {{otp}}'
body = render_string_template(body_template,kwargs_dict)
body_template = 'Enter this code to complete your login:<br><br> <b>{{otp}}</b>'
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.<br> {{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.<br><br> {{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


+ 0
- 1
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


+ 5
- 1
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


+ 20
- 24
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 %}
<div>
<div style="text-align:center">
<table>
<tr>
<td width="50%">
<div style="margin:auto;text-align:left;">
<p>
Hi {{qr_code_user.first_name}}, please perform the following actions:
<li> Open your authentication app on your mobile phone,
<li> Scan the QR Code and enter the resulting code displayed
<li> Return to the Verification screen and enter the code displayed by your authentication app
</p>
<p>Examples of Authentication Apps you can use are Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile.
</p>
</div>
</td>
<td>
<div style="padding:10px;">
<img src="data:image/svg+xml;base64,{{qrcode_svg}}">
</div>
</td>
</tr>
</table>
<h1>{{ _("QR Code for Login Verification") }}</h1>
<div class='row'>
<div class='col-sm-6'>
<p>{{ _("Hi {0}").format(qr_code_user.first_name) }},</p>

<p>{{ _("Steps to verify your login") }}:</p>
<ol>
<li> {{ _("Open your authentication app on your mobile phone.") }}
<li> {{ _("Scan the QR Code and enter the resulting code displayed.") }}
<li> {{ _("Return to the Verification screen and enter the code displayed by your authentication app") }}
</ol>
</p>
<br>
<p class='text-muted small'>{{ _("Authentication Apps you can use are: ") }}
Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile.
</p>
</div>
<div class='col-sm-6' style='padding-top: 15px;'>
<img src="data:image/svg+xml;base64,{{qrcode_svg}}">
</div>
</div>
{% endblock %}

+ 1
- 3
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():


Загрузка…
Отмена
Сохранить