[Containment] Merge in policy

Merge in policy support, as this is necessary to support
containment rules, WTD-962.
This commit is contained in:
Victor Woeltjen 2015-04-07 20:03:00 -07:00
commit 931ca73d6b
10 changed files with 443 additions and 0 deletions

View File

@ -14,6 +14,7 @@
"platform/forms",
"platform/persistence/queue",
"platform/persistence/elastic",
"platform/policy",
"example/generator"
]

View 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"
}
]
}
}

View 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
View 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.)

View 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[]" ]
}
]
}
}

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

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

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

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

View File

@ -0,0 +1,4 @@
[
"PolicyActionDecorator",
"PolicyProvider"
]