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.
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(

View File

@ -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)

View File

@ -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,

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

View File

@ -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