From e9c52903add7ca5193a7b350d2d0c4195c3b8c86 Mon Sep 17 00:00:00 2001 From: Jorge Lobo Date: Fri, 28 Jun 2019 15:01:53 +0200 Subject: [PATCH] F #2497: two factor authentication Signed-off-by: Jorge Lobo --- install.sh | 5 +- share/install_gems/Gemfile | 2 + src/sunstone/etc/sunstone-server.conf | 4 + .../etc/sunstone-views/kvm/admin.yaml | 3 + .../etc/sunstone-views/kvm/cloud.yaml | 2 + .../etc/sunstone-views/kvm/groupadmin.yaml | 3 + src/sunstone/etc/sunstone-views/kvm/user.yaml | 3 + .../etc/sunstone-views/mixed/admin.yaml | 3 + .../etc/sunstone-views/mixed/cloud.yaml | 2 + .../etc/sunstone-views/mixed/groupadmin.yaml | 3 + .../etc/sunstone-views/mixed/user.yaml | 3 + .../etc/sunstone-views/vcenter/admin.yaml | 3 + .../etc/sunstone-views/vcenter/cloud.yaml | 2 + .../sunstone-views/vcenter/groupadmin.yaml | 3 + .../etc/sunstone-views/vcenter/user.yaml | 3 + .../models/OpenNebulaJSON/JSONUtils.rb | 9 ++ .../models/OpenNebulaJSON/UserJSON.rb | 41 ++++++-- src/sunstone/models/my_qr_code.rb | 32 +++++++ src/sunstone/models/my_totp.rb | 37 ++++++++ src/sunstone/models/two_factor_auth.rb | 24 +++++ src/sunstone/public/app/login.js | 14 ++- src/sunstone/public/app/opennebula/auth.js | 3 +- src/sunstone/public/app/opennebula/user.js | 8 ++ src/sunstone/public/app/tabs/settings-tab.js | 3 +- .../tabs/settings-tab/panels/user-config.js | 32 ++++++- .../settings-tab/panels/user-config/html.hbs | 34 +++++++ .../public/app/tabs/users-tab/actions.js | 28 ++++++ .../tabs/users-tab/dialogs/two-factor-auth.js | 93 +++++++++++++++++++ .../dialogs/two-factor-auth/dialogId.js | 19 ++++ .../dialogs/two-factor-auth/html.hbs | 67 +++++++++++++ .../app/tabs/users-tab/panels/auth-common.js | 49 ++++++---- .../app/tabs/users-tab/panels/auth/html.hbs | 11 +++ src/sunstone/public/css/login.css | 18 +++- src/sunstone/sunstone-server.rb | 34 +++++++ src/sunstone/views/_login_standard.erb | 42 ++++----- src/sunstone/views/_login_x509.erb | 31 ++----- src/sunstone/views/login.erb | 34 +++++-- 37 files changed, 621 insertions(+), 86 deletions(-) create mode 100644 src/sunstone/models/my_qr_code.rb create mode 100644 src/sunstone/models/my_totp.rb create mode 100644 src/sunstone/models/two_factor_auth.rb create mode 100644 src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth.js create mode 100644 src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/dialogId.js create mode 100644 src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/html.hbs diff --git a/install.sh b/install.sh index 47dbca2f9bb..5dd6287086c 100755 --- a/install.sh +++ b/install.sh @@ -1927,7 +1927,10 @@ SUNSTONE_ETC_VIEW_MIXED="src/sunstone/etc/sunstone-views/mixed/admin.yaml \ SUNSTONE_MODELS_FILES="src/sunstone/models/OpenNebulaJSON.rb \ src/sunstone/models/SunstoneServer.rb \ - src/sunstone/models/SunstoneViews.rb" + src/sunstone/models/SunstoneViews.rb \ + src/sunstone/models/my_qr_code.rb \ + src/sunstone/models/my_totp.rb \ + src/sunstone/models/two_factor_auth.rb " SUNSTONE_MODELS_JSON_FILES="src/sunstone/models/OpenNebulaJSON/HostJSON.rb \ src/sunstone/models/OpenNebulaJSON/ImageJSON.rb \ diff --git a/share/install_gems/Gemfile b/share/install_gems/Gemfile index e03ab46bb1b..84560c3fde5 100644 --- a/share/install_gems/Gemfile +++ b/share/install_gems/Gemfile @@ -46,6 +46,8 @@ gem 'sinatra' # sunstone, cloud, oneflow gem 'thin' # sunstone, cloud gem 'memcache-client' # sunstone gem 'zendesk_api' # sunstone +gem 'rotp' # sunstone +gem 'rqrcode' # sunstone gem 'amazon-ec2' # cloud gem 'uuidtools' # cloud gem 'curb' # cloud diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index fc87e1a956e..f7401c18a5b 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -92,6 +92,10 @@ # :core_auth: cipher +# Two Factor Authentication Issuer Label +# JORGE +:two_factor_auth_issuer: Sunstone + ################################################################################ # Check Upgrades ################################################################################ diff --git a/src/sunstone/etc/sunstone-views/kvm/admin.yaml b/src/sunstone/etc/sunstone-views/kvm/admin.yaml index f66ef98997b..097337b095e 100644 --- a/src/sunstone/etc/sunstone-views/kvm/admin.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/admin.yaml @@ -154,6 +154,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: true User.chgrp: true @@ -883,6 +884,7 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true @@ -890,6 +892,7 @@ tabs: Settings.ssh_key: true Settings.login_token: true # Edit button in settings_quotas_tab + User.two_factor_auth: true User.quotas_dialog: false upgrade-top-tab: panel_tabs: diff --git a/src/sunstone/etc/sunstone-views/kvm/cloud.yaml b/src/sunstone/etc/sunstone-views/kvm/cloud.yaml index 35df83c14d2..c6406e73cf1 100644 --- a/src/sunstone/etc/sunstone-views/kvm/cloud.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/cloud.yaml @@ -120,6 +120,7 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true @@ -128,6 +129,7 @@ tabs: Settings.login_token: true # Edit button in settings_quotas_tab User.quotas_dialog: false + User.two_factor_auth: true vms-tab: actions: *provisionactions images-tab: diff --git a/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml b/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml index e7f203182df..1ac30296cb0 100644 --- a/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/groupadmin.yaml @@ -153,6 +153,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: false User.chgrp: false @@ -884,12 +885,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/kvm/user.yaml b/src/sunstone/etc/sunstone-views/kvm/user.yaml index ba3481855cc..37c2cc14c1e 100644 --- a/src/sunstone/etc/sunstone-views/kvm/user.yaml +++ b/src/sunstone/etc/sunstone-views/kvm/user.yaml @@ -146,6 +146,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: true User.chgrp: true @@ -877,12 +878,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/mixed/admin.yaml b/src/sunstone/etc/sunstone-views/mixed/admin.yaml index 2db9785c82b..2f92946bb20 100644 --- a/src/sunstone/etc/sunstone-views/mixed/admin.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/admin.yaml @@ -154,6 +154,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: true User.chgrp: true @@ -886,12 +887,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/mixed/cloud.yaml b/src/sunstone/etc/sunstone-views/mixed/cloud.yaml index 35df83c14d2..8d123eed726 100644 --- a/src/sunstone/etc/sunstone-views/mixed/cloud.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/cloud.yaml @@ -120,12 +120,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false vms-tab: diff --git a/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml b/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml index e7f203182df..1ac30296cb0 100644 --- a/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/groupadmin.yaml @@ -153,6 +153,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: false User.chgrp: false @@ -884,12 +885,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/mixed/user.yaml b/src/sunstone/etc/sunstone-views/mixed/user.yaml index ba3481855cc..37c2cc14c1e 100644 --- a/src/sunstone/etc/sunstone-views/mixed/user.yaml +++ b/src/sunstone/etc/sunstone-views/mixed/user.yaml @@ -146,6 +146,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: true User.chgrp: true @@ -877,12 +878,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/admin.yaml b/src/sunstone/etc/sunstone-views/vcenter/admin.yaml index ff5fdf7c24d..db6c5dd0356 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/admin.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/admin.yaml @@ -152,6 +152,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: true User.chgrp: true @@ -883,12 +884,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml b/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml index 2d40b8e3ec6..021e985aa74 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/cloud.yaml @@ -121,12 +121,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false vms-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml b/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml index 37bc512e013..ca398d3f27b 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/groupadmin.yaml @@ -153,6 +153,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: false User.chgrp: false @@ -884,12 +885,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/etc/sunstone-views/vcenter/user.yaml b/src/sunstone/etc/sunstone-views/vcenter/user.yaml index ba3481855cc..37c2cc14c1e 100644 --- a/src/sunstone/etc/sunstone-views/vcenter/user.yaml +++ b/src/sunstone/etc/sunstone-views/vcenter/user.yaml @@ -146,6 +146,7 @@ tabs: User.create_dialog: true User.update_password: true User.login_token: true + User.two_factor_auth: true User.quotas_dialog: true User.groups_dialog: true User.chgrp: true @@ -877,12 +878,14 @@ tabs: # Buttons for settings_info_tab User.update_password: true User.login_token: true + User.two_factor_auth: true # Buttons for settings_config_tab Settings.change_language: true Settings.change_password: true Settings.change_view: true Settings.ssh_key: true Settings.login_token: true + Settings.two_factor_auth: true # Edit button in settings_quotas_tab User.quotas_dialog: false upgrade-top-tab: diff --git a/src/sunstone/models/OpenNebulaJSON/JSONUtils.rb b/src/sunstone/models/OpenNebulaJSON/JSONUtils.rb index 62a39b41a56..525873c217a 100644 --- a/src/sunstone/models/OpenNebulaJSON/JSONUtils.rb +++ b/src/sunstone/models/OpenNebulaJSON/JSONUtils.rb @@ -61,6 +61,15 @@ def parse_json_sym(json_str, root_element) end end + def template_to_str_sunstone_with_explicite_empty_value(attributes) + result = template_to_str(attributes, indent=true) + if result == "" + "SUNSTONE=[]\n" + else + result + end + end + def template_to_str(attributes, indent=true) if indent ind_enter="\n" diff --git a/src/sunstone/models/OpenNebulaJSON/UserJSON.rb b/src/sunstone/models/OpenNebulaJSON/UserJSON.rb index b62e92b6a1d..c9268b294a5 100644 --- a/src/sunstone/models/OpenNebulaJSON/UserJSON.rb +++ b/src/sunstone/models/OpenNebulaJSON/UserJSON.rb @@ -15,6 +15,7 @@ #--------------------------------------------------------------------------- # require 'OpenNebulaJSON/JSONUtils' +require 'two_factor_auth' module OpenNebulaJSON class UserJSON < OpenNebula::User @@ -39,14 +40,16 @@ def perform_action(template_json) end rc = case action_hash['perform'] - when "passwd" then self.passwd(action_hash['params']) - when "chgrp" then self.chgrp(action_hash['params']) - when "chauth" then self.chauth(action_hash['params']) - when "update" then self.update(action_hash['params']) - when "set_quota" then self.set_quota(action_hash['params']) - when "addgroup" then self.addgroup(action_hash['params']) - when "delgroup" then self.delgroup(action_hash['params']) - when "login" then self.login(action_hash['params']) + when "passwd" then self.passwd(action_hash['params']) + when "chgrp" then self.chgrp(action_hash['params']) + when "chauth" then self.chauth(action_hash['params']) + when "update" then self.update(action_hash['params']) + when "enable_two_factor_auth" then self.enable_two_factor_auth(action_hash['params']) + when "disable_two_factor_auth" then self.disable_two_factor_auth(action_hash['params']) + when "set_quota" then self.set_quota(action_hash['params']) + when "addgroup" then self.addgroup(action_hash['params']) + when "delgroup" then self.delgroup(action_hash['params']) + when "login" then self.login(action_hash['params']) else error_msg = "#{action_hash['perform']} action not " << " available for this resource" @@ -74,6 +77,28 @@ def update(params=Hash.new) end end + def enable_two_factor_auth(params=Hash.new) + unless TwoFactorAuth.authenticate(params["secret"], params["token"]) + return OpenNebula::Error.new("Invalid token.") + end + + sunstone_setting = { + "sunstone" => params["current_sunstone_setting"].merge("TWO_FACTOR_AUTH_SECRET" => params["secret"]) + } + template_raw = template_to_str(sunstone_setting) + update_params = { "template_raw" => template_raw, "append" => true } + update(update_params) + end + + def disable_two_factor_auth(params=Hash.new) + sunstone_setting = params["current_sunstone_setting"] + sunstone_setting.delete("TWO_FACTOR_AUTH_SECRET") + sunstone_setting = { "sunstone" => sunstone_setting } + template_raw = template_to_str_sunstone_with_explicite_empty_value(sunstone_setting) + update_params = { "template_raw" => template_raw, "append" => true } + update(update_params) + end + def set_quota(params=Hash.new) quota_json = params['quotas'] quota_template = template_to_str(quota_json) diff --git a/src/sunstone/models/my_qr_code.rb b/src/sunstone/models/my_qr_code.rb new file mode 100644 index 00000000000..4831c36a737 --- /dev/null +++ b/src/sunstone/models/my_qr_code.rb @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2018, OpenNebula Project, OpenNebula Systems # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'rqrcode' + +class MyQrCode + def self.build(code) + qr_code = RQRCode::QRCode.new(code) + new(qr_code) + end + + def initialize(qr_code) + @qr_code = qr_code + end + + def as_svg + @qr_code.as_svg + end +end diff --git a/src/sunstone/models/my_totp.rb b/src/sunstone/models/my_totp.rb new file mode 100644 index 00000000000..33e5707505c --- /dev/null +++ b/src/sunstone/models/my_totp.rb @@ -0,0 +1,37 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2018, OpenNebula Project, OpenNebula Systems # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'rotp' + +class MyTotp + def self.build(secret, issuer) + totp = ROTP::TOTP.new(secret, issuer: issuer) + new(totp) + end + + def initialize(totp) + @totp = totp + @five_minutes = 5 * 60 + end + + def verify(token) + @totp.verify(token, drift_ahead: @five_minutes, drift_behind: @five_minutes) + end + + def provisioning_uri(account_name) + @totp.provisioning_uri(account_name) + end +end diff --git a/src/sunstone/models/two_factor_auth.rb b/src/sunstone/models/two_factor_auth.rb new file mode 100644 index 00000000000..d0541aa2bfc --- /dev/null +++ b/src/sunstone/models/two_factor_auth.rb @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------- # +# Copyright 2002-2018, OpenNebula Project, OpenNebula Systems # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'my_totp' + +module TwoFactorAuth + def self.authenticate(secret, token) + totp = MyTotp.build(secret, nil) + totp.verify(token) + end +end diff --git a/src/sunstone/public/app/login.js b/src/sunstone/public/app/login.js index 269a6a52abc..4be9605a4ee 100644 --- a/src/sunstone/public/app/login.js +++ b/src/sunstone/public/app/login.js @@ -19,7 +19,13 @@ define(function(require) { var OpenNebulaAuth = require('opennebula/auth'); function auth_success(req, response) { - window.location.href = "."; + if (response.code === "two_factor_auth") { + $("#login_form").fadeOut("slow"); + $("#login_spinner").hide(); + $("#two_factor_auth").fadeIn("slow"); + } else { + window.location.href = "."; + } } function auth_error(req, error) { @@ -47,6 +53,7 @@ define(function(require) { var username = $("#username").val(); var password = $("#password").val(); var remember = $("#check_remember").is(":checked"); + var two_factor_auth_token = $("#two_factor_auth_token").val(); $("#error_box").fadeOut("slow"); $("#login_spinner").show(); @@ -58,6 +65,7 @@ define(function(require) { }, remember: remember, success: auth_success, + two_factor_auth_token: two_factor_auth_token, error: auth_error }); } @@ -93,6 +101,10 @@ define(function(require) { return false; }); + $("#two_factor_auth_login").click(function() { + authenticate(); + }); + //compact login elements according to screen height if (screen.height <= 600) { $('div#logo_sunstone').css("top", "15px"); diff --git a/src/sunstone/public/app/opennebula/auth.js b/src/sunstone/public/app/opennebula/auth.js index 23f44a21a7c..c1320d6893d 100644 --- a/src/sunstone/public/app/opennebula/auth.js +++ b/src/sunstone/public/app/opennebula/auth.js @@ -27,13 +27,14 @@ define(function(require) { var username = params.data.username; var password = params.data.password; var remember = params.remember; + var two_factor_auth_token = params.two_factor_auth_token; var request = OpenNebulaHelper.request(RESOURCE, "login"); $.ajax({ url: "login", type: "POST", - data: {remember: remember}, + data: {remember: remember, two_factor_auth_token: two_factor_auth_token}, beforeSend : function(req) { if (username && password) { var token = username + ':' + password; diff --git a/src/sunstone/public/app/opennebula/user.js b/src/sunstone/public/app/opennebula/user.js index 46da82b5e6a..cd408d4f32f 100644 --- a/src/sunstone/public/app/opennebula/user.js +++ b/src/sunstone/public/app/opennebula/user.js @@ -100,6 +100,14 @@ define(function(require) { var action_obj = {"template_raw" : params.data.extra_param, append : true}; OpenNebulaAction.simple_action(params, RESOURCE, "update", action_obj); }, + "enable_sunstone_two_factor_auth": function(params) { + var action_obj = params.data.extra_param; + OpenNebulaAction.simple_action(params, RESOURCE, "enable_two_factor_auth", action_obj); + }, + "disable_sunstone_two_factor_auth": function(params) { + var action_obj = params.data.extra_param; + OpenNebulaAction.simple_action(params, RESOURCE, "disable_two_factor_auth", action_obj); + }, "accounting" : function(params) { OpenNebulaAction.monitor(params, RESOURCE, false); }, diff --git a/src/sunstone/public/app/tabs/settings-tab.js b/src/sunstone/public/app/tabs/settings-tab.js index f82af23aaee..171d16831e9 100644 --- a/src/sunstone/public/app/tabs/settings-tab.js +++ b/src/sunstone/public/app/tabs/settings-tab.js @@ -29,7 +29,8 @@ define(function(require) { var _dialogs = [ require("tabs/users-tab/dialogs/password"), - require("./users-tab/dialogs/login-token") + require('./users-tab/dialogs/login-token'), + require('./users-tab/dialogs/two-factor-auth') ]; var _panels = [ diff --git a/src/sunstone/public/app/tabs/settings-tab/panels/user-config.js b/src/sunstone/public/app/tabs/settings-tab/panels/user-config.js index d3a7f3ad461..ee87fbb5870 100644 --- a/src/sunstone/public/app/tabs/settings-tab/panels/user-config.js +++ b/src/sunstone/public/app/tabs/settings-tab/panels/user-config.js @@ -43,6 +43,7 @@ define(function(require) { var RESOURCE = "User"; var XML_ROOT = "USER"; var LOGIN_TOKEN_DIALOG_ID = require("tabs/users-tab/dialogs/login-token/dialogId"); + var TWO_FACTOR_AUTH_DIALOG_ID = require('tabs/users-tab/dialogs/two-factor-auth/dialogId'); /* CONSTRUCTOR @@ -93,14 +94,41 @@ define(function(require) { $("#provision_user_views_select option[value=\"" + config["user_config"]["default_view"] + "\"]", context).attr("selected", "selected"); + if (that.element.TEMPLATE.SUNSTONE && that.element.TEMPLATE.SUNSTONE.TWO_FACTOR_AUTH_SECRET) { + $(".provision_two_factor_auth_button", context).html(Locale.tr("Disable")); + } else { + $(".provision_two_factor_auth_button", context).html(Locale.tr("Manage two factor authentication")); + } + // Login token button context.off("click", ".provision_login_token_button"); context.on("click", ".provision_login_token_button", function(){ - Sunstone.getDialog(LOGIN_TOKEN_DIALOG_ID).setParams({element: that.element}); - Sunstone.getDialog(LOGIN_TOKEN_DIALOG_ID).reset(); + //Sunstone.getDialog(LOGIN_TOKEN_DIALOG_ID).setParams({element: that.element}); + //Sunstone.getDialog(LOGIN_TOKEN_DIALOG_ID).reset(); Sunstone.getDialog(LOGIN_TOKEN_DIALOG_ID).show(); }); + // Two factor auth button + context.off("click", ".provision_two_factor_auth_button"); + context.on("click", ".provision_two_factor_auth_button", function(){ + var sunstone_setting = that.element.TEMPLATE.SUNSTONE || {}; + if (sunstone_setting.TWO_FACTOR_AUTH_SECRET) { + Sunstone.runAction( + "User.disable_sunstone_two_factor_auth", + that.element.ID, + {current_sunstone_setting: sunstone_setting} + ); + } else { + Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({ + element: that.element, + sunstone_setting: sunstone_setting + }); + Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset(); + Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show(); + } + }); + + $("#provision_change_password_form").submit(function() { var pw = $("#provision_new_password", this).val(); var confirm_password = $("#provision_new_confirm_password", this).val(); diff --git a/src/sunstone/public/app/tabs/settings-tab/panels/user-config/html.hbs b/src/sunstone/public/app/tabs/settings-tab/panels/user-config/html.hbs index 886f39a48c5..dde7b2f54fe 100644 --- a/src/sunstone/public/app/tabs/settings-tab/panels/user-config/html.hbs +++ b/src/sunstone/public/app/tabs/settings-tab/panels/user-config/html.hbs @@ -210,4 +210,38 @@ {{/isTabActionEnabled}} + {{#isTabActionEnabled "settings-tab" "Settings.two_factor_auth"}} +
+
+
+
+
+ +
+ + + + +
+
+ + {{tr "Two Factor Authentication"}} +
+
+
+

+ {{tr "Two factor authentication can be enabled for loging into Sunestone UI."}} +

+
+
+ +
+
+
+
+
+
+
+
+ {{/isTabActionEnabled}} \ No newline at end of file diff --git a/src/sunstone/public/app/tabs/users-tab/actions.js b/src/sunstone/public/app/tabs/users-tab/actions.js index 9ca8664bf44..c6345851c0b 100644 --- a/src/sunstone/public/app/tabs/users-tab/actions.js +++ b/src/sunstone/public/app/tabs/users-tab/actions.js @@ -166,6 +166,34 @@ define(function(require) { error: Notifier.onError }, + "User.enable_sunstone_two_factor_auth" : { + type: "single", + call: OpenNebulaResource.enable_sunstone_two_factor_auth, + callback: function(request) { + var reqId = request.request.data[0]; + if (reqId == config['user_id'] || reqId == "-1") { + window.location.href = "."; + } else { + Sunstone.runAction(RESOURCE+'.show',reqId); + } + }, + error: Notifier.onError + }, + + "User.disable_sunstone_two_factor_auth" : { + type: "single", + call: OpenNebulaResource.disable_sunstone_two_factor_auth, + callback: function(request) { + var reqId = request.request.data[0]; + if (reqId == config['user_id'] || reqId == "-1") { + window.location.href = "."; + } else { + Sunstone.runAction(RESOURCE+'.show',reqId); + } + }, + error: Notifier.onError + }, + "User.append_sunstone_setting_refresh" : { type: "single", call: function(params){ diff --git a/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth.js b/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth.js new file mode 100644 index 00000000000..5a07b55833a --- /dev/null +++ b/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth.js @@ -0,0 +1,93 @@ +/* -------------------------------------------------------------------------- */ +/* Copyright 2002-2018, OpenNebula Project, OpenNebula Systems */ +/* */ +/* Licensed under the Apache License, Version 2.0 (the "License"); you may */ +/* not use this file except in compliance with the License. You may obtain */ +/* a copy of the License at */ +/* */ +/* http://www.apache.org/licenses/LICENSE-2.0 */ +/* */ +/* Unless required by applicable law or agreed to in writing, software */ +/* distributed under the License is distributed on an "AS IS" BASIS, */ +/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ +/* See the License for the specific language governing permissions and */ +/* limitations under the License. */ +/* -------------------------------------------------------------------------- */ + +define(function(require) { + /* DEPENDENCIES */ + + var BaseDialog = require('utils/dialogs/dialog'); + var TemplateHTML = require('hbs!./two-factor-auth/html'); + var Sunstone = require('sunstone'); + var Notifier = require('utils/notifier'); + var Locale = require('utils/locale'); + var OpenNebula = require('opennebula'); + var ResourceSelect = require('utils/resource-select'); + + /* CONSTANTS */ + + var DIALOG_ID = require('./two-factor-auth/dialogId'); + var USERS_TAB_ID = require('../tabId'); + + /* CONSTRUCTOR */ + + function Dialog() { + this.dialogId = DIALOG_ID; + BaseDialog.call(this); + } + + Dialog.DIALOG_ID = DIALOG_ID; + Dialog.prototype = Object.create(BaseDialog.prototype); + Dialog.prototype.constructor = Dialog; + Dialog.prototype.html = _html; + Dialog.prototype.onShow = _onShow; + Dialog.prototype.setup = _setup; + Dialog.prototype.setParams = _setParams; + return Dialog; + + /* FUNCTION DEFINITIONS */ + + function _setParams(params) { + this.element = params.element; + this.sunstone_setting = params.sunstone_setting; + } + + function _html() { + return TemplateHTML({ + 'dialogId': this.dialogId + }); + } + + function _setup(context) { + var that = this; + var secret = randomBase32(); + $("#secret", context).val(secret); + $('#qr_code', context).html('' + secret + ''); + $("#enable_btn", context).click(function(){ + var secret = $("#secret", context).val(); + var token = $("#token", context).val(); + Sunstone.runAction( + "User.enable_sunstone_two_factor_auth", + that.element.ID, + {current_sunstone_setting: that.sunstone_setting, secret: secret, token: token} + ); + }); + + return false; + } + + function randomBase32() { + const items = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'); + const result = Array(16).fill(0).map(() => items[Math.floor(Math.random()*items.length)]); + return result.join(''); + }; + + function _onShow(context) { + var tabId = Sunstone.getTab(); + if (tabId == USERS_TAB_ID){ + this.setNames( Sunstone.getDataTable(USERS_TAB_ID).elements({names: true}) ); + } + return false; + } +}); \ No newline at end of file diff --git a/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/dialogId.js b/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/dialogId.js new file mode 100644 index 00000000000..52156b7fe57 --- /dev/null +++ b/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/dialogId.js @@ -0,0 +1,19 @@ +/* -------------------------------------------------------------------------- */ +/* Copyright 2002-2018, OpenNebula Project, OpenNebula Systems */ +/* */ +/* Licensed under the Apache License, Version 2.0 (the "License"); you may */ +/* not use this file except in compliance with the License. You may obtain */ +/* a copy of the License at */ +/* */ +/* http://www.apache.org/licenses/LICENSE-2.0 */ +/* */ +/* Unless required by applicable law or agreed to in writing, software */ +/* distributed under the License is distributed on an "AS IS" BASIS, */ +/* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ +/* See the License for the specific language governing permissions and */ +/* limitations under the License. */ +/* -------------------------------------------------------------------------- */ + +define(function(require){ + return 'userTwoFactorAuthDialog'; +}); \ No newline at end of file diff --git a/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/html.hbs b/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/html.hbs new file mode 100644 index 00000000000..89066686713 --- /dev/null +++ b/src/sunstone/public/app/tabs/users-tab/dialogs/two-factor-auth/html.hbs @@ -0,0 +1,67 @@ +{{! -------------------------------------------------------------------------- }} +{{! Copyright 2002-2018, OpenNebula Project, OpenNebula Systems }} +{{! }} +{{! Licensed under the Apache License, Version 2.0 (the "License"); you may }} +{{! not use this file except in compliance with the License. You may obtain }} +{{! a copy of the License at }} +{{! }} +{{! http://www.apache.org/licenses/LICENSE-2.0 }} +{{! }} +{{! Unless required by applicable law or agreed to in writing, software }} +{{! distributed under the License is distributed on an "AS IS" BASIS, }} +{{! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. }} +{{! See the License for the specific language governing permissions and }} +{{! limitations under the License. }} +{{! -------------------------------------------------------------------------- }} + + \ No newline at end of file diff --git a/src/sunstone/public/app/tabs/users-tab/panels/auth-common.js b/src/sunstone/public/app/tabs/users-tab/panels/auth-common.js index 08806552629..c921d8edbc3 100644 --- a/src/sunstone/public/app/tabs/users-tab/panels/auth-common.js +++ b/src/sunstone/public/app/tabs/users-tab/panels/auth-common.js @@ -15,9 +15,7 @@ /* -------------------------------------------------------------------------- */ define(function(require) { - /* - DEPENDENCIES - */ + /* DEPENDENCIES */ var TemplateInfo = require('hbs!./auth/html'); var ResourceSelect = require('utils/resource-select'); @@ -27,30 +25,24 @@ define(function(require) { var Sunstone = require('sunstone'); var UserCreation = require('tabs/users-tab/utils/user-creation'); - /* - TEMPLATES - */ + /* TEMPLATES */ var TemplateTable = require('utils/panel/template-table'); - /* - CONSTANTS - */ + /* CONSTANTS */ var RESOURCE = "User"; var XML_ROOT = "USER"; var PASSWORD_DIALOG_ID = require('tabs/users-tab/dialogs/password/dialogId'); var LOGIN_TOKEN_DIALOG_ID = require('tabs/users-tab/dialogs/login-token/dialogId'); + var TWO_FACTOR_AUTH_DIALOG_ID = require('tabs/users-tab/dialogs/two-factor-auth/dialogId'); var CONFIRM_DIALOG_ID = require('utils/dialogs/generic-confirm/dialogId'); - /* - CONSTRUCTOR - */ + /* CONSTRUCTOR */ function Panel(info) { this.title = Locale.tr("Auth"); this.icon = "fa-key"; - this.element = info[XML_ROOT]; this.userCreation = new UserCreation(this.tabId, {name: false, password: false, group_select: false}); return this; @@ -61,9 +53,7 @@ define(function(require) { return Panel; - /* - FUNCTION DEFINITIONS - */ + /* FUNCTION DEFINITIONS */ function _html() { @@ -106,6 +96,33 @@ define(function(require) { TemplateTable.setup(strippedTemplate, RESOURCE, this.element.ID, context, hiddenValues); //=== + // Change two factor auth + if (that.element.TEMPLATE.SUNSTONE && that.element.TEMPLATE.SUNSTONE.TWO_FACTOR_AUTH_SECRET) { + $("#manage_two_factor_auth", context).html(Locale.tr("Disable")); + } else { + if (that.element.ID == config['user_id']) { + $("#manage_two_factor_auth", context).html(Locale.tr("Manage two factor authentication")); + } else { + $("#manage_two_factor_auth", context).prop("disabled", true); + $("#manage_two_factor_auth", context).html(Locale.tr("No")); + } + } + context.off("click", "#manage_two_factor_auth"); + context.on("click", "#manage_two_factor_auth", function() { + var sunstone_setting = that.element.TEMPLATE.SUNSTONE || {}; + if (sunstone_setting.TWO_FACTOR_AUTH_SECRET) { + Sunstone.runAction( + "User.disable_sunstone_two_factor_auth", + that.element.ID, + {current_sunstone_setting: sunstone_setting} + ); + } else { + Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).setParams({element: that.element, sunstone_setting: sunstone_setting}); + Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).reset(); + Sunstone.getDialog(TWO_FACTOR_AUTH_DIALOG_ID).show(); + } + }); + // View password button context.off("click", "#view_password"); context.on("click", "#view_password", function(){ diff --git a/src/sunstone/public/app/tabs/users-tab/panels/auth/html.hbs b/src/sunstone/public/app/tabs/users-tab/panels/auth/html.hbs index f4ac10cad63..9b81d1b712b 100644 --- a/src/sunstone/public/app/tabs/users-tab/panels/auth/html.hbs +++ b/src/sunstone/public/app/tabs/users-tab/panels/auth/html.hbs @@ -33,6 +33,17 @@ {{/isTabActionEnabled}} + {{#isTabActionEnabled tabId "User.two_factor_auth"}} + + {{tr "Two factor authtentication"}} + + + + + + {{/isTabActionEnabled}} {{tr "Password"}} diff --git a/src/sunstone/public/css/login.css b/src/sunstone/public/css/login.css index 908248b5b6e..edb9627b94e 100644 --- a/src/sunstone/public/css/login.css +++ b/src/sunstone/public/css/login.css @@ -149,11 +149,23 @@ div#login input#login_btn:hover { background: url(../images/login_over.png) no-repeat center ; } +div#two_factor_auth button { + width: 130px; + height: 45px; + cursor: pointer; + margin-left: auto; + margin-right: 43px; + margin-top: 20px; + background-color: transparent; + border: 0; + float: right; + background: url(../images/login_over.png) no-repeat center ; +} #login_spinner { - left: 22px; - position: relative; - top: 2px; + left: 22px; + position: relative; + top: 2px; } #label_remember { diff --git a/src/sunstone/sunstone-server.rb b/src/sunstone/sunstone-server.rb index 25d97360fbb..64af4b0ea3d 100755 --- a/src/sunstone/sunstone-server.rb +++ b/src/sunstone/sunstone-server.rb @@ -55,6 +55,7 @@ GROUP_ADMIN_DEFAULT_VIEW_XPATH = 'TEMPLATE/SUNSTONE/GROUP_ADMIN_DEFAULT_VIEW' TABLE_DEFAULT_PAGE_LENGTH_XPATH = 'TEMPLATE/SUNSTONE/TABLE_DEFAULT_PAGE_LENGTH' LANG_XPATH = 'TEMPLATE/SUNSTONE/LANG' +TWO_FACTOR_AUTH_SECRET_XPATH = 'TEMPLATE/SUNSTONE/TWO_FACTOR_AUTH_SECRET' DEFAULT_ZONE_ENDPOINT_XPATH = 'TEMPLATE/SUNSTONE/DEFAULT_ZONE_ENDPOINT' ONED_CONF_OPTS = { @@ -97,6 +98,9 @@ require 'uri' require 'open3' +require "my_qr_code" +require "my_totp" +require "two_factor_auth" require 'CloudAuth' require 'SunstoneServer' require 'SunstoneViews' @@ -205,6 +209,7 @@ DEFAULT_TABLE_ORDER = "desc" DEFAULT_PAGE_LENGTH = 10 +DEFAULT_TWO_FACTOR_AUTH = false SUPPORT = { :zendesk_url => "https://opennebula.zendesk.com/api/v2", @@ -322,6 +327,26 @@ def build_session end client_active_endpoint = $cloud_auth.client(result, session[:active_zone_endpoint]) + # two factor_auth + two_factor_auth = + if user[TWO_FACTOR_AUTH_SECRET_XPATH] + user[TWO_FACTOR_AUTH_SECRET_XPATH] != "" + else + DEFAULT_TWO_FACTOR_AUTH + end + + if two_factor_auth + two_factor_auth_token = params[:two_factor_auth_token] + if !two_factor_auth_token || two_factor_auth_token == "" + return [202, { code: "two_factor_auth" }.to_json] + else + unless TwoFactorAuth.authenticate(user[TWO_FACTOR_AUTH_SECRET_XPATH], two_factor_auth_token) + logger.info { "Unauthorized two factor authentication login attempt" } + return [401, ""] + end + end + end + session[:user] = user['NAME'] session[:user_id] = user['ID'] session[:user_gid] = user['GID'] @@ -542,6 +567,15 @@ def destroy_session end end +get '/two_factor_auth_hotp_qr_code' do + content_type 'image/svg+xml' + + totp = MyTotp.build(params[:secret], $conf[:two_factor_auth_issuer]) + totp_uri = totp.provisioning_uri(session[:user]) + qr_code = MyQrCode.build(totp_uri) + [200, qr_code.as_svg] +end + get '/vnc' do content_type 'text/html', :charset => 'utf-8' if !authorized? diff --git a/src/sunstone/views/_login_standard.erb b/src/sunstone/views/_login_standard.erb index 26fcaf71dca..fe53315f906 100644 --- a/src/sunstone/views/_login_standard.erb +++ b/src/sunstone/views/_login_standard.erb @@ -1,27 +1,17 @@ -
-
-
- -
-
-
- Username - - Password - -
- <% if $conf[:keep_me_logged] %> - - - <% end %> - - retrieving -
- + +
+
+ Username + + Password + +
+ <% if $conf[:keep_me_logged] %> + + + <% end %> + + retrieving
- -
+
+ \ No newline at end of file diff --git a/src/sunstone/views/_login_x509.erb b/src/sunstone/views/_login_x509.erb index a18b0dc98ad..209744198ef 100644 --- a/src/sunstone/views/_login_x509.erb +++ b/src/sunstone/views/_login_x509.erb @@ -1,22 +1,11 @@ -
-
-
- -
-
-
- - <% if $conf[:keep_me_logged] %> - - - <% end %> -
- + +
+
+ + <% if $conf[:keep_me_logged] %> + + + <% end %>
- -
+
+ \ No newline at end of file diff --git a/src/sunstone/views/login.erb b/src/sunstone/views/login.erb index 6e2fe1065b3..eb9c9025b32 100644 --- a/src/sunstone/views/login.erb +++ b/src/sunstone/views/login.erb @@ -21,11 +21,33 @@ - <% if (settings.config[:auth] == "x509") || (settings.config[:auth] == "remote") %> - <%= erb :_login_x509 %> - <% else %> - <%= erb :_login_standard %> - <% end %> +
+
+
+ + <% if (settings.config[:auth] == "x509") || (settings.config[:auth] == "remote") %> + <%= erb :_login_x509 %> + <% else %> + <%= erb :_login_standard %> + <% end %> + + + +
- + \ No newline at end of file