diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index d408d1be4..3131e7c36 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -23,7 +23,8 @@ angular.module('app') } }) .controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, - syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) { + syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager, + privilegesManager) { authManager.checkForSecurityUpdate().then((available) => { this.securityUpdateAvailable = available; @@ -173,6 +174,26 @@ angular.module('app') } this.selectRoom = function(room) { - room.showRoom = !room.showRoom; + let run = () => { + room.showRoom = !room.showRoom; + } + + if(!room.showRoom) { + // About to show, check if has privileges + if(privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => { + run(); + }); + } + } else { + run(); + } + } + + this.clickOutsideAccountMenu = function() { + if(privilegesManager.authenticationInProgress()) { + return; + } + this.showAccountMenu = false; } }); diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js index 6d8805094..c83a1c0a4 100644 --- a/app/assets/javascripts/app/controllers/home.js +++ b/app/assets/javascripts/app/controllers/home.js @@ -1,6 +1,7 @@ angular.module('app') .controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, - dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) { + dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager, + privilegesManager) { storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession()); diff --git a/app/assets/javascripts/app/controllers/lockScreen.js b/app/assets/javascripts/app/controllers/lockScreen.js index e38eff9f6..8f3a9efd0 100644 --- a/app/assets/javascripts/app/controllers/lockScreen.js +++ b/app/assets/javascripts/app/controllers/lockScreen.js @@ -41,7 +41,6 @@ class LockScreen { }) } } - } angular.module('app').directive('lockScreen', () => new LockScreen); diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index d24684515..b49dfff81 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -10,7 +10,7 @@ class AccountMenu { } controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager, - $timeout, $compile, archiveManager) { + $timeout, $compile, archiveManager, privilegesManager) { 'ngInject'; $scope.formData = {mergeLocal: true, ephemeral: false}; @@ -317,7 +317,17 @@ class AccountMenu { */ $scope.downloadDataArchive = function() { - archiveManager.downloadBackup($scope.archiveFormData.encrypted); + let run = () => { + archiveManager.downloadBackup($scope.archiveFormData.encrypted); + } + + if(privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDownloadBackup)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDownloadBackup, () => { + run(); + }); + } else { + run(); + } } /* diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js new file mode 100644 index 000000000..56dd04aba --- /dev/null +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -0,0 +1,60 @@ +/* + The purpose of the conflict resoltion modal is to present two versions of a conflicted item, + and allow the user to choose which to keep (or to keep both.) +*/ + +class PrivilegesAuthModal { + + constructor() { + this.restrict = "E"; + this.templateUrl = "directives/privileges-auth-modal.html"; + this.scope = { + action: "=", + onSuccess: "=", + onCancel: "=", + }; + } + + link($scope, el, attrs) { + $scope.dismiss = function() { + el.remove(); + } + } + + controller($scope, privilegesManager, $timeout) { + 'ngInject'; + + $scope.privileges = privilegesManager.privilegesForAction($scope.action); + + $scope.cancel = function() { + $scope.dismiss(); + $scope.onCancel && $scope.onCancel(); + } + + $scope.doesPrivHaveFail = function(priv) { + if(!$scope.failedPrivs) { + return false; + } + return $scope.failedPrivs.find((failedPriv) => { + return failedPriv.name == priv.name; + }) != null; + } + + $scope.submit = function() { + privilegesManager.verifyPrivilegesForAction($scope.action, $scope.privileges).then((result) => { + console.log("Result", result); + $timeout(() => { + if(result.success) { + $scope.onSuccess(); + $scope.dismiss(); + } else { + $scope.failedPrivs = result.failedPrivs; + } + }) + }) + } + + } +} + +angular.module('app').directive('privilegesAuthModal', () => new PrivilegesAuthModal); diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 6787d3709..47e169a68 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -92,6 +92,13 @@ class AuthManager extends SFAuthManager { } } + async verifyAccountPassword(password) { + let authParams = await this.getAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(password, authParams); + let success = keys.mk === (await this.keys()).mk; + return success; + } + async checkForSecurityUpdate() { if(this.offline()) { return false; diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 47c9e763b..8d6fad7fa 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -36,6 +36,18 @@ angular.module('app') return authParams; } + this.verifyPasscode = async function(passcode) { + return new Promise(async (resolve, reject) => { + var params = this.passcodeAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); + if(keys.pw !== params.hash) { + resolve(false); + } else { + resolve(true); + } + }) + } + this.unlock = function(passcode, callback) { var params = this.passcodeAuthParams(); SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js new file mode 100644 index 000000000..a621e6d5b --- /dev/null +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -0,0 +1,107 @@ +class PrivilegesManager { + + constructor(passcodeManager, authManager, $rootScope, $compile) { + this.passcodeManager = passcodeManager; + this.authManager = authManager; + this.$rootScope = $rootScope; + this.$compile = $compile; + + PrivilegesManager.PrivilegeAccountPassword = "PrivilegeAccountPassword"; + PrivilegesManager.PrivilegeLocalPasscode = "PrivilegeLocalPasscode"; + + PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; + PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; + } + + presentPrivilegesModal(action, onSuccess, onCancel) { + + let customSuccess = () => { + onSuccess(); + this.currentAuthenticationElement = null; + } + + let customCancel = () => { + onCancel(); + this.currentAuthenticationElement = null; + } + + var scope = this.$rootScope.$new(true); + scope.action = action; + scope.onSuccess = customSuccess; + scope.onCancel = customCancel; + var el = this.$compile( "" )(scope); + angular.element(document.body).append(el); + + this.currentAuthenticationElement = el; + } + + authenticationInProgress() { + return this.currentAuthenticationElement != null; + } + + privilegesForAction(action) { + return [ + { + name: PrivilegesManager.PrivilegeAccountPassword, + label: "Account Password", + prompt: "Please enter your account password." + }, + { + name: PrivilegesManager.PrivilegeLocalPasscode, + label: "Local Passcode", + prompt: "Please enter your local passcode." + } + ] + } + + actionRequiresPrivilege(action) { + return this.privilegesForAction(action).length > 0; + } + + async verifyPrivilegesForAction(action, inputPrivs) { + + let findInputPriv = (name) => { + return inputPrivs.find((priv) => { + return priv.name == name; + }) + } + + var requiredPrivileges = this.privilegesForAction(action); + var successfulPrivs = [], failedPrivs = []; + for(let requiredPriv of requiredPrivileges) { + var matchingPriv = findInputPriv(requiredPriv.name); + var passesAuth = await this.verifyAuthenticationParameters(matchingPriv); + if(passesAuth) { + successfulPrivs.push(matchingPriv); + } else { + failedPrivs.push(matchingPriv); + } + } + + return { + success: failedPrivs.length == 0, + successfulPrivs: successfulPrivs, + failedPrivs: failedPrivs + } + } + + async verifyAuthenticationParameters(parameters) { + + let verifyAccountPassword = async (password) => { + return this.authManager.verifyAccountPassword(password); + } + + let verifyLocalPasscode = async (passcode) => { + return this.passcodeManager.verifyPasscode(passcode); + } + + if(parameters.name == PrivilegesManager.PrivilegeAccountPassword) { + return verifyAccountPassword(parameters.authenticationValue); + } else if(parameters.name == PrivilegesManager.PrivilegeLocalPasscode) { + return verifyLocalPasscode(parameters.authenticationValue); + } + } + +} + +angular.module('app').service('privilegesManager', PrivilegesManager); diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml new file mode 100644 index 000000000..a83f99b80 --- /dev/null +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -0,0 +1,17 @@ +.background{"ng-click" => "cancel()"} + +.content#privileges-modal + .sn-component + .panel + .header + %h1.title Authentication Required + %a.close-button.info{"ng-click" => "cancel()"} Cancel + .content + .panel-section + .panel-row{"ng-repeat" => "priv in privileges"} + %p {{priv.prompt}} + %input{"type" => "password", "ng-model" => "priv.authenticationValue"} + %label.danger{"ng-if" => "doesPrivHaveFail(priv)"} Invalid authentication. Please try again. + + .footer + .button.info.big.block.bold{"ng-click" => "submit()"} Submit diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml index c4dfd0171..8ba4a11cf 100644 --- a/app/assets/templates/footer.html.haml +++ b/app/assets/templates/footer.html.haml @@ -1,7 +1,7 @@ .sn-component #footer-bar.app-bar.no-edges .left - .item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"} + .item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.clickOutsideAccountMenu()", "is-open" => "ctrl.showAccountMenu"} .column .circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'default')"} .column