mirror of
https://github.com/nasa/openmct.git
synced 2024-12-24 07:16:39 +00:00
[Containment] Merge in policy
Merge in policy support, as this is necessary to support containment rules, WTD-962.
This commit is contained in:
commit
931ca73d6b
@ -14,6 +14,7 @@
|
||||
"platform/forms",
|
||||
"platform/persistence/queue",
|
||||
"platform/persistence/elastic",
|
||||
"platform/policy",
|
||||
|
||||
"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"
|
||||
]
|
Loading…
Reference in New Issue
Block a user