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.
|
# 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(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
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};
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user