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:
Travis Tripp 2016-07-08 17:04:31 -06:00
parent 5401d9245a
commit 65f0f74390
6 changed files with 467 additions and 7 deletions

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

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

View File

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

View File

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

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

View File

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