From c174036c842b1393a26da60d6b0d7a07046b9140 Mon Sep 17 00:00:00 2001 From: Shu Muto Date: Wed, 29 Nov 2017 17:44:37 +0900 Subject: [PATCH] Add Change Password Action for Angular users panel To Test - set 'users_panel' to True in settings.py Change-Id: I779b26d34658ea5f3222ebf31f1401bc7a43960b Partially-Implements: blueprint ng-users --- openstack_dashboard/api/keystone.py | 2 +- openstack_dashboard/api/rest/keystone.py | 5 + .../identity/users/actions/actions.module.js | 10 ++ .../users/actions/password.action.service.js | 109 +++++++++++++++ .../actions/password.action.service.spec.js | 130 ++++++++++++++++++ .../error.admin-password-incorrect.html | 1 + .../users/actions/workflow/error.default.html | 1 + .../actions/workflow/info.password.help.html | 1 + .../actions/workflow/workflow.service.js | 47 +++++-- .../local/local_settings.py.example | 3 +- 10 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.js create mode 100644 openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.spec.js create mode 100644 openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.admin-password-incorrect.html create mode 100644 openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.default.html create mode 100644 openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/info.password.help.html diff --git a/openstack_dashboard/api/keystone.py b/openstack_dashboard/api/keystone.py index 5cc37cd7e8..fecd3f552d 100644 --- a/openstack_dashboard/api/keystone.py +++ b/openstack_dashboard/api/keystone.py @@ -537,7 +537,7 @@ def user_verify_admin_password(request, admin_password): # verify if it's correct. client = keystone_client_v2 if VERSIONS.active < 3 else keystone_client_v3 try: - endpoint = _get_endpoint_url(request, 'internalURL') + endpoint = _get_endpoint_url(request, 'publicURL') insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) client.Client( diff --git a/openstack_dashboard/api/rest/keystone.py b/openstack_dashboard/api/rest/keystone.py index 2f2e2c7cb0..ce29e7a9c9 100644 --- a/openstack_dashboard/api/rest/keystone.py +++ b/openstack_dashboard/api/rest/keystone.py @@ -153,6 +153,11 @@ class User(generic.View): user = api.keystone.user_get(request, id) if 'password' in keys: + if getattr(settings, 'ENFORCE_PASSWORD_CHECK', False): + admin_password = request.DATA['admin_password'] + if not api.keystone.user_verify_admin_password(request, + admin_password): + raise rest_utils.AjaxError(400, 'ADMIN_PASSWORD_INCORRECT') password = request.DATA['password'] api.keystone.user_update_password(request, user, password) diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/actions.module.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/actions.module.js index bbdfaee09a..eff5a0d813 100644 --- a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/actions.module.js +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/actions.module.js @@ -35,6 +35,7 @@ 'horizon.framework.conf.resource-type-registry.service', 'horizon.dashboard.identity.users.actions.create.service', 'horizon.dashboard.identity.users.actions.update.service', + 'horizon.dashboard.identity.users.actions.password.service', 'horizon.dashboard.identity.users.actions.delete.service', 'horizon.dashboard.identity.users.resourceType' ]; @@ -43,6 +44,7 @@ registry, createService, updateService, + passwordService, deleteService, userResourceTypeCode ) { @@ -57,6 +59,14 @@ type: 'row' } }) + .append({ + id: 'passwordAction', + service: passwordService, + template: { + text: gettext('Change Password'), + type: 'row' + } + }) .append({ id: 'deleteAction', service: deleteService, diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.js new file mode 100644 index 0000000000..af939a3e77 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.js @@ -0,0 +1,109 @@ +/** + * 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. + */ + +(function() { + 'use strict'; + + angular + .module('horizon.dashboard.identity.users') + .factory('horizon.dashboard.identity.users.actions.password.service', passwordService); + + passwordService.$inject = [ + '$q', + 'horizon.dashboard.identity.users.resourceType', + 'horizon.dashboard.identity.users.actions.basePath', + 'horizon.dashboard.identity.users.actions.workflow.service', + 'horizon.app.core.openstack-service-api.keystone', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.openstack-service-api.settings', + 'horizon.framework.util.actions.action-result.service', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service' + ]; + + /** + * @ngDoc factory + * @name horizon.dashboard.identity.users.actions.password.service + * @Description A service to change the user password. + */ + function passwordService( + $q, + resourceType, + basePath, + workflow, + keystone, + policy, + settings, + actionResultService, + modal, + toast + ) { + var message = { + success: gettext('User password has been updated successfully.') + }; + + return { + allowed: allowed, + perform: perform, + submit: submit + }; + + ////////////// + + function allowed() { + return policy.ifAllowed({ rules: [['identity', 'identity:update_user']] }); + } + + // eslint-disable-next-line no-unused-vars + function perform(selected, scope, errorCode) { + return settings.getSetting('ENFORCE_PASSWORD_CHECK', false).then(function (response) { + var adminPassword = response; + return keystone.getUser(selected.id).then(function (response) { + var config = workflow.init("password", adminPassword, errorCode); + config.title = gettext("Change Password"); + config.model = {}; + config.model.id = response.data.id; + config.model.domain_name = response.data.domain_name; + config.model.domain_id = response.data.domain_id; + config.model.name = response.data.name; + return modal.open(config).then(submit); + }); + }); + } + + function submit(context) { + delete context.model.domain_name; + delete context.model.domain_id; + delete context.model.enabled; + return keystone.editUser(context.model).then(success, error); + + function success() { + toast.add('success', message.success); + return actionResultService.getActionResult() + .updated(resourceType, context.model.id) + .result; + } + + function error(response) { + if (response.status === 400) { + perform(context.model, null, response.data); + } else { + return actionResultService.getActionResult() + .updated(resourceType, context.model.id) + .result; + } + } + } + } +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.spec.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.spec.js new file mode 100644 index 0000000000..ee6e7688c2 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/password.action.service.spec.js @@ -0,0 +1,130 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use self 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. + */ + +(function() { + 'use strict'; + + describe('horizon.dashboard.identity.users.actions.password.service', function() { + + var $q, $scope, keystone, service, modal, policy, toast, settings; + + /////////////////////// + + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.dashboard.identity.users')); + + beforeEach(inject(function($injector, _$rootScope_, _$q_) { + $scope = _$rootScope_.$new(); + $q = _$q_; + service = $injector.get('horizon.dashboard.identity.users.actions.password.service'); + toast = $injector.get('horizon.framework.widgets.toast.service'); + modal = $injector.get('horizon.framework.widgets.form.ModalFormService'); + keystone = $injector.get('horizon.app.core.openstack-service-api.keystone'); + policy = $injector.get('horizon.app.core.openstack-service-api.policy'); + settings = $injector.get('horizon.app.core.openstack-service-api.settings'); + })); + + it('should check the policy if the user is allowed to change user password', function() { + var deferred = $q.defer(); + spyOn(policy, 'ifAllowed').and.returnValue(deferred.promise); + deferred.resolve({allowed: true}); + var handler = jasmine.createSpyObj('handler', ['success']); + + service.allowed().then(handler.success); + $scope.$apply(); + + expect(handler.success).toHaveBeenCalled(); + var allowed = handler.success.calls.first().args[0]; + + expect(allowed).toBeTruthy(); + expect(policy.ifAllowed).toHaveBeenCalledWith( + { rules: [['identity', 'identity:update_user']] }); + }); + + it('should open the modal', function() { + spyOn(modal, 'open').and.returnValue($q.defer().promise); + spyOn(keystone, 'getVersion').and.returnValue($q.defer().promise); + spyOn(keystone, 'getDefaultDomain').and.returnValue($q.defer().promise); + spyOn(keystone, 'getProjects').and.returnValue($q.defer().promise); + spyOn(keystone, 'getRoles').and.returnValue($q.defer().promise); + var deferred = $q.defer(); + spyOn(keystone, 'getUser').and.returnValue(deferred.promise); + deferred.resolve({data: {name: 'saved', id: '12345'}}); + var deferredSettings = $q.defer(); + spyOn(settings, 'getSetting').and.returnValue(deferredSettings.promise); + deferredSettings.resolve(true); + + service.perform({name: 'saved', id: '12345'}); + $scope.$apply(); + + expect(modal.open).toHaveBeenCalled(); + }); + + it('should submit change user password request to keystone', function() { + + var deferred = $q.defer(); + spyOn(keystone, 'editUser').and.returnValue(deferred.promise); + deferred.resolve({data: {id: '12345', password: 'changed'}}); + + spyOn(toast, 'add').and.callFake(angular.noop); + + service.submit({model: {id: '12345', password: 'changed'}}); + + $scope.$apply(); + + expect(keystone.editUser).toHaveBeenCalledWith({id: '12345', password: 'changed'}); + expect(toast.add) + .toHaveBeenCalledWith('success', 'User password has been updated successfully.'); + }); + + it('should call error process if failed', function() { + + var deferred = $q.defer(); + spyOn(keystone, 'editUser').and.returnValue(deferred.promise); + deferred.reject({status: 500}); + + service.submit({model: {id: '12345', password: 'changed'}}); + + $scope.$apply(); + + expect(keystone.editUser).toHaveBeenCalledWith({id: '12345', password: 'changed'}); + }); + + it('should reopen modal if failed due to admin password incorrect', function() { + + spyOn(modal, 'open').and.returnValue($q.defer().promise); + spyOn(keystone, 'getVersion').and.returnValue($q.defer().promise); + spyOn(keystone, 'getDefaultDomain').and.returnValue($q.defer().promise); + spyOn(keystone, 'getProjects').and.returnValue($q.defer().promise); + spyOn(keystone, 'getRoles').and.returnValue($q.defer().promise); + var deferredUser = $q.defer(); + spyOn(keystone, 'getUser').and.returnValue(deferredUser.promise); + deferredUser.resolve({data: {name: 'saved', id: '12345'}}); + var deferredSettings = $q.defer(); + spyOn(settings, 'getSetting').and.returnValue(deferredSettings.promise); + deferredSettings.resolve(true); + var deferred = $q.defer(); + spyOn(keystone, 'editUser').and.returnValue(deferred.promise); + deferred.reject({status: 400, data: "ADMIN_PASSWORD_INCORRECT"}); + + service.submit({model: {id: '12345', password: 'changed', admin_password: 'incorrect'}}); + + $scope.$apply(); + + expect(keystone.editUser).toHaveBeenCalledWith({id: '12345', password: 'changed', + admin_password: 'incorrect'}); + }); + }); +})(); diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.admin-password-incorrect.html b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.admin-password-incorrect.html new file mode 100644 index 0000000000..0a4cdaa1cd --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.admin-password-incorrect.html @@ -0,0 +1 @@ +
The admin password is incorrect.
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.default.html b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.default.html new file mode 100644 index 0000000000..1aab941cd6 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/error.default.html @@ -0,0 +1 @@ +
Something wrong to change password.
\ No newline at end of file diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/info.password.help.html b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/info.password.help.html new file mode 100644 index 0000000000..ab6c52e8d1 --- /dev/null +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/info.password.help.html @@ -0,0 +1 @@ +

Change user's password. We highly recommend you create a strong one.

\ No newline at end of file diff --git a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/workflow.service.js b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/workflow.service.js index 39d818f2c9..cd98d6dd4d 100644 --- a/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/workflow.service.js +++ b/openstack_dashboard/dashboards/identity/static/dashboard/identity/users/actions/workflow/workflow.service.js @@ -40,7 +40,9 @@ init: init }; - function init(action) { + function init(action, adminPassword, errorCode) { + var errorTemplate = typeof errorCode === "string" + ? errorCode.toLowerCase().replace(/_/g, "-") : "default"; var schema = { type: 'object', properties: { @@ -66,6 +68,10 @@ title: gettext('Password'), type: 'string' }, + admin_password: { + title: gettext('Admin Password'), + type: 'string' + }, project: { title: gettext('Primary Project'), type: 'string' @@ -86,6 +92,9 @@ }, required: ['name', 'password', 'project', 'role', 'enabled'] }; + if (adminPassword) { + schema.required.push('admin_password'); + } var form = [ { @@ -96,14 +105,25 @@ type: 'section', htmlClass: 'col-sm-12', items: [ + { + type: 'template', + templateUrl: basePath + "actions/workflow/error." + errorTemplate + ".html", + condition: errorTemplate === "default" + }, { type: 'template', templateUrl: basePath + "actions/workflow/info." + action + ".help.html" }, { key: 'domain_name' }, { key: 'domain_id' }, - { key: 'name' }, - { key: 'email' }, + { + key: 'name', + readonly: action === 'password' + }, + { + key: 'email', + condition: action === 'password' + }, { key: 'password', type: 'password', @@ -116,21 +136,30 @@ match: 'model.password', condition: action === 'update' }, + { + key: 'admin_password', + type: 'password', + condition: !(action === 'password' && adminPassword) + }, { key: 'project', type: 'select', - titleMap: [] + titleMap: [], + condition: action === 'password' }, { key: 'role', type: 'select', titleMap: [], - condition: action === 'update' + condition: action === 'update' || action === 'password' + }, + { + key: 'description', + condition: action === 'password' }, - { key: 'description' }, { key: 'enabled', - condition: action === 'update' + condition: action === 'update' || action === 'password' } ] } @@ -162,13 +191,13 @@ return response.data; }); keystone.getProjects().then(function (response) { - var projectField = config.form[0].items[0].items[7]; + var projectField = config.form[0].items[0].items[8]; projectField.titleMap = response.data.items.map(function each(item) { return {value: item.id, name: item.name}; }); }); keystone.getRoles().then(function (response) { - var roleField = config.form[0].items[0].items[8]; + var roleField = config.form[0].items[0].items[9]; roleField.titleMap = response.data.items.map(function each(item) { return {value: item.id, name: item.name}; }); diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example index f265c5ba72..4fbe81ef0b 100644 --- a/openstack_dashboard/local/local_settings.py.example +++ b/openstack_dashboard/local/local_settings.py.example @@ -825,7 +825,8 @@ REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES', 'LAUNCH_INSTANCE_DEFAULTS', 'OPENSTACK_IMAGE_FORMATS', 'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN', - 'CREATE_IMAGE_DEFAULTS'] + 'CREATE_IMAGE_DEFAULTS', + 'ENFORCE_PASSWORD_CHECK'] # Additional settings can be made available to the client side for # extensibility by specifying them in REST_API_ADDITIONAL_SETTINGS