hz-dynamic-table: Column level permissions
hz-dynamic-table doesn't have any way to dynamically determine whether or not to display a column. For example, in openstack dashboard, we may want to display a column only in certain policies pass check, if certain settings are enabled, or services are enabled. In addition, a generic allowed function can just be set on the column config. This patch adds this ability as a reusable service. The reusable service will make it possible for horizon to easily check common sets of permissions on a variety of objects including the hz-dynamic-table columns (in this patch). Child patches demonstrate its use on the images table. Change-Id: I9f92b69d86d830387a83c28ec5829fb3c43fc4a6 Partially-Implements: blueprint angularize-images-table Co-Authored-By: Matt Borland <matt.borland@hpe.com>
This commit is contained in:
parent
5401d9245a
commit
65f0f74390
158
horizon/static/framework/conf/permissions.service.js
Normal file
158
horizon/static/framework/conf/permissions.service.js
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Hewlett Packard Enterprise Development LP
|
||||
*
|
||||
* 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.framework.conf')
|
||||
.service('horizon.framework.conf.permissions.service', Permissions);
|
||||
|
||||
Permissions.$inject = ['$q'];
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name horizon.framework.conf.permissions
|
||||
* @module horizon.framework.conf
|
||||
*
|
||||
* This service provides a common set of a promise based permission checking that
|
||||
* can be applied to various entities in horizon. In the most basic case,
|
||||
* this code executes the allowed() function (if it exists) on
|
||||
* items passed in and returns the promise which will either resolve
|
||||
* if the item should be allowed or reject if it should not be allowed.
|
||||
* An example of an item passed in might be a hz-dynamic-table column spec.
|
||||
* See that code for an example of permissionService in use.
|
||||
*
|
||||
* Horizon is technically a set of application agnostic widgets and utilities
|
||||
* that can be used for various applications. The OpenStack Dashboard is
|
||||
* an implementation that uses these widgets. Many of these widgets, such
|
||||
* as hz-dynamic-table may need to be displayed based on a variety of permissions
|
||||
* that are application specific and that are more simply expressed in a declarative
|
||||
* matter rather than functional code such as the allowed function. This service
|
||||
* provides this capability by allowing applications to decorate this service and
|
||||
* override the extendedPermissions function.
|
||||
*
|
||||
* In the openstack dashboard additional checks beyond the simple allowed() function
|
||||
* are added to this service in the app.core.conf permissionsDecorator,
|
||||
* including policies, services and settings.
|
||||
*/
|
||||
function Permissions($q) {
|
||||
var service = {
|
||||
checkAllowed: checkAllowed,
|
||||
checkAll: checkAll,
|
||||
extendedPermissions: extendedPermissions
|
||||
};
|
||||
|
||||
return service;
|
||||
|
||||
/**
|
||||
* If this input configItem has an allowed function, this will execute it and return
|
||||
* the promise. Otherwise it will return a resolved promise.
|
||||
*/
|
||||
function checkAllowed(configItem) {
|
||||
if (angular.isFunction(configItem.allowed)) {
|
||||
return configItem.allowed();
|
||||
} else {
|
||||
return $q.when(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On the given configItem, this will get configItem.allowed and get each
|
||||
* additional property matching a permission defined in extended permissions.
|
||||
* All of the promises will then be run using $q.all. If all promises resolve
|
||||
* then this will resolve. Otherwise, this will reject.
|
||||
*
|
||||
* When the input to any given permission is an array, each element will be
|
||||
* treated as a distinct permission and a single promise resolver will be invoked for
|
||||
* each input element.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* In openstack dashboard, each of the policies in the below array will result in a distinct
|
||||
* policy check and all of the policies must pass in order for this to permit (resolve) the
|
||||
* permissions check.
|
||||
*
|
||||
* configItem.policies = [{ rules: [["identity", "identity:get_project"]] }, { <rule 2>}]
|
||||
*/
|
||||
function checkAll(configItem) {
|
||||
var promises = [];
|
||||
promises = promises.concat(getConfigurationPromises(configItem));
|
||||
promises.push(checkAllowed(configItem));
|
||||
return $q.all(promises);
|
||||
}
|
||||
|
||||
/*
|
||||
* This defines all additional permissions that can be defined on the input configItem
|
||||
* of checkAll. This is a function that returns an object definition where
|
||||
* each property on the object is the name of a permission that can be set on
|
||||
* the configItem in order have permission to access / use whatever is being requested.
|
||||
* The value of each property should be a promise that can resolve a single instance of that
|
||||
* permission. When the input to any given permission is an array, each element will be
|
||||
* treated as a distinct input and a single promise resolver will be invoked for
|
||||
* each input element.
|
||||
*
|
||||
* Since this is part of the horizon framework, it defaults to returning an empty object.
|
||||
* However, individual applications can decorate this service and override this
|
||||
* function to provide common permissions for their application.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* Openstack Dashboard may set permissions for required policies, services, or settings
|
||||
* (amongst other items). To do this, the service would be decorated like the following:
|
||||
*
|
||||
function decorator($delegate, policy, serviceCatalog, settings) {
|
||||
var permissionsService = $delegate;
|
||||
|
||||
permissionsService.extendedPermissions = extendedPermissions;
|
||||
|
||||
function extendedPermissions() {
|
||||
return {
|
||||
policies: policy.ifAllowed,
|
||||
services: serviceCatalog.ifTypeEnabled,
|
||||
settings: settings.ifEnabled
|
||||
};
|
||||
}
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
}
|
||||
*/
|
||||
function extendedPermissions() {
|
||||
return {};
|
||||
}
|
||||
|
||||
function getConfigurationPromises(configItem) {
|
||||
|
||||
var promises = [];
|
||||
|
||||
angular.forEach(service.extendedPermissions, checkPermissions);
|
||||
|
||||
function checkPermissions(permissionResolver, permissionName) {
|
||||
var permissionInput = configItem[permissionName];
|
||||
if (angular.isArray(permissionInput)) {
|
||||
angular.forEach(permissionInput, function addPermissionCheck(singlePermissionInput) {
|
||||
promises.push(permissionResolver(singlePermissionInput));
|
||||
});
|
||||
} else if (angular.isDefined(permissionInput)) {
|
||||
promises.push(permissionResolver(permissionInput));
|
||||
}
|
||||
}
|
||||
|
||||
return promises;
|
||||
}
|
||||
|
||||
}
|
||||
})();
|
119
horizon/static/framework/conf/permissions.service.spec.js
Normal file
119
horizon/static/framework/conf/permissions.service.spec.js
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* 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';
|
||||
|
||||
describe('permissions service', function() {
|
||||
var service;
|
||||
|
||||
beforeEach(module('horizon.framework.conf'));
|
||||
beforeEach(inject(function($injector) {
|
||||
service = $injector.get('horizon.framework.conf.permissions.service');
|
||||
}));
|
||||
|
||||
it("is defined", function() {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe("checkAllowed", function() {
|
||||
|
||||
it("returns rejected promise returned by configItem.allowed", inject(function($q, $timeout) {
|
||||
var deferred = $q.defer();
|
||||
deferred.reject();
|
||||
var item = {allowed: function() { return deferred.promise; }};
|
||||
service.checkAllowed(item).then(fail, pass);
|
||||
$timeout.flush();
|
||||
}));
|
||||
|
||||
it("returns resolved promise returned by configItem.allowed", inject(function($q, $timeout) {
|
||||
var deferred = $q.defer();
|
||||
deferred.resolve({});
|
||||
var item = {allowed: function() { return deferred.promise; }};
|
||||
service.checkAllowed(item).then(pass, fail);
|
||||
$timeout.flush();
|
||||
}));
|
||||
|
||||
it("returns resolved promise when no configItem.allowed", inject(function($q, $timeout) {
|
||||
var item = {};
|
||||
service.checkAllowed(item).then(pass, fail);
|
||||
$timeout.flush();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe("checkAll", function() {
|
||||
describe("with extended permissions", function() {
|
||||
beforeEach(inject(function($q) {
|
||||
var resolver = function() { return $q.defer().promise; };
|
||||
service.extendedPermissions = { perm1: resolver };
|
||||
}));
|
||||
|
||||
it("with promise array, adds checks for permissions", inject(function($q) {
|
||||
var input = {perm1: [$q.defer().promise]};
|
||||
service.checkAll(input).then(verifyResult);
|
||||
function verifyResult(result) {
|
||||
expect(angular.isArray(result)).toBe(true);
|
||||
expect(result.length).toBe(1);
|
||||
}
|
||||
}));
|
||||
|
||||
it("with promise, adds checks for permissions", inject(function($q) {
|
||||
var input = {perm1: $q.defer().promise};
|
||||
service.checkAll(input).then(verifyResult);
|
||||
function verifyResult(result) {
|
||||
expect(angular.isArray(result)).toBe(true);
|
||||
expect(result.length).toBe(1);
|
||||
}
|
||||
}));
|
||||
|
||||
it("with no promise, adds checks for permissions", inject(function($q) {
|
||||
var input = {unlisted: $q.defer().promise};
|
||||
service.checkAll(input).then(verifyResult);
|
||||
function verifyResult(result) {
|
||||
expect(angular.isArray(result)).toBe(true);
|
||||
expect(result.length).toBe(1);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
it("without extended permissions it returns no promises", inject(function($q) {
|
||||
var retval = service.checkAll({perm1: [$q.defer().promise]});
|
||||
retval.then(verifyResult);
|
||||
function verifyResult(result) {
|
||||
expect(angular.isArray(result)).toBe(true);
|
||||
expect(result.length).toBe(0);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
describe("extendedPermissions", function() {
|
||||
it("defaults to returning no permissions", function() {
|
||||
expect(service.extendedPermissions()).toEqual({});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function pass() {
|
||||
expect(true).toBe(true);
|
||||
}
|
||||
|
||||
function fail() {
|
||||
expect(true).toBe(false);
|
||||
}
|
||||
})();
|
@ -13,14 +13,17 @@
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
(function() {
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('horizon.framework.widgets.table')
|
||||
.directive('hzDynamicTable', hzDynamicTable);
|
||||
|
||||
hzDynamicTable.$inject = ['horizon.framework.widgets.basePath'];
|
||||
hzDynamicTable.$inject = [
|
||||
'horizon.framework.widgets.basePath',
|
||||
'horizon.framework.conf.permissions.service'
|
||||
];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@ -52,8 +55,20 @@
|
||||
* not provided, the default message is used.
|
||||
* columns {Array} of objects to describe each column. Each object
|
||||
* requires: 'id', 'title', 'priority' (responsive priority when table resized)
|
||||
* optional: 'sortDefault', 'filters' (to apply to the column cells),
|
||||
* 'template' (see hz-cell directive for details)
|
||||
* optional: 'sortDefault',
|
||||
* 'filters' (to apply to the column cells),
|
||||
* 'template' (see hz-cell directive for details),
|
||||
* 'allowed' (a promise that must resolve in order for the column to be viewed),
|
||||
*
|
||||
* This directive provides an extension point for applications to decorate additional declarative
|
||||
* column level permissions that must be fulfilled in order for the column to be viewed. For
|
||||
* example, openstack dashboard adds the following optional declarative permissions:
|
||||
* 'services' (OpenStack services that must be enabled in the current region),
|
||||
* 'settings' (horizon settings that must be enabled)
|
||||
* 'policies' (policy rules that must be allowed)
|
||||
*
|
||||
* This is accomplished by decorating the 'horizon.framework.conf.permissions' service.
|
||||
* See that service for more information.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
@ -66,6 +81,7 @@
|
||||
* {id: 'b', title: 'B', priority: 2},
|
||||
* {id: 'c', title: 'C', priority: 1, sortDefault: true},
|
||||
* {id: 'd', title: 'D', priority: 2, filters: [myFilter, 'yesno']}
|
||||
* {id: 'e', title: 'E', allowed: allowedPromiseFunction}
|
||||
* ]
|
||||
* };
|
||||
* ```
|
||||
@ -86,7 +102,7 @@
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
function hzDynamicTable(basePath) {
|
||||
function hzDynamicTable(basePath, permissionsService) {
|
||||
|
||||
// <r1chardj0n3s>: there are some configuration items which are on the directive,
|
||||
// and some on the "config" attribute of the directive. Those latter configuration
|
||||
@ -114,6 +130,8 @@
|
||||
return directive;
|
||||
|
||||
function preLink(scope) {
|
||||
//Isolate config changes we do here from propagating out.
|
||||
scope.config = angular.copy(scope.config);
|
||||
scope.items = [];
|
||||
}
|
||||
|
||||
@ -125,6 +143,30 @@
|
||||
if (angular.isUndefined(scope.config.expand)) {
|
||||
scope.config.expand = true;
|
||||
}
|
||||
|
||||
setColumnPermitted(scope.config.columns);
|
||||
}
|
||||
|
||||
function setColumnPermitted(columns) {
|
||||
|
||||
angular.forEach(columns, checkPermissions);
|
||||
|
||||
function checkPermissions(column) {
|
||||
if (column.permitted === true || column.permitted === false) {
|
||||
// No need to check again
|
||||
return;
|
||||
} else {
|
||||
permissionsService.checkAll(column).then(allow, disallow);
|
||||
}
|
||||
|
||||
function allow() {
|
||||
column.permitted = true;
|
||||
}
|
||||
|
||||
function disallow() {
|
||||
column.permitted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
@ -38,7 +38,8 @@
|
||||
class="rsp-p{$ column.priority $}"
|
||||
st-sort="{$ column.id $}"
|
||||
ng-attr-st-sort-default="{$ column.sortDefault $}"
|
||||
translate>
|
||||
translate
|
||||
ng-if="column.permitted">
|
||||
{$ column.title $}
|
||||
</th>
|
||||
<th ng-if="itemActions"></th>
|
||||
@ -69,7 +70,8 @@
|
||||
</span>
|
||||
</td>
|
||||
<td ng-repeat="column in config.columns"
|
||||
class="rsp-p{$ column.priority $}">
|
||||
class="rsp-p{$ column.priority $}"
|
||||
ng-if="column.permitted">
|
||||
<hz-cell table="table" column="column" item="item"></hz-cell>
|
||||
</td>
|
||||
<td ng-if="itemActions" class="actions_column">
|
||||
|
138
openstack_dashboard/static/app/core/conf/conf.module.js
Normal file
138
openstack_dashboard/static/app/core/conf/conf.module.js
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
|
||||
*
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* @ngdoc overview
|
||||
* @ngname horizon.app.core.conf
|
||||
*
|
||||
* @description
|
||||
* Provides features that support app configuration.
|
||||
*/
|
||||
angular
|
||||
.module('horizon.app.core.conf', ['horizon.framework.conf'])
|
||||
.config(permissionsDecorator);
|
||||
|
||||
permissionsDecorator.$inject = [
|
||||
'$provide'
|
||||
];
|
||||
|
||||
/**
|
||||
* @name permissionsDecorator
|
||||
* @param {Object} $provide
|
||||
* @description
|
||||
* This decorates the horizon.framework.conf.permissions service to
|
||||
* support declarative level permissions based on openstack_dashboard requirements.
|
||||
* This adds the following declarative permissions:
|
||||
* 'services' (OpenStack services that must be enabled in the current region),
|
||||
* 'settings' (OpenStack Dashboard settings that must be enabled)
|
||||
* 'policies' (policy rules that must be allowed)
|
||||
*
|
||||
* For example, these may all be used with the registry service and table column registrations.
|
||||
* They are passed through to hz-dynamic-table which asks the permissions service to check
|
||||
* the column's permissions before displaying each column. Below is a pseudo example where you
|
||||
* want to show the instance compute host only if the user is an admin. So you'd create a policy
|
||||
* rule for checking if the user is an admin and add that check here. Or you'd want to only
|
||||
* show the volume column if Cinder (the volume service) is enabled.
|
||||
*
|
||||
* registry.getResourceType('OS::Nova::Server')
|
||||
.tableColumns
|
||||
.append({
|
||||
id: 'host',
|
||||
priority: 1,
|
||||
policies: [{rules: [['compute', 'compute:is_admin']]}]
|
||||
})
|
||||
.append({
|
||||
id: 'volumes',
|
||||
priority: 2,
|
||||
services: "volume"
|
||||
})
|
||||
*
|
||||
* 'policies' may be set to a single object or an array of rules objects.
|
||||
*
|
||||
* All of the following are examples:
|
||||
*
|
||||
* policies = { rules: [["identity", "identity:get_project"]] }
|
||||
* policies = [{ rules: [["identity", "identity:get_project"]] }, { <rule 2>}]
|
||||
*
|
||||
* 'services' may by set to a single service type or an array of service types.
|
||||
* This may be done inline or may come from an object in scope.
|
||||
*
|
||||
* All of the following are valid examples:
|
||||
*
|
||||
* services="network"
|
||||
* services=["network"]
|
||||
* services=["network", "metering"]
|
||||
*
|
||||
* 'settings' Additional info. In local_settings.py allows you to specify settings such as:
|
||||
*
|
||||
* OPENSTACK_HYPERVISOR_FEATURES = {
|
||||
* 'can_set_mount_point': True,
|
||||
* 'can_set_password': False,
|
||||
* }
|
||||
*
|
||||
* To access a specific setting, use a simplified path where a . (dot)
|
||||
* separates elements in the path. So in the above example, the paths
|
||||
* would be:
|
||||
*
|
||||
* OPENSTACK_HYPERVISOR_FEATURES.can_set_mount_point
|
||||
* OPENSTACK_HYPERVISOR_FEATURES.can_set_password
|
||||
*
|
||||
* The `settings` attribute may be set to a single setting path
|
||||
* or an array of setting paths. All of the following are examples:
|
||||
*
|
||||
* settings="SETTING_GROUP.my_setting_1"
|
||||
* settings=["SETTING_GROUP.my_setting_1"]
|
||||
* settings=["SETTING_GROUP.my_setting_1", "SETTING_GROUP.my_setting_2"]
|
||||
*
|
||||
* The desired setting must be listed in one of the two following locations
|
||||
* in settings.py or local_settings.py in order for it to be available
|
||||
* to the client side for evaluation. If it is not, it will always evaluate
|
||||
* to false.
|
||||
*
|
||||
* REST_API_REQUIRED_SETTINGS
|
||||
* REST_API_REQUIRED_SETTINGS
|
||||
*
|
||||
* This directive currently only supports settings that are set to
|
||||
* true or false. So currently, you only need to provide the path to
|
||||
* the setting. Future enhancements should allow for alternatively providing
|
||||
* an object or array of objects with the path and expected value:
|
||||
* {path:"SOME_setting_1", expected:"1.0"}.
|
||||
*/
|
||||
function permissionsDecorator($provide) {
|
||||
$provide.decorator('horizon.framework.conf.permissions.service',
|
||||
['$delegate',
|
||||
'horizon.app.core.openstack-service-api.policy',
|
||||
'horizon.app.core.openstack-service-api.serviceCatalog',
|
||||
'horizon.app.core.openstack-service-api.settings',
|
||||
decorator]);
|
||||
|
||||
function decorator($delegate, policy, serviceCatalog, settings) {
|
||||
var permissionsService = $delegate;
|
||||
|
||||
permissionsService.extendedPermissions = {
|
||||
policies: policy.ifAllowed,
|
||||
services: serviceCatalog.ifTypeEnabled,
|
||||
settings: settings.ifEnabled
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
@ -31,6 +31,7 @@
|
||||
*/
|
||||
angular
|
||||
.module('horizon.app.core', [
|
||||
'horizon.app.core.conf',
|
||||
'horizon.app.core.cloud-services',
|
||||
'horizon.app.core.images',
|
||||
'horizon.app.core.metadata',
|
||||
|
Loading…
Reference in New Issue
Block a user