Browse Source

[fix] style and move setup to system settings

version-14
Rushabh Mehta 8 years ago
parent
commit
bc4d46a362
13 changed files with 269 additions and 185 deletions
  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 View File

@@ -17,13 +17,12 @@ from frappe.translate import get_lang_code
from frappe.utils.password import check_password from frappe.utils.password import check_password
from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log
from frappe.utils.background_jobs import enqueue 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 from six.moves.urllib.parse import quote


import pyotp,base64,os
import pyotp, base64, os


class HTTPRequest: class HTTPRequest:
def __init__(self): def __init__(self):
@@ -68,7 +67,7 @@ class HTTPRequest:


def validate_csrf_token(self): def validate_csrf_token(self):
if frappe.local.request and frappe.local.request.method=="POST": 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 \ if not frappe.local.session.data.csrf_token \
or frappe.local.session.data.device=="mobile" \ or frappe.local.session.data.device=="mobile" \
or frappe.conf.get('ignore_csrf', None): or frappe.conf.get('ignore_csrf', None):
@@ -95,7 +94,7 @@ class HTTPRequest:
def connect(self, ac_name = None): def connect(self, ac_name = None):
"""connect to db, from ac_name or db_name""" """connect to db, from ac_name or db_name"""
frappe.local.db = frappe.database.Database(user = self.get_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: class LoginManager:
def __init__(self): def __init__(self):
@@ -105,7 +104,7 @@ class LoginManager:
self.user_type = None self.user_type = None


if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": 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 self.resume = False


# run login triggers # run login triggers
@@ -120,20 +119,17 @@ class LoginManager:
self.make_session() self.make_session()
self.set_user_info() self.set_user_info()



def login(self): def login(self):
# clear cache # clear cache
frappe.clear_cache(user = frappe.form_dict.get('usr')) 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): if should_run_2fa(self.user):
authenticate_for_2factor(self.user) authenticate_for_2factor(self.user)
if not confirm_otp_token(self): if not confirm_otp_token(self):
return False return False
self.post_login() self.post_login()




def post_login(self): def post_login(self):
self.run_trigger('on_login') self.run_trigger('on_login')
self.validate_ip_address() self.validate_ip_address()
@@ -198,7 +194,7 @@ class LoginManager:
if not (user and pwd): if not (user and pwd):
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
if not (user and 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")): 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 user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
@@ -220,7 +216,9 @@ class LoginManager:
except frappe.AuthenticationError: except frappe.AuthenticationError:
self.fail('Incorrect password', user=user) 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 frappe.local.response['message'] = message
add_authentication_log(message, user, status="Failed") add_authentication_log(message, user, status="Failed")
frappe.db.commit() frappe.db.commit()


+ 93
- 33
frappe/core/doctype/system_settings/system_settings.json View File

@@ -778,11 +778,8 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -790,10 +787,8 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Two Factor Authentication method",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "OTP App\nSMS\nEmail",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -812,10 +807,9 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -823,7 +817,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Expiry time of QR Code Image Page",
"label": "Allow only one session per user",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -844,10 +838,9 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -855,10 +848,9 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "OTP Issuer Name",
"label": "Allow Login using Mobile Number",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -877,8 +869,10 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"fieldname": "column_break_13",
"fieldtype": "Column Break",
"default": "1",
"description": "",
"fieldname": "allow_error_traceback",
"fieldtype": "Check",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -886,6 +880,37 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 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, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -906,8 +931,7 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@@ -916,7 +940,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Allow only one session per user",
"label": "Enable Two Factor Auth",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -937,9 +961,11 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -947,9 +973,10 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Allow Login using Mobile Number",
"label": "Two Factor Authentication method",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "OTP App\nSMS\nEmail",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -968,10 +995,10 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 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, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -979,7 +1006,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 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, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -994,6 +1021,39 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 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_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -1126,7 +1186,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-08-04 12:05:08.054099",
"modified": "2017-08-07 23:29:18.858797",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "System Settings", "name": "System Settings",


+ 7
- 0
frappe/core/doctype/system_settings/system_settings.py View File

@@ -9,6 +9,7 @@ from frappe.model import no_value_fields
from frappe.translate import set_default_language from frappe.translate import set_default_language
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.momentjs import get_all_timezones from frappe.utils.momentjs import get_all_timezones
from frappe.twofactor import toggle_two_factor_auth


class SystemSettings(Document): class SystemSettings(Document):
def validate(self): def validate(self):
@@ -25,6 +26,12 @@ class SystemSettings(Document):
if len(parts)!=2 or not (cint(parts[0]) or cint(parts[1])): 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")) 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): def on_update(self):
for df in self.meta.get("fields"): for df in self.meta.get("fields"):
if df.fieldtype not in no_value_fields: if df.fieldtype not in no_value_fields:


+ 1
- 1
frappe/desk/page/setup_wizard/setup_wizard.py View File

@@ -80,7 +80,7 @@ def update_system_settings(args):
'backup_limit': 3 # Default for downloadable backups 'backup_limit': 3 # Default for downloadable backups
}) })
if args.get("twofactor_enable") == 1: 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.two_factor_method = args.get('twofactor_method')
system_settings.save() system_settings.save()




+ 7
- 0
frappe/public/css/website.css View File

@@ -507,6 +507,7 @@ li {
border-top: 1px solid #EBEFF2; border-top: 1px solid #EBEFF2;
} }
.page_content { .page_content {
padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
} }
.carousel-control .icon { .carousel-control .icon {
@@ -554,6 +555,9 @@ li {
.panel-body { .panel-body {
padding-left: 15px; padding-left: 15px;
} }
.page-head {
margin-bottom: -30px;
}
.page-head h1, .page-head h1,
.page-head h2 { .page-head h2 {
margin-top: 0px; margin-top: 0px;
@@ -813,6 +817,9 @@ a.active {
padding: 30px; padding: 30px;
padding-left: 40px; padding-left: 40px;
} }
.page-content.without-sidebar {
padding-top: 30px;
}
.your-account-info { .your-account-info {
margin-top: 30px; margin-top: 30px;
} }


+ 6
- 0
frappe/public/less/website.less View File

@@ -125,6 +125,7 @@ li {
} }


.page_content { .page_content {
padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
} }


@@ -181,6 +182,7 @@ li {
} }


.page-head { .page-head {
margin-bottom: -30px;
h1, h2 { h1, h2 {
margin-top: 0px; margin-top: 0px;
} }
@@ -504,6 +506,10 @@ a.active {
padding-left: 40px; padding-left: 40px;
} }


.page-content.without-sidebar {
padding-top: 30px;
}

.your-account-info { .your-account-info {
margin-top: 30px; margin-top: 30px;
} }


+ 3
- 3
frappe/templates/includes/login/login.js View File

@@ -267,7 +267,7 @@ var request_otp = function(r){


var continue_otp_app = function(setup, qrcode){ var continue_otp_app = function(setup, qrcode){
request_otp(); 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){ if (setup){
direction = $('<div>').attr('id','qr_info').text('Enter Code displayed in OTP App.'); 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){ var continue_sms = function(setup, prompt){
request_otp(); 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){ if (setup){
sms_div.append(prompt) sms_div.append(prompt)
@@ -296,7 +296,7 @@ var continue_sms = function(setup, prompt){


var continue_email = function(setup, prompt){ var continue_email = function(setup, prompt){
request_otp(); 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){ if (setup){
email_div.append(prompt) email_div.append(prompt)


+ 1
- 1
frappe/templates/web.html View File

@@ -11,7 +11,7 @@
{% include "templates/includes/web_sidebar.html" %} {% include "templates/includes/web_sidebar.html" %}
</div> </div>
{% endif %} {% 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="page-content-wrapper">
<div class="row page-head"> <div class="row page-head">
<div class='col-sm-12'> <div class='col-sm-12'>


+ 113
- 104
frappe/twofactor.py View File

@@ -5,31 +5,27 @@ from __future__ import unicode_literals


import frappe import frappe
from frappe import _ from frappe import _
import pyotp,base64,os
import pyotp, os
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from jinja2 import Template from jinja2 import Template
from pyqrcode import create as qrcreate from pyqrcode import create as qrcreate
from StringIO import StringIO 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.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''' '''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: 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.two_factor_auth = state
role.save(ignore_permissions=True) role.save(ignore_permissions=True)



def two_factor_is_enabled(user=None): def two_factor_is_enabled(user=None):
'''Returns True if 2FA is enabled.''' '''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: if not user or not enabled:
return enabled return enabled
return two_factor_is_enabled_for_(user) return two_factor_is_enabled_for_(user)
@@ -38,7 +34,6 @@ def should_run_2fa(user):
'''Check if 2fa should run.''' '''Check if 2fa should run.'''
return two_factor_is_enabled(user=user) return two_factor_is_enabled(user=user)



def get_cached_user_pass(): def get_cached_user_pass():
'''Get user and password if set.''' '''Get user and password if set.'''
user = pwd = None user = pwd = None
@@ -46,23 +41,22 @@ def get_cached_user_pass():
if tmp_id: if tmp_id:
user = frappe.cache().get(tmp_id+'_usr') user = frappe.cache().get(tmp_id+'_usr')
pwd = frappe.cache().get(tmp_id+'_pwd') pwd = frappe.cache().get(tmp_id+'_pwd')
return (user,pwd)

return (user, pwd)


def authenticate_for_2factor(user): def authenticate_for_2factor(user):
'''Authenticate two factor for enabled user before login.''' '''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) 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()) token = int(pyotp.TOTP(otp_secret).now())
tmp_id = frappe.generate_hash(length=8) 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 # Save data in local
frappe.local.response['verification'] = verification_obj frappe.local.response['verification'] = verification_obj
frappe.local.response['tmp_id'] = tmp_id 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.''' '''Cache and set expiry for data.'''
pwd = frappe.form_dict.get('pwd') pwd = frappe.form_dict.get('pwd')
verification_method = get_verification_method() 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) frappe.cache().expire(tmp_id + '_token', expiry_time)
else: else:
expiry_time = 180 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): def two_factor_is_enabled_for_(user):
'''Check if 2factor 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 return False


def get_otpsecret_for_(user): def get_otpsecret_for_(user):
@@ -102,9 +100,7 @@ def get_otpsecret_for_(user):
def get_verification_method(): def get_verification_method():
return frappe.db.get_value('System Settings', None, 'two_factor_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.''' '''Confirm otp matches.'''
if not otp: if not otp:
otp = frappe.form_dict.get('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: if not otp_secret:
raise ExpiredLoginException(_('Login session expired, refresh page to retry')) raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
hotp = pyotp.HOTP(otp_secret) hotp = pyotp.HOTP(otp_secret)
if hotp_token:
if hotp_token:
if hotp.verify(otp, int(hotp_token)): if hotp.verify(otp, int(hotp_token)):
frappe.cache().delete(tmp_id + '_token') frappe.cache().delete(tmp_id + '_token')
return True 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) 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') otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
verification_method = get_verification_method() verification_method = get_verification_method()
verification_obj = None verification_obj = None
if verification_method == 'SMS': 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': elif verification_method == 'OTP App':
#check if this if the first time that the user is trying to login. If so, send an email #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'): 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: 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': 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 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.''' '''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 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 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.''' '''Process OTP App method for 2fa.'''
totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
if frappe.db.get_default(user + '_otplogin'): if frappe.db.get_default(user + '_otplogin'):
@@ -173,13 +171,15 @@ def process_2fa_for_otp_app(user,otp_secret,otp_issuer):
else: else:
otp_setup_completed = False 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 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.''' '''Process Email method for 2fa.'''
subject = None subject = None
message = 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'): if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'):
'''Sending one-time email for OTP App''' '''Sending one-time email for OTP App'''
totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) 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: else:
'''Sending email verification''' '''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 return verification_obj


def get_email_subject_for_2fa(kwargs_dict): def get_email_subject_for_2fa(kwargs_dict):
'''Get email subject for 2fa.''' '''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 return subject


def get_email_body_for_2fa(kwargs_dict): def get_email_body_for_2fa(kwargs_dict):
'''Get email body for 2fa.''' '''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 return body


def get_email_subject_for_qr_code(kwargs_dict): def get_email_subject_for_qr_code(kwargs_dict):
'''Get QRCode email subject.''' '''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 return subject


def get_email_body_for_qr_code(kwargs_dict): def get_email_body_for_qr_code(kwargs_dict):
'''Get QRCode email body.''' '''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 return body


def render_string_template(_str,kwargs_dict):
def render_string_template(_str, kwargs_dict):
'''Render string with jinja.''' '''Render string with jinja.'''
s = Template(_str) s = Template(_str)
s = s.render(**kwargs_dict) s = s.render(**kwargs_dict)
return s 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.''' '''Get link to temporary page showing QRCode.'''
key = frappe.generate_hash(length=20) key = frappe.generate_hash(length=20)
key_user = "{}_user".format(key) 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')) lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image'))
if lifespan<=0: if lifespan<=0:
lifespan = 240 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)) return get_url('/qrcode?k={}'.format(key))


def send_token_via_sms(otpsecret, token=None, phone_no=None): 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') ss = frappe.get_doc('SMS Settings', 'SMS Settings')
if not ss.sms_gateway_url: if not ss.sms_gateway_url:
return False return False
hotp = pyotp.HOTP(otpsecret) hotp = pyotp.HOTP(otpsecret)
args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer} 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"): 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 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) enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args)
return True 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.''' '''Send token to user as email.'''
user_email = frappe.db.get_value('User', user, 'email') user_email = frappe.db.get_value('User', user, 'email')
if not user_email: if not user_email:
return False return False
hotp = pyotp.HOTP(otp_secret) hotp = pyotp.HOTP(otp_secret)
otp = hotp.at(int(token)) otp = hotp.at(int(token))
template_args = {'otp':otp,'otp_issuer':otp_issuer}
template_args = {'otp': otp, 'otp_issuer': otp_issuer}
if not subject: if not subject:
subject = get_email_subject_for_2fa(template_args) subject = get_email_subject_for_2fa(template_args)
if not message: if not message:
message = get_email_body_for_2fa(template_args) 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 return True


def get_qr_svg_code(totp_uri): def get_qr_svg_code(totp_uri):
@@ -297,62 +306,62 @@ def get_qr_svg_code(totp_uri):
stream = StringIO() stream = StringIO()
try: try:
url.svg(stream, scale=4, background="#eee", module_color="#222") url.svg(stream, scale=4, background="#eee", module_color="#222")
svg = stream.getvalue().replace('\n','')
svg = stream.getvalue().replace('\n', '')
svg = b64encode(bytes(svg)) svg = b64encode(bytes(svg))
finally: finally:
stream.close() stream.close()
return svg return svg


def qrcode_as_png(user,totp_uri):
def qrcode_as_png(user, totp_uri):
'''Save temporary Qrcode to server.''' '''Save temporary Qrcode to server.'''
from frappe.utils.file_manager import save_file from frappe.utils.file_manager import save_file
folder = create_barcode_folder() folder = create_barcode_folder()
png_file_name = '{}.png'.format(frappe.generate_hash(length=20)) 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() frappe.db.commit()
file_url = get_url(file_obj.file_url) 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 return file_url


def create_barcode_folder(): def create_barcode_folder():
'''Get Barcodes folder.''' '''Get Barcodes folder.'''
folder_name = 'Barcodes' folder_name = 'Barcodes'
folder = frappe.db.exists('File',{'file_name':folder_name})
folder = frappe.db.exists('File', {'file_name': folder_name})
if folder: if folder:
return folder return folder
folder = frappe.get_doc({ folder = frappe.get_doc({
'doctype':'File',
'file_name':folder_name,
'doctype': 'File',
'file_name': folder_name,
'is_folder':1, 'is_folder':1,
'folder':'Home'
'folder': 'Home'
}) })
folder.insert(ignore_permissions=True) folder.insert(ignore_permissions=True)
return folder.name return folder.name


def delete_qrimage(user,check_expiry=False):
def delete_qrimage(user, check_expiry=False):
'''Delete Qrimage when user logs in.''' '''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: 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(): def delete_all_barcodes_for_users():
'''Task to delete all barcodes for user.''' '''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: for user in users:
delete_qrimage(user.name,check_expiry=True)
delete_qrimage(user.name, check_expiry=True)


def should_remove_barcode_image(barcode): def should_remove_barcode_image(barcode):
'''Check if it's time to delete barcode image from server. ''' '''Check if it's time to delete barcode image from server. '''
if isinstance(barcode, basestring): 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') 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 True
return False return False



+ 0
- 1
frappe/website/router.py View File

@@ -35,7 +35,6 @@ def get_page_context(path):
page_context = make_page_context(path) page_context = make_page_context(path)
if can_cache(page_context.no_cache): if can_cache(page_context.no_cache):
page_context_cache[frappe.local.lang] = page_context page_context_cache[frappe.local.lang] = page_context

frappe.cache().hset("page_context", path, page_context_cache) frappe.cache().hset("page_context", path, page_context_cache)


return page_context return page_context


+ 5
- 1
frappe/website/utils.py View File

@@ -24,7 +24,11 @@ def find_first_image(html):
return None return None


def can_cache(no_cache=False): 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): def get_comment_list(doctype, name):
return frappe.db.sql("""select return frappe.db.sql("""select


+ 20
- 24
frappe/www/qrcode.html View File

@@ -1,31 +1,27 @@
{% extends "templates/web.html" %} {% extends "templates/web.html" %}


{% block title %}Register OTP Secret{% endblock %}
{% block title %}{{ _("QR Code") }}{% endblock %}


{% block page_content %} {% 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>
</div> </div>
{% endblock %} {% endblock %}

+ 1
- 3
frappe/www/qrcode.py View File

@@ -8,10 +8,8 @@ from frappe import _
from urlparse import parse_qs from urlparse import parse_qs
from frappe.twofactor import get_qr_svg_code from frappe.twofactor import get_qr_svg_code


no_cache = 1


def get_context(context): def get_context(context):
context.no_cache = 1
context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache() context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache()


def get_query_key(): def get_query_key():


Loading…
Cancel
Save