Merge branch 'open973' into open-master
Merge in policy service, WTD-973 Conflicts: bundles.json
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"platform/forms",
|
"platform/forms",
|
||||||
"platform/persistence/queue",
|
"platform/persistence/queue",
|
||||||
"platform/persistence/elastic",
|
"platform/persistence/elastic",
|
||||||
|
"platform/policy",
|
||||||
|
|
||||||
"example/generator"
|
"example/generator"
|
||||||
]
|
]
|
||||||
|
|||||||
12
example/policy/bundle.json
Normal file
12
example/policy/bundle.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "Example Policy",
|
||||||
|
"description": "Provides an example of using policies to prohibit actions.",
|
||||||
|
"extensions": {
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"implementation": "ExamplePolicy.js",
|
||||||
|
"category": "action"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
26
example/policy/src/ExamplePolicy.js
Normal file
26
example/policy/src/ExamplePolicy.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function ExamplePolicy() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Disallow the Remove action on objects whose name contains
|
||||||
|
* "foo."
|
||||||
|
*/
|
||||||
|
allow: function (action, context) {
|
||||||
|
var domainObject = (context || {}).domainObject,
|
||||||
|
model = (domainObject && domainObject.getModel()) || {},
|
||||||
|
name = model.name || "",
|
||||||
|
metadata = action.getMetadata() || {};
|
||||||
|
return metadata.key !== 'remove' || name.indexOf('foo') < 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExamplePolicy;
|
||||||
|
}
|
||||||
|
);
|
||||||
93
platform/policy/README.md
Normal file
93
platform/policy/README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
This bundle provides support for policy in Open MCT Web. Policy can be
|
||||||
|
used to limit the applicability of certain actions, or more broadly,
|
||||||
|
to provide an extension point for arbitrary decisions.
|
||||||
|
|
||||||
|
# Services
|
||||||
|
|
||||||
|
This bundle introduces the `policyService`, which may be consulted for
|
||||||
|
various decisions which are intended to be open for extension.
|
||||||
|
|
||||||
|
The `policyService` has a single method, `allow`, which takes three
|
||||||
|
arguments and returns a boolean value (true if policy says this decision
|
||||||
|
should be allowed, false if not):
|
||||||
|
|
||||||
|
* `category`: A string identifying which kind of decision is being made.
|
||||||
|
Typically, this will be a non-plural form of an extension type that is
|
||||||
|
being filtered down; for instance, to check whether or not a given
|
||||||
|
action should be returned by an `actionService`, one would use the
|
||||||
|
`action` category of extension.
|
||||||
|
* `candidate`: An object representing the thing which shall or shall not
|
||||||
|
be allowed. Usually, this will be an instance of an extension of the
|
||||||
|
category defined above.
|
||||||
|
* This does need to be the case; additional
|
||||||
|
policies which are not specific to any extension may also be defined
|
||||||
|
and consulted using unique `category` identifiers. In this case, the
|
||||||
|
type of the object delivered for the candidate may be unique to the
|
||||||
|
policy type.
|
||||||
|
* `context`: An object representing the context in which the decision is
|
||||||
|
occurring. Its contents are specific to each policy category.
|
||||||
|
* `callback`: Optional; a function to call if the policy decision is
|
||||||
|
rejected. This function will be called with the `message` string
|
||||||
|
(which may be undefined) of whichever individual policy caused the
|
||||||
|
operation to fail.
|
||||||
|
|
||||||
|
_Design rationale_: Returning a boolean here limits the amount of
|
||||||
|
information that can be conveyed by a policy decision, but has the
|
||||||
|
benefit of simplicity. In MCT on the desktop, the policy service
|
||||||
|
returned a more complex object with both a boolean status and a string
|
||||||
|
message; the string message was used rarely (by only around 15% of
|
||||||
|
policy user code) and as such is made optional in the call itself here.
|
||||||
|
|
||||||
|
_Design rationale_: Returning a boolean instead of a promise here implies
|
||||||
|
that policy decisions must occur synchronously. This limits the logic
|
||||||
|
which can be involved in a policy decision, but broadens its applicability;
|
||||||
|
policy is meant to be used by a variety of other services to separate out
|
||||||
|
a certain category of business logic, and a synchronous response means
|
||||||
|
that this capability may be utilized by both synchronous and asynchronous
|
||||||
|
services. Additionally, policies will often be used in loops (e.g. to filter
|
||||||
|
down a set of applicable actions) where latency will have the result of
|
||||||
|
harming the user experience (e.g. the user right-clicks and gets stuck
|
||||||
|
waiting for a bunch of policy decisions to complete before a menu showing
|
||||||
|
available actions can appear.)
|
||||||
|
|
||||||
|
The `policyService` is a composite service; it may be modified by adding
|
||||||
|
decorators, aggregators, etc.
|
||||||
|
|
||||||
|
## Service Components
|
||||||
|
|
||||||
|
The policy service is most often used by decorators for other composite
|
||||||
|
services. For instance, this bundle contains a decorator for `actionService`
|
||||||
|
which filters down the applicable actions exposed by that service based
|
||||||
|
on policy.
|
||||||
|
|
||||||
|
# Policy Categories
|
||||||
|
|
||||||
|
This bundle introduces `action` as a policy category. Policies of this
|
||||||
|
category shall take action instances as their candidate argument, and
|
||||||
|
action contexts as their context argument.
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
|
||||||
|
This bundle introduces the `policies` category of extension. An extension
|
||||||
|
of this category should have both an implementation, as well as the following
|
||||||
|
metadata:
|
||||||
|
|
||||||
|
* `category`: A string identifying which kind of policy decision this
|
||||||
|
effects.
|
||||||
|
* `message`: Optional; a human-readable string describing the policy
|
||||||
|
decision when it fails.
|
||||||
|
|
||||||
|
An extension of this category must also have an implementation which
|
||||||
|
takes no arguments to its constructor and provides a single method,
|
||||||
|
`allow`, which takes two arguments, `candidate` and `context` (see
|
||||||
|
descriptions above under documentation for `actionService`) and returns
|
||||||
|
a boolean indicating whether or not it allows the policy decision.
|
||||||
|
|
||||||
|
Policy decisions require consensus among all policies; that is, if a
|
||||||
|
single policy returns false, then the policy decision as a whole returns
|
||||||
|
false. As a consequence, policies should be written in a permissive
|
||||||
|
manner; that is, they should be designed to prohibit behavior under a
|
||||||
|
specific set of conditions (by returning false), and allow any behavior
|
||||||
|
which does not match those conditions (by returning true.)
|
||||||
21
platform/policy/bundle.json
Normal file
21
platform/policy/bundle.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Policy Service",
|
||||||
|
"description": "Provides support for extension-driven decisions.",
|
||||||
|
"sources": "src",
|
||||||
|
"extensions": {
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "decorator",
|
||||||
|
"provides": "actionService",
|
||||||
|
"implementation": "PolicyActionDecorator.js",
|
||||||
|
"depends": [ "policyService" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "provider",
|
||||||
|
"provides": "policyService",
|
||||||
|
"implementation": "PolicyProvider.js",
|
||||||
|
"depends": [ "policies[]" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
37
platform/policy/src/PolicyActionDecorator.js
Normal file
37
platform/policy/src/PolicyActionDecorator.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out actions based on policy.
|
||||||
|
* @param {PolicyService} policyService the service which provides
|
||||||
|
* policy decisions
|
||||||
|
* @param {ActionService} actionService the service to decorate
|
||||||
|
*/
|
||||||
|
function PolicyActionDecorator(policyService, actionService) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Get actions which are applicable in this context.
|
||||||
|
* These will be filters to remove any actions which
|
||||||
|
* are deemed inapplicable by policy.
|
||||||
|
* @param context the context in which the action will occur
|
||||||
|
* @returns {Action[]} applicable actions
|
||||||
|
*/
|
||||||
|
getActions: function (context) {
|
||||||
|
// Check if an action is allowed by policy.
|
||||||
|
function allow(action) {
|
||||||
|
return policyService.allow('action', action, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up actions, filter out the disallowed ones.
|
||||||
|
return actionService.getActions(context).filter(allow);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return PolicyActionDecorator;
|
||||||
|
}
|
||||||
|
);
|
||||||
85
platform/policy/src/PolicyProvider.js
Normal file
85
platform/policy/src/PolicyProvider.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*global define*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
[],
|
||||||
|
function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an implementation of `policyService` which consults
|
||||||
|
* various policy extensions to determine whether or not a specific
|
||||||
|
* decision should be allowed.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
function PolicyProvider(policies) {
|
||||||
|
var policyMap = {};
|
||||||
|
|
||||||
|
// Instantiate a policy. Mostly just a constructor call, but
|
||||||
|
// we also track the message (which was provided as metadata
|
||||||
|
// along with the constructor) so that we can expose this later.
|
||||||
|
function instantiate(Policy) {
|
||||||
|
var policy = Object.create(new Policy());
|
||||||
|
policy.message = Policy.message;
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a specific policy to the map for later lookup,
|
||||||
|
// according to its category. Note that policy extensions are
|
||||||
|
// provided as constructors, so they are instantiated here.
|
||||||
|
function addToMap(Policy) {
|
||||||
|
var category = (Policy || {}).category;
|
||||||
|
if (category) {
|
||||||
|
// Create a new list for that category if needed...
|
||||||
|
policyMap[category] = policyMap[category] || [];
|
||||||
|
// ...and put an instance of this policy in that list.
|
||||||
|
policyMap[category].push(instantiate(Policy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the map for subsequent lookup
|
||||||
|
policies.forEach(addToMap);
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Check whether or not a certain decision is allowed by
|
||||||
|
* policy.
|
||||||
|
* @param {string} category a machine-readable identifier
|
||||||
|
* for the kind of decision being made
|
||||||
|
* @param candidate the object about which the decision is
|
||||||
|
* being made
|
||||||
|
* @param context the context in which the decision occurs
|
||||||
|
* @param {Function} [callback] callback to invoke with a
|
||||||
|
* string message describing the reason a decision
|
||||||
|
* was disallowed (if its disallowed)
|
||||||
|
* @returns {boolean} true if the decision is allowed,
|
||||||
|
* otherwise false.
|
||||||
|
*/
|
||||||
|
allow: function (category, candidate, context, callback) {
|
||||||
|
var policyList = policyMap[category] || [],
|
||||||
|
i;
|
||||||
|
|
||||||
|
// Iterate through policies. We do this instead of map or
|
||||||
|
// forEach so that we can return immediately if a policy
|
||||||
|
// chooses to disallow this decision.
|
||||||
|
for (i = 0; i < policyList.length; i += 1) {
|
||||||
|
// Consult the policy...
|
||||||
|
if (!policyList[i].allow(candidate, context)) {
|
||||||
|
// ...it disallowed, so pass its message to
|
||||||
|
// the callback (if any)
|
||||||
|
if (callback) {
|
||||||
|
callback(policyList[i].message);
|
||||||
|
}
|
||||||
|
// And return the failed result.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No policy disallowed this decision.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return PolicyProvider;
|
||||||
|
}
|
||||||
|
);
|
||||||
79
platform/policy/test/PolicyActionDecoratorSpec.js
Normal file
79
platform/policy/test/PolicyActionDecoratorSpec.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../src/PolicyActionDecorator"],
|
||||||
|
function (PolicyActionDecorator) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The policy action decorator", function () {
|
||||||
|
var mockPolicyService,
|
||||||
|
mockActionService,
|
||||||
|
testContext,
|
||||||
|
testActions,
|
||||||
|
decorator;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
mockPolicyService = jasmine.createSpyObj(
|
||||||
|
'policyService',
|
||||||
|
['allow']
|
||||||
|
);
|
||||||
|
mockActionService = jasmine.createSpyObj(
|
||||||
|
'actionService',
|
||||||
|
['getActions']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Content of actions should be irrelevant to this
|
||||||
|
// decorator, so just give it some objects to pass
|
||||||
|
// around.
|
||||||
|
testActions = [
|
||||||
|
{ someKey: "a" },
|
||||||
|
{ someKey: "b" },
|
||||||
|
{ someKey: "c" }
|
||||||
|
];
|
||||||
|
testContext = { someKey: "some value" };
|
||||||
|
|
||||||
|
mockActionService.getActions.andReturn(testActions);
|
||||||
|
mockPolicyService.allow.andReturn(true);
|
||||||
|
|
||||||
|
decorator = new PolicyActionDecorator(
|
||||||
|
mockPolicyService,
|
||||||
|
mockActionService
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates to its decorated action service", function () {
|
||||||
|
decorator.getActions(testContext);
|
||||||
|
expect(mockActionService.getActions)
|
||||||
|
.toHaveBeenCalledWith(testContext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides actions from its decorated action service", function () {
|
||||||
|
// Mock policy service allows everything by default,
|
||||||
|
// so everything should be returned
|
||||||
|
expect(decorator.getActions(testContext))
|
||||||
|
.toEqual(testActions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("consults the policy service for each candidate action", function () {
|
||||||
|
decorator.getActions(testContext);
|
||||||
|
testActions.forEach(function (testAction) {
|
||||||
|
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||||
|
'action',
|
||||||
|
testAction,
|
||||||
|
testContext
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters out policy-disallowed actions", function () {
|
||||||
|
// Disallow the second action
|
||||||
|
mockPolicyService.allow.andCallFake(function (cat, candidate, ctxt) {
|
||||||
|
return candidate.someKey !== 'b';
|
||||||
|
});
|
||||||
|
expect(decorator.getActions(testContext))
|
||||||
|
.toEqual([ testActions[0], testActions[2] ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
85
platform/policy/test/PolicyProviderSpec.js
Normal file
85
platform/policy/test/PolicyProviderSpec.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
|
||||||
|
|
||||||
|
define(
|
||||||
|
["../src/PolicyProvider"],
|
||||||
|
function (PolicyProvider) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("The policy provider", function () {
|
||||||
|
var testPolicies,
|
||||||
|
mockPolicies,
|
||||||
|
mockPolicyConstructors,
|
||||||
|
testCandidate,
|
||||||
|
testContext,
|
||||||
|
provider;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
testPolicies = [
|
||||||
|
{ category: "a", message: "some message", result: true },
|
||||||
|
{ category: "a", result: true },
|
||||||
|
{ category: "a", result: true },
|
||||||
|
{ category: "b", message: "some message", result: true },
|
||||||
|
{ category: "b", result: true },
|
||||||
|
{ category: "b", result: true }
|
||||||
|
];
|
||||||
|
mockPolicies = testPolicies.map(function (p) {
|
||||||
|
var mockPolicy = jasmine.createSpyObj("policy", ['allow']);
|
||||||
|
mockPolicy.allow.andCallFake(function () { return p.result; });
|
||||||
|
return mockPolicy;
|
||||||
|
});
|
||||||
|
mockPolicyConstructors = testPolicies.map(function (p, i) {
|
||||||
|
var mockPolicyConstructor = jasmine.createSpy();
|
||||||
|
mockPolicyConstructor.andReturn(mockPolicies[i]);
|
||||||
|
mockPolicyConstructor.message = p.message;
|
||||||
|
mockPolicyConstructor.category = p.category;
|
||||||
|
return mockPolicyConstructor;
|
||||||
|
});
|
||||||
|
|
||||||
|
testCandidate = { someKey: "some value" };
|
||||||
|
testContext = { someOtherKey: "some other value" };
|
||||||
|
|
||||||
|
provider = new PolicyProvider(mockPolicyConstructors);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has an allow method", function () {
|
||||||
|
expect(provider.allow).toEqual(jasmine.any(Function));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("consults all relevant policies", function () {
|
||||||
|
provider.allow("a", testCandidate, testContext);
|
||||||
|
expect(mockPolicies[0].allow)
|
||||||
|
.toHaveBeenCalledWith(testCandidate, testContext);
|
||||||
|
expect(mockPolicies[1].allow)
|
||||||
|
.toHaveBeenCalledWith(testCandidate, testContext);
|
||||||
|
expect(mockPolicies[2].allow)
|
||||||
|
.toHaveBeenCalledWith(testCandidate, testContext);
|
||||||
|
expect(mockPolicies[3].allow)
|
||||||
|
.not.toHaveBeenCalled();
|
||||||
|
expect(mockPolicies[4].allow)
|
||||||
|
.not.toHaveBeenCalled();
|
||||||
|
expect(mockPolicies[5].allow)
|
||||||
|
.not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows what all policies allow", function () {
|
||||||
|
expect(provider.allow("a", testCandidate, testContext))
|
||||||
|
.toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disallows what any one policy disallows", function () {
|
||||||
|
testPolicies[1].result = false;
|
||||||
|
expect(provider.allow("a", testCandidate, testContext))
|
||||||
|
.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides a message for policy failure, when available", function () {
|
||||||
|
var mockCallback = jasmine.createSpy();
|
||||||
|
testPolicies[0].result = false;
|
||||||
|
expect(provider.allow("a", testCandidate, testContext, mockCallback))
|
||||||
|
.toBeFalsy();
|
||||||
|
expect(mockCallback).toHaveBeenCalledWith(testPolicies[0].message);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
4
platform/policy/test/suite.json
Normal file
4
platform/policy/test/suite.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"PolicyActionDecorator",
|
||||||
|
"PolicyProvider"
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user