diff --git a/src/app/constants.js b/src/app/constants.js index c81c135b69..b0e4ce69ff 100644 --- a/src/app/constants.js +++ b/src/app/constants.js @@ -26,7 +26,8 @@ angular.module('bit') duo: 2, authenticator: 0, email: 1, - remember: 5 + remember: 5, + organizationDuo: 6 }, cipherType: { login: 1, @@ -153,6 +154,19 @@ angular.module('bit') requiresUsb: false } ], + orgTwoFactorProviderInfo: [ + { + type: 6, + name: 'Duo', + description: 'Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.', + enabled: false, + active: true, + image: 'duo.png', + displayOrder: 1, + priority: 0, + requiresUsb: false + } + ], plans: { free: { basePrice: 0, diff --git a/src/app/organization/organizationSettingsController.js b/src/app/organization/organizationSettingsController.js index df08f98a44..1ce066f644 100644 --- a/src/app/organization/organizationSettingsController.js +++ b/src/app/organization/organizationSettingsController.js @@ -2,11 +2,14 @@ .module('bit.organization') .controller('organizationSettingsController', function ($scope, $state, apiService, toastr, authService, $uibModal, - $analytics, appSettings) { + $analytics, appSettings, constants, $filter) { $scope.selfHosted = appSettings.selfHosted; $scope.model = {}; + $scope.twoStepProviders = constants.orgTwoFactorProviderInfo; + $scope.use2fa = false; + $scope.$on('$viewContentLoaded', function () { - apiService.organizations.get({ id: $state.params.orgId }, function (org) { + apiService.organizations.get({ id: $state.params.orgId }).$promise.then(function (org) { $scope.model = { name: org.Name, billingEmail: org.BillingEmail, @@ -17,6 +20,29 @@ businessCountry: org.BusinessCountry, businessTaxNumber: org.BusinessTaxNumber }; + + $scope.use2fa = org.Use2fa; + if (org.Use2fa) { + return apiService.twoFactor.listOrganization({ orgId: $state.params.orgId }).$promise; + } + else { + return null; + } + }).then(function (response) { + if (!response || !response.Data) { + return; + } + + for (var i = 0; i < response.Data.length; i++) { + if (!response.Data[i].Enabled) { + continue; + } + + var provider = $filter('filter')($scope.twoStepProviders, { type: response.Data[i].Type }); + if (provider.length) { + provider[0].enabled = true; + } + } }); }); @@ -56,4 +82,30 @@ controller: 'organizationDeleteController' }); }; + + $scope.edit = function (provider) { + if (provider.type === constants.twoFactorProvider.organizationDuo) { + typeName = 'Duo'; + } + else { + return; + } + + var modal = $uibModal.open({ + animation: true, + templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html', + controller: 'settingsTwoStep' + typeName + 'Controller', + resolve: { + enabled: function () { return provider.enabled; }, + orgId: function () { return $state.params.orgId; } + } + }); + + modal.result.then(function (enabled) { + if (enabled || enabled === false) { + // do not adjust when undefined or null + provider.enabled = enabled; + } + }); + }; }); diff --git a/src/app/organization/views/organizationSettings.html b/src/app/organization/views/organizationSettings.html index 7894248570..55cf237982 100644 --- a/src/app/organization/views/organizationSettings.html +++ b/src/app/organization/views/organizationSettings.html @@ -63,6 +63,36 @@ +
+
+

Two-step Login Providers

+
+
+
+ + + + + + + + +
+ + {{::provider.name}} + + + {{::provider.name}} +
{{::provider.description}}
+
+ + {{provider.enabled ? 'Enabled' : 'Disabled'}} + +
+
+
+

Import/Export

diff --git a/src/app/services/apiService.js b/src/app/services/apiService.js index 84a1c9473e..d533434628 100644 --- a/src/app/services/apiService.js +++ b/src/app/services/apiService.js @@ -160,9 +160,11 @@ _service.twoFactor = $resource(_apiUri + '/two-factor', {}, { list: { method: 'GET', params: {} }, + listOrganization: { url: _apiUri + '/organizations/:orgId/two-factor', method: 'GET', params: { orgId: '@orgId' } }, getEmail: { url: _apiUri + '/two-factor/get-email', method: 'POST', params: {} }, getU2f: { url: _apiUri + '/two-factor/get-u2f', method: 'POST', params: {} }, getDuo: { url: _apiUri + '/two-factor/get-duo', method: 'POST', params: {} }, + getOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/get-duo', method: 'POST', params: { orgId: '@orgId' } }, getAuthenticator: { url: _apiUri + '/two-factor/get-authenticator', method: 'POST', params: {} }, getYubi: { url: _apiUri + '/two-factor/get-yubikey', method: 'POST', params: {} }, sendEmail: { url: _apiUri + '/two-factor/send-email', method: 'POST', params: {} }, @@ -171,8 +173,10 @@ putU2f: { url: _apiUri + '/two-factor/u2f', method: 'POST', params: {} }, putAuthenticator: { url: _apiUri + '/two-factor/authenticator', method: 'POST', params: {} }, putDuo: { url: _apiUri + '/two-factor/duo', method: 'POST', params: {} }, + putOrganizationDuo: { url: _apiUri + '/organizations/:orgId/two-factor/duo', method: 'POST', params: { orgId: '@orgId' } }, putYubi: { url: _apiUri + '/two-factor/yubikey', method: 'POST', params: {} }, disable: { url: _apiUri + '/two-factor/disable', method: 'POST', params: {} }, + disableOrganization: { url: _apiUri + '/organizations/:orgId/two-factor/disable', method: 'POST', params: { orgId: '@orgId' } }, recover: { url: _apiUri + '/two-factor/recover', method: 'POST', params: {} }, getRecover: { url: _apiUri + '/two-factor/get-recover', method: 'POST', params: {} } }); diff --git a/src/app/services/authService.js b/src/app/services/authService.js index 33d76db57e..c69ec0cece 100644 --- a/src/app/services/authService.js +++ b/src/app/services/authService.js @@ -152,6 +152,7 @@ angular useGroups: profile.Organizations[i].UseGroups, useDirectory: profile.Organizations[i].UseDirectory, useEvents: profile.Organizations[i].UseEvents, + use2fa: profile.Organizations[i].Use2fa, useTotp: profile.Organizations[i].UseTotp }; } @@ -187,6 +188,7 @@ angular useGroups: org.UseGroups, useDirectory: org.UseDirectory, useEvents: org.UseEvents, + use2fa: org.Use2fa, useTotp: org.UseTotp }; profile.organizations[o.id] = o; diff --git a/src/app/settings/settingsTwoStepController.js b/src/app/settings/settingsTwoStepController.js index 2cc753a5a0..630a9f10bf 100644 --- a/src/app/settings/settingsTwoStepController.js +++ b/src/app/settings/settingsTwoStepController.js @@ -60,7 +60,8 @@ templateUrl: 'app/settings/views/settingsTwoStep' + typeName + '.html', controller: 'settingsTwoStep' + typeName + 'Controller', resolve: { - enabled: function () { return provider.enabled; } + enabled: function () { return provider.enabled; }, + orgId: function () { return null; } } }); diff --git a/src/app/settings/settingsTwoStepDuoController.js b/src/app/settings/settingsTwoStepDuoController.js index 20ed5144c0..ad1b14fd07 100644 --- a/src/app/settings/settingsTwoStepDuoController.js +++ b/src/app/settings/settingsTwoStepDuoController.js @@ -2,7 +2,7 @@ .module('bit.settings') .controller('settingsTwoStepDuoController', function ($scope, apiService, $uibModalInstance, cryptoService, - toastr, $analytics, constants, $timeout) { + toastr, $analytics, constants, $timeout, orgId) { $analytics.eventTrack('settingsTwoStepDuoController', { category: 'Modal' }); var _masterPasswordHash; @@ -20,9 +20,16 @@ $scope.auth = function (model) { $scope.authPromise = cryptoService.hashPassword(model.masterPassword).then(function (hash) { _masterPasswordHash = hash; - return apiService.twoFactor.getDuo({}, { + var requestModel = { masterPasswordHash: _masterPasswordHash - }).$promise; + }; + + if (orgId) { + return apiService.twoFactor.getOrganizationDuo({ orgId: orgId }, requestModel).$promise; + } + else { + return apiService.twoFactor.getDuo({}, requestModel).$promise; + } }).then(function (apiResponse) { processResult(apiResponse); $scope.authed = true; @@ -43,27 +50,52 @@ return; } - $scope.submitPromise = apiService.twoFactor.disable({}, { - masterPasswordHash: _masterPasswordHash, - type: constants.twoFactorProvider.duo - }, function (response) { - $analytics.eventTrack('Disabled Two-step Duo'); - toastr.success('Duo has been disabled.'); - $scope.enabled = response.Enabled; - $scope.close(); - }).$promise; + if (orgId) { + $scope.submitPromise = apiService.twoFactor.disableOrganization({ orgId: orgId }, { + masterPasswordHash: _masterPasswordHash, + type: constants.twoFactorProvider.organizationDuo + }, function (response) { + $analytics.eventTrack('Disabled Two-step Organization Duo'); + toastr.success('Duo has been disabled.'); + $scope.enabled = response.Enabled; + $scope.close(); + }).$promise; + } + else { + $scope.submitPromise = apiService.twoFactor.disable({}, { + masterPasswordHash: _masterPasswordHash, + type: constants.twoFactorProvider.duo + }, function (response) { + $analytics.eventTrack('Disabled Two-step Duo'); + toastr.success('Duo has been disabled.'); + $scope.enabled = response.Enabled; + $scope.close(); + }).$promise; + } } function update(model) { - $scope.submitPromise = apiService.twoFactor.putDuo({}, { + var requestModel = { integrationKey: model.ikey, secretKey: model.skey, host: model.host, masterPasswordHash: _masterPasswordHash - }, function (response) { - $analytics.eventTrack('Enabled Two-step Duo'); - processResult(response); - }).$promise; + }; + + if (orgId) { + $scope.submitPromise = apiService.twoFactor.putOrganizationDuo({ orgId: orgId }, requestModel, + function (response) { + $analytics.eventTrack('Enabled Two-step Organization Duo'); + processResult(response); + }).$promise; + } + else { + $scope.submitPromise = apiService.twoFactor.putDuo({}, requestModel, + function (response) { + $analytics.eventTrack('Enabled Two-step Duo'); + processResult(response); + }).$promise; + } } function processResult(response) { @@ -80,7 +112,7 @@ closing = true; $uibModalInstance.close($scope.enabled); }; - + $scope.$on('modal.closing', function (e, reason, closed) { if (closing) { return;