Compare commits

...

9 Commits

Author SHA1 Message Date
Victor Woeltjen
9ae898427a [Build] Read commit hash from package.json
...when not building from a git repository. Fixes #1924
2018-02-22 15:08:09 -08:00
Pete Richards
12574a1333 Tests for Composition API providers 2018-02-20 09:40:57 -08:00
Deep Tailor
dc91a94f0e Merge pull request #1913 from nasa/layout-issue-1909
[Layout] Select fixed position view only if the parent is not selected
2018-02-15 15:20:03 -08:00
Pete Richards
0243aa6584 [API] provider support for dynamic composition is optional (#1915)
All views are expected to implement dynamic composition handling
by listening for the "add" and "remove" events and then calling
"collection.load()" when they are ready to handle these events.

However, it does not make sense that every composition provider will
be dynamic, so implementing support for dynamic composition should
not be a requirement.  This commit removes that requirement.

Fixes #1914
2018-02-13 18:00:35 -08:00
Pegah Sarram
e5d869f01e [Layout] Select the fixed position view only if the parent is not selected.
Also, add mutation listener if domain object is defined to fix the TypeError.

Fixes # 1909 and #1912
2018-02-13 13:23:49 -08:00
Pegah Sarram
d4e3e6689c [Inspector] Listen for mutation and refresh composition
...so that elements pool is updated when selected object's composition changes. Fixes #1869
2018-02-12 10:49:56 -08:00
Sam Price
0363d0e8ad d3 selection filepath changed (#1898)
* d3 selection changed from build to dist.
* build to dist for test-main.js
2018-02-05 11:12:22 -08:00
Deep Tailor
3669e776a9 add parameter for background color, only change color when parameter is passed in (export image service is used in notebook which needs the background color not changed
added tests
2018-02-05 10:46:56 -08:00
Victor Woeltjen
5d3adc6a7f [Documentation] Add security guide (#1900)
* [Documentation] Add initial security overview content

Fixes #1833

* [Documentation] Outline security guide

* [Documentation] Retitle Security Guide

* [Documentation] Reformat security procedures

* [Documentation] Flesh out security notes

* [Documentation] Add references to Security Guide

* [Documentation] Note role of static analysis

https://github.com/nasa/openmct/pull/1900#pullrequestreview-93769470
2018-02-02 14:23:08 -08:00
12 changed files with 541 additions and 57 deletions

121
docs/src/guide/security.md Normal file
View File

@@ -0,0 +1,121 @@
# Security Guide
Open MCT is a rich client with plugin support that executes as a single page
web application in a browser environment. Security concerns and
vulnerabilities associated with the web as a platform should be considered
before deploying Open MCT (or any other web application) for mission or
production usage.
This document describes several important points to consider when developing
for or deploying Open MCT securely. Other resources such as
[Open Web Application Security Project (OWASP)](https://www.owasp.org)
provide a deeper and more general overview of security for web applications.
## Security Model
Open MCT has been architected assuming the following deployment pattern:
* A tagged, tested Open MCT version will be used.
* Externally authored plugins will be installed.
* A server will provide persistent storage, telemetry, and other shared data.
* Authorization, authentication, and auditing will be handled by a server.
## Security Procedures
The Open MCT team secures our code base using a combination of code review,
dependency review, and periodic security reviews. Static analysis performed
during automated verification additionally safeguards against common
coding errors which may result in vulnerabilities.
### Code Review
All contributions are reviewed by internal team members. External
contributors receive increased scrutiny for security and quality,
and must sign a licensing agreement.
### Dependency Review
Before integrating third-party dependencies, they are reviewed for security
and quality, with consideration given to authors and users of these
dependencies, as well as review of open source code.
### Periodic Security Reviews
Open MCT's code, design, and architecture are periodically reviewed
(approximately annually) for common security issues, such as the
[OWASP Top Ten](https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project).
## Security Concerns
Certain security concerns deserve special attention when deploying Open MCT,
or when authoring plugins.
### Identity Spoofing
Open MCT issues calls to web services with the privileges of a logged in user.
Compromised sources (either for Open MCT itself or a plugin) could
therefore allow malicious code to execute with those privileges.
To avoid this:
* Serve Open MCT and other scripts over SSL (https rather than http)
to prevent man-in-the-middle attacks.
* Exercise precautions such as security reviews for any plugins or
applications built for or with Open MCT to reject malicious changes.
### Information Disclosure
If Open MCT is used to handle or display sensitive data, any components
(such as adapter plugins) must take care to avoid leaking or disclosing
this information. For example, avoid sending sensitive data to third-party
servers or insecure APIs.
### Data Tampering
The web application architecture leaves open the possibility that direct
calls will be made to back-end services, circumventing Open MCT entirely.
As such, Open MCT assumes that server components will perform any necessary
data validation during calls issues to the server.
Additionally, plugins which serialize and write data to the server must
escape that data to avoid database injection attacks, and similar.
### Repudiation
Open MCT assumes that servers log any relevant interactions and associates
these with a user identity; the specific user actions taken within the
application are assumed not to be of concern for auditing.
In the absence of server-side logging, users may disclaim (maliciously,
mistakenly, or otherwise) actions taken within the system without any
way to prove otherwise.
If keeping client-level interactions is important, this will need to be
implemented via a plugin.
### Denial-of-service
Open MCT assumes that server-side components will be insulated against
denial-of-service attacks. Services should only permit resource-intensive
tasks to be initiated by known or trusted users.
### Elevation of Privilege
Corollary to the assumption that servers guide against identity spoofing,
Open MCT assumes that services do not allow a user to act with
inappropriately escalated privileges. Open MCT cannot protect against
such escalation; in the clearest case, a malicious actor could interact
with web services directly to exploit such a vulnerability.
## Additional Reading
The following resources have been used as a basis for identifying potential
security threats to Open MCT deployments in preparation of this document:
* [STRIDE model](https://www.owasp.org/index.php/Threat_Risk_Modeling#STRIDE)
* [Attack Surface Analysis Cheat Sheet](https://www.owasp.org/index.php/Attack_Surface_Analysis_Cheat_Sheet)
* [XSS Prevention Cheat Sheet](https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet)

View File

@@ -52,7 +52,9 @@ var gulp = require('gulp'),
var buildVariables = {
version: project.version,
timestamp: moment.utc(Date.now()).format(),
revision: fs.existsSync('.git') ? git.long() : 'Unknown',
revision: fs.existsSync('.git') ? git.long() :
project._resolved ? project._resolved.replace(/^.*#/g, "") :
'Unknown',
branch: fs.existsSync('.git') ? git.branch() : 'Unknown'
};
return fs.readFileSync("src/start.frag", 'utf-8')

View File

@@ -40,7 +40,7 @@ requirejs.config({
"vue": "node_modules/vue/dist/vue.min",
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash",
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
"d3-array": "node_modules/d3-array/build/d3-array.min",

View File

@@ -52,8 +52,22 @@ define(
}
function setSelection(selection) {
self.scope.selection = selection;
self.refreshComposition(selection);
if (!selection[0]) {
return;
}
if (self.mutationListener) {
self.mutationListener();
delete self.mutationListener;
}
var domainObject = selection[0].context.oldItem;
self.refreshComposition(domainObject);
if (domainObject) {
self.mutationListener = domainObject.getCapability('mutation')
.listen(self.refreshComposition.bind(self, domainObject));
}
}
$scope.filterBy = filterBy;
@@ -70,16 +84,11 @@ define(
/**
* Gets the composition for the selected object and populates the scope with it.
*
* @param selection the selection object
* @param domainObject the selected object
* @private
*/
ElementsController.prototype.refreshComposition = function (selection) {
if (!selection[0]) {
return;
}
var selected = selection[0].context.oldItem;
var selectedObjectComposition = selected && selected.useCapability('composition');
ElementsController.prototype.refreshComposition = function (domainObject) {
var selectedObjectComposition = domainObject && domainObject.useCapability('composition');
if (selectedObjectComposition) {
selectedObjectComposition.then(function (composition) {

View File

@@ -29,9 +29,25 @@ define(
var mockScope,
mockOpenMCT,
mockSelection,
mockDomainObject,
mockMutationCapability,
mockUnlisten,
selectable = [],
controller;
beforeEach(function () {
mockUnlisten = jasmine.createSpy('unlisten');
mockMutationCapability = jasmine.createSpyObj("mutationCapability", [
"listen"
]);
mockMutationCapability.listen.andReturn(mockUnlisten);
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getCapability",
"useCapability"
]);
mockDomainObject.useCapability.andCallThrough();
mockDomainObject.getCapability.andReturn(mockMutationCapability);
mockScope = jasmine.createSpyObj("$scope", ['$on']);
mockSelection = jasmine.createSpyObj("selection", [
'on',
@@ -43,6 +59,14 @@ define(
selection: mockSelection
};
selectable[0] = {
context: {
oldItem: mockDomainObject
}
};
spyOn(ElementsController.prototype, 'refreshComposition');
controller = new ElementsController(mockScope, mockOpenMCT);
});
@@ -75,6 +99,44 @@ define(
expect(objects.filter(mockScope.searchElements).length).toBe(4);
});
it("refreshes composition on selection", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(ElementsController.prototype.refreshComposition).toHaveBeenCalledWith(mockDomainObject);
});
it("listens on mutation and refreshes composition", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('mutation');
expect(mockMutationCapability.listen).toHaveBeenCalled();
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(1);
mockMutationCapability.listen.mostRecentCall.args[0](mockDomainObject);
expect(ElementsController.prototype.refreshComposition.calls.length).toBe(2);
});
it("cleans up mutation listener when selection changes", function () {
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockMutationCapability.listen).toHaveBeenCalled();
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockUnlisten).toHaveBeenCalled();
});
it("does not listen on mutation for element proxy selectable", function () {
selectable[0] = {
context: {
elementProxy: {}
}
};
mockOpenMCT.selection.on.mostRecentCall.args[1](selectable);
expect(mockDomainObject.getCapability).not.toHaveBeenCalledWith('mutation');
});
});
}
);

View File

@@ -272,7 +272,7 @@ define(
self.resizeHandles = self.generateDragHandles(self.selectedElementProxy);
} else {
// Make fixed view selectable if it's not already.
if (!self.fixedViewSelectable) {
if (!self.fixedViewSelectable && selectable.length === 1) {
self.fixedViewSelectable = true;
selection.context.viewProxy = new FixedProxy(addElement, $q, dialogService);
self.openmct.selection.select(selection);

View File

@@ -415,7 +415,7 @@ define(
PlotController.prototype.exportPNG = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportPNG(self.$element[0], "plot.png").finally(function () {
self.exportImageService.exportPNG(self.$element[0], "plot.png", 'white').finally(function () {
self.hideExportButtons = false;
});
};
@@ -426,7 +426,7 @@ define(
PlotController.prototype.exportJPG = function () {
var self = this;
self.hideExportButtons = true;
self.exportImageService.exportJPG(self.$element[0], "plot.jpg").finally(function () {
self.exportImageService.exportJPG(self.$element[0], "plot.jpg", 'white').finally(function () {
self.hideExportButtons = false;
});
};

View File

@@ -61,7 +61,7 @@ define(
* @param {string} type of image to convert the element to
* @returns {promise}
*/
function renderElement(element, type) {
function renderElement(element, type, color) {
var defer = self.$q.defer(),
validTypes = ["png", "jpg", "jpeg"],
renderTimeout,
@@ -72,11 +72,13 @@ define(
return;
}
// Save color to be restored later
originalColor = element.style.backgroundColor || '';
if (color) {
// Save color to be restored later
originalColor = element.style.backgroundColor || '';
// Defaulting to white so we can see the chart when printed
self.changeBackgroundColor(element, 'white');
// Defaulting to white so we can see the chart when printed
self.changeBackgroundColor(element, color);
}
renderTimeout = self.$timeout(function () {
defer.reject("html2canvas timed out");
@@ -86,7 +88,9 @@ define(
try {
self.html2canvas(element, {
onrendered: function (canvas) {
self.changeBackgroundColor(element, originalColor);
if (color) {
self.changeBackgroundColor(element, originalColor);
}
switch (type.toLowerCase()) {
case "png":
@@ -106,7 +110,10 @@ define(
defer.promise.finally(function () {
renderTimeout.cancel();
self.changeBackgroundColor(element, originalColor);
if (color) {
self.changeBackgroundColor(element, originalColor);
}
});
return defer.promise;
@@ -149,8 +156,8 @@ define(
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename) {
return renderElement(element, "jpeg").then(function (img) {
ExportImageService.prototype.exportJPG = function (element, filename, color) {
return renderElement(element, "jpeg", color).then(function (img) {
self.saveAs(img, filename);
});
};
@@ -161,8 +168,8 @@ define(
* @param {string} filename the exported image
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename) {
return renderElement(element, "png").then(function (img) {
ExportImageService.prototype.exportPNG = function (element, filename, color) {
return renderElement(element, "png", color).then(function (img) {
self.saveAs(img, filename);
});
};

View File

@@ -121,16 +121,26 @@ define(
});
it("changes background color to white and returns color back to original after snapshot, for better visibility of plot lines on print", function () {
exportImageService.exportPNG(testElement, "plot.png");
exportImageService.exportPNG(testElement, "plot.png", 'white');
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
exportImageService.exportJPG(testElement, "plot.jpg");
exportImageService.exportJPG(testElement, "plot.jpg", 'white');
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'white');
expect(mockChangeBackgroundColor).toHaveBeenCalledWith(testElement, 'black');
});
it("does not change background color when color is not specified in parameters", function () {
exportImageService.exportPNG(testElement, "plot.png");
expect(mockChangeBackgroundColor).not.toHaveBeenCalled();
exportImageService.exportJPG(testElement, "plot.jpg");
expect(mockChangeBackgroundColor).not.toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,269 @@
define([
'./CompositionAPI',
'./CompositionCollection'
], function (
CompositionAPI,
CompositionCollection
) {
describe('The Composition API', function () {
var publicAPI;
var compositionAPI;
var topicService;
var mutationTopic;
beforeEach(function () {
mutationTopic = jasmine.createSpyObj('mutationTopic', [
'listen'
]);
topicService = jasmine.createSpy('topicService');
topicService.andReturn(mutationTopic);
publicAPI = {};
publicAPI.objects = jasmine.createSpyObj('ObjectAPI', [
'get'
]);
publicAPI.objects.get.andCallFake(function (identifier) {
return Promise.resolve({identifier: identifier});
});
publicAPI.$injector = jasmine.createSpyObj('$injector', [
'get'
]);
publicAPI.$injector.get.andReturn(topicService);
compositionAPI = new CompositionAPI(publicAPI);
});
it('returns falsy if an object does not support composition', function () {
expect(compositionAPI.get({})).toBeFalsy();
});
describe('default composition', function () {
var domainObject;
var composition;
beforeEach(function () {
domainObject = {
name: 'test folder',
identifier: {
namespace: 'test',
key: '1'
},
composition: [
{
namespace: 'test',
key: 'a'
}
]
};
composition = compositionAPI.get(domainObject);
});
it('returns composition collection', function () {
expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
it('loads composition from domain object', function () {
var listener = jasmine.createSpy('addListener');
var loaded = false;
composition.on('add', listener);
composition.load()
.then(function () {
loaded = true;
});
waitsFor(function () {
return loaded;
});
runs(function () {
expect(listener.calls.length).toBe(1);
expect(listener).toHaveBeenCalledWith({
identifier: {namespace: 'test', key: 'a'}
});
});
});
// TODO: Implement add/removal in new default provider.
xit('synchronizes changes between instances', function () {
var otherComposition = compositionAPI.get(domainObject);
var addListener = jasmine.createSpy('addListener');
var removeListener = jasmine.createSpy('removeListener');
var otherAddListener = jasmine.createSpy('otherAddListener');
var otherRemoveListener = jasmine.createSpy('otherRemoveListener');
composition.on('add', addListener);
composition.on('remove', removeListener);
otherComposition.on('add', otherAddListener);
otherComposition.on('remove', otherRemoveListener);
var loaded = false;
Promise.all([composition.load(), otherComposition.load()])
.then(function () {
loaded = true;
});
waitsFor(function () {
return loaded;
});
runs(function () {
expect(addListener).toHaveBeenCalled();
expect(otherAddListener).toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
expect(otherRemoveListener).not.toHaveBeenCalled();
var object = addListener.mostRecentCall.args[0];
composition.remove(object);
expect(removeListener).toHaveBeenCalled();
expect(otherRemoveListener).toHaveBeenCalled();
addListener.reset();
otherAddListener.reset();
composition.add(object);
expect(addListener).toHaveBeenCalled();
expect(otherAddListener).toHaveBeenCalled();
removeListener.reset();
otherRemoveListener.reset();
otherComposition.remove(object);
expect(removeListener).toHaveBeenCalled();
expect(otherRemoveListener).toHaveBeenCalled();
addListener.reset();
otherAddListener.reset();
otherComposition.add(object);
expect(addListener).toHaveBeenCalled();
expect(otherAddListener).toHaveBeenCalled();
});
});
});
describe('static custom composition', function () {
var customProvider;
var domainObject;
var composition;
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
}
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
var listener = jasmine.createSpy('addListener');
var loaded = false;
composition.on('add', listener);
composition.load()
.then(function () {
loaded = true;
});
waitsFor(function () {
return loaded;
});
runs(function () {
expect(listener.calls.length).toBe(1);
expect(listener).toHaveBeenCalledWith({
identifier: {namespace: 'custom', key: 'thing'}
});
});
});
});
describe('dynamic custom composition', function () {
var customProvider;
var domainObject;
var composition;
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', [
'appliesTo',
'load',
'on',
'off'
]);
customProvider.appliesTo.andReturn('true');
customProvider.load.andReturn(Promise.resolve([]));
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
var addListener = jasmine.createSpy('addListener');
var removeListener = jasmine.createSpy('removeListener');
var loaded = false;
composition.on('add', addListener);
composition.on('remove', removeListener);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
var add = customProvider.on.calls[0].args[2];
var remove = customProvider.on.calls[1].args[2];
composition.load()
.then(function () {
loaded = true;
});
waitsFor(function () {
return loaded;
});
runs(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({namespace: 'custom', key: 'thing'});
});
waitsFor(function () {
return addListener.calls.length > 0;
});
runs(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {namespace: 'custom', key: 'thing'}
});
remove(addListener.mostRecentCall.args[0]);
});
waitsFor(function () {
return removeListener.calls.length > 0;
});
runs(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {namespace: 'custom', key: 'thing'}
});
});
});
});
});
});

View File

@@ -76,20 +76,22 @@ define([
throw new Error('Event not supported by composition: ' + event);
}
if (event === 'add') {
this.provider.on(
this.domainObject,
'add',
this.onProviderAdd,
this
);
} if (event === 'remove') {
this.provider.on(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
if (this.provider.on && this.provider.off) {
if (event === 'add') {
this.provider.on(
this.domainObject,
'add',
this.onProviderAdd,
this
);
} if (event === 'remove') {
this.provider.on(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
}
}
this.listeners[event].push({
@@ -124,20 +126,22 @@ define([
if (this.listeners[event].length === 0) {
// Remove provider listener if this is the last callback to
// be removed.
if (event === 'add') {
this.provider.off(
this.domainObject,
'add',
this.onProviderAdd,
this
);
} else if (event === 'remove') {
this.provider.off(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
if (this.provider.off && this.provider.on) {
if (event === 'add') {
this.provider.off(
this.domainObject,
'add',
this.onProviderAdd,
this
);
} else if (event === 'remove') {
this.provider.off(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
}
}
}
};

View File

@@ -66,7 +66,7 @@ requirejs.config({
"vue": "node_modules/vue/dist/vue.min",
"zepto": "bower_components/zepto/zepto.min",
"lodash": "bower_components/lodash/lodash",
"d3-selection": "node_modules/d3-selection/build/d3-selection.min",
"d3-selection": "node_modules/d3-selection/dist/d3-selection.min",
"d3-scale": "node_modules/d3-scale/build/d3-scale.min",
"d3-axis": "node_modules/d3-axis/build/d3-axis.min",
"d3-array": "node_modules/d3-array/build/d3-array.min",