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:
parent
86e4e92129
commit
c174036c84
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
@ -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'});
|
||||
});
|
||||
});
|
||||
})();
|
@ -0,0 +1 @@
|
||||
<div class="alert alert-dismissable alert-danger" translate>The admin password is incorrect.</div>
|
@ -0,0 +1 @@
|
||||
<div class="alert alert-dismissable alert-danger" translate>Something wrong to change password.</div>
|
@ -0,0 +1 @@
|
||||
<p translate>Change user's password. We highly recommend you create a strong one.</p>
|
@ -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};
|
||||
});
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user