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
This commit is contained in:
Shu Muto 2017-11-29 17:44:37 +09:00
parent 86e4e92129
commit c174036c84
10 changed files with 298 additions and 11 deletions

View File

@ -537,7 +537,7 @@ def user_verify_admin_password(request, admin_password):
# verify if it's correct. # verify if it's correct.
client = keystone_client_v2 if VERSIONS.active < 3 else keystone_client_v3 client = keystone_client_v2 if VERSIONS.active < 3 else keystone_client_v3
try: try:
endpoint = _get_endpoint_url(request, 'internalURL') endpoint = _get_endpoint_url(request, 'publicURL')
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
client.Client( client.Client(

View File

@ -153,6 +153,11 @@ class User(generic.View):
user = api.keystone.user_get(request, id) user = api.keystone.user_get(request, id)
if 'password' in keys: 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'] password = request.DATA['password']
api.keystone.user_update_password(request, user, password) api.keystone.user_update_password(request, user, password)

View File

@ -35,6 +35,7 @@
'horizon.framework.conf.resource-type-registry.service', 'horizon.framework.conf.resource-type-registry.service',
'horizon.dashboard.identity.users.actions.create.service', 'horizon.dashboard.identity.users.actions.create.service',
'horizon.dashboard.identity.users.actions.update.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.actions.delete.service',
'horizon.dashboard.identity.users.resourceType' 'horizon.dashboard.identity.users.resourceType'
]; ];
@ -43,6 +44,7 @@
registry, registry,
createService, createService,
updateService, updateService,
passwordService,
deleteService, deleteService,
userResourceTypeCode userResourceTypeCode
) { ) {
@ -57,6 +59,14 @@
type: 'row' type: 'row'
} }
}) })
.append({
id: 'passwordAction',
service: passwordService,
template: {
text: gettext('Change Password'),
type: 'row'
}
})
.append({ .append({
id: 'deleteAction', id: 'deleteAction',
service: deleteService, service: deleteService,

View File

@ -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;
}
}
}
}
})();

View File

@ -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'});
});
});
})();

View File

@ -0,0 +1 @@
<div class="alert alert-dismissable alert-danger" translate>The admin password is incorrect.</div>

View File

@ -0,0 +1 @@
<div class="alert alert-dismissable alert-danger" translate>Something wrong to change password.</div>

View File

@ -0,0 +1 @@
<p translate>Change user's password. We highly recommend you create a strong one.</p>

View File

@ -40,7 +40,9 @@
init: init init: init
}; };
function init(action) { function init(action, adminPassword, errorCode) {
var errorTemplate = typeof errorCode === "string"
? errorCode.toLowerCase().replace(/_/g, "-") : "default";
var schema = { var schema = {
type: 'object', type: 'object',
properties: { properties: {
@ -66,6 +68,10 @@
title: gettext('Password'), title: gettext('Password'),
type: 'string' type: 'string'
}, },
admin_password: {
title: gettext('Admin Password'),
type: 'string'
},
project: { project: {
title: gettext('Primary Project'), title: gettext('Primary Project'),
type: 'string' type: 'string'
@ -86,6 +92,9 @@
}, },
required: ['name', 'password', 'project', 'role', 'enabled'] required: ['name', 'password', 'project', 'role', 'enabled']
}; };
if (adminPassword) {
schema.required.push('admin_password');
}
var form = [ var form = [
{ {
@ -96,14 +105,25 @@
type: 'section', type: 'section',
htmlClass: 'col-sm-12', htmlClass: 'col-sm-12',
items: [ items: [
{
type: 'template',
templateUrl: basePath + "actions/workflow/error." + errorTemplate + ".html",
condition: errorTemplate === "default"
},
{ {
type: 'template', type: 'template',
templateUrl: basePath + "actions/workflow/info." + action + ".help.html" templateUrl: basePath + "actions/workflow/info." + action + ".help.html"
}, },
{ key: 'domain_name' }, { key: 'domain_name' },
{ key: 'domain_id' }, { key: 'domain_id' },
{ key: 'name' }, {
{ key: 'email' }, key: 'name',
readonly: action === 'password'
},
{
key: 'email',
condition: action === 'password'
},
{ {
key: 'password', key: 'password',
type: 'password', type: 'password',
@ -116,21 +136,30 @@
match: 'model.password', match: 'model.password',
condition: action === 'update' condition: action === 'update'
}, },
{
key: 'admin_password',
type: 'password',
condition: !(action === 'password' && adminPassword)
},
{ {
key: 'project', key: 'project',
type: 'select', type: 'select',
titleMap: [] titleMap: [],
condition: action === 'password'
}, },
{ {
key: 'role', key: 'role',
type: 'select', type: 'select',
titleMap: [], titleMap: [],
condition: action === 'update' condition: action === 'update' || action === 'password'
},
{
key: 'description',
condition: action === 'password'
}, },
{ key: 'description' },
{ {
key: 'enabled', key: 'enabled',
condition: action === 'update' condition: action === 'update' || action === 'password'
} }
] ]
} }
@ -162,13 +191,13 @@
return response.data; return response.data;
}); });
keystone.getProjects().then(function (response) { 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) { projectField.titleMap = response.data.items.map(function each(item) {
return {value: item.id, name: item.name}; return {value: item.id, name: item.name};
}); });
}); });
keystone.getRoles().then(function (response) { 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) { roleField.titleMap = response.data.items.map(function each(item) {
return {value: item.id, name: item.name}; return {value: item.id, name: item.name};
}); });

View File

@ -825,7 +825,8 @@ REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES',
'LAUNCH_INSTANCE_DEFAULTS', 'LAUNCH_INSTANCE_DEFAULTS',
'OPENSTACK_IMAGE_FORMATS', 'OPENSTACK_IMAGE_FORMATS',
'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN', 'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN',
'CREATE_IMAGE_DEFAULTS'] 'CREATE_IMAGE_DEFAULTS',
'ENFORCE_PASSWORD_CHECK']
# Additional settings can be made available to the client side for # Additional settings can be made available to the client side for
# extensibility by specifying them in REST_API_ADDITIONAL_SETTINGS # extensibility by specifying them in REST_API_ADDITIONAL_SETTINGS