Add security check (#7247)
* added Parse Server security option * added SecurityRouter * added Check class * added CheckGroup class * moved parameter validation to Utils * added CheckRunner class * added auto-run on server start * added custom security checks as Parse Server option * renamed script to check * reformat log output * added server config check * improved contributing guideline * improved contribution guide * added check security log * improved log format * added checks * fixed log fomat typo * added database checks * fixed database check * removed database auth check in initial version * improved contribution guide * added security check tests * fixed typo * improved wording guidelines * improved wording guidelines
This commit is contained in:
@@ -13,6 +13,8 @@
|
||||
- [Postgres with Docker](#postgres-with-docker)
|
||||
- [Feature Considerations](#feature-considerations)
|
||||
- [Security Checks](#security-checks)
|
||||
- [Add Security Check](#add-security-check)
|
||||
- [Wording Guideline](#wording-guideline)
|
||||
- [Parse Error](#parse-error)
|
||||
- [Parse Server Configuration](#parse-server-configuration)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
@@ -162,7 +164,55 @@ A security check needs to be added for every new feature or enhancement that all
|
||||
|
||||
For example, allowing public read and write to a class may be useful to simplify development but should be disallowed in a production environment.
|
||||
|
||||
Security checks are added in [SecurityChecks.js](https://github.com/parse-community/parse-server/blob/master/src/SecurityChecks.js).
|
||||
Security checks are added in [CheckGroups](https://github.com/parse-community/parse-server/tree/master/src/Security/CheckGroups).
|
||||
|
||||
#### Add Security Check
|
||||
Adding a new security check for your feature is easy and fast:
|
||||
1. Look into [CheckGroups](https://github.com/parse-community/parse-server/tree/master/src/Security/CheckGroups) whether there is an existing `CheckGroup[Category].js` file for the category of check to add. For example, a check regarding the database connection is added to `CheckGroupDatabase.js`.
|
||||
2. If you did not find a file, duplicate an existing file and replace the category name in `setName()` and the checks in `setChecks()`:
|
||||
```js
|
||||
class CheckGroupNewCategory extends CheckGroup {
|
||||
setName() {
|
||||
return 'House';
|
||||
}
|
||||
setChecks() {
|
||||
return [
|
||||
new Check({
|
||||
title: 'Door locked',
|
||||
warning: 'Anyone can enter your house.',
|
||||
solution: 'Lock the door.',
|
||||
check: () => {
|
||||
return; // Example of a passing check
|
||||
}
|
||||
}),
|
||||
new Check({
|
||||
title: 'Camera online',
|
||||
warning: 'Security camera is offline.',
|
||||
solution: 'Check the camera.',
|
||||
check: async () => {
|
||||
throw 1; // Example of a failing check
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. If you added a new file in the previous step, reference the file in [CheckGroups.js](https://github.com/parse-community/parse-server/blob/master/src/Security/CheckGroups/CheckGroups.js), which is the collector of all security checks:
|
||||
```
|
||||
export { default as CheckGroupNewCategory } from './CheckGroupNewCategory';
|
||||
```
|
||||
4. Add a test that covers the new check to [SecurityCheckGroups.js](https://github.com/parse-community/parse-server/blob/master/spec/SecurityCheckGroups.js) for the cases of success and failure.
|
||||
|
||||
#### Wording Guideline
|
||||
Consider the following when adding a new security check:
|
||||
- *Group.name*: The category name; ends without period as this is a headline.
|
||||
- *Check.title*: Is the positive hypothesis that should be checked, for example "Door locked" instead of "Door unlocked"; ends without period as this is a title.
|
||||
- *Check.warning*: The warning if the test fails; ends with period as this is a description.
|
||||
- *Check.solution*: The recommended solution if the test fails; ends with period as this is an instruction.
|
||||
- The wordings must not contain any sensitive information such as keys, as the security report may be exposed in logs.
|
||||
- The wordings should be concise and not contain verbose explanations, for example "Door locked" instead of "Door has been locked securely".
|
||||
- Do not use pronouns such as "you" or "your" because log files can have various readers with different roles. Do not use pronouns such as "I" or "me" because although we love it dearly, Parse Server is not a human.
|
||||
|
||||
### Parse Error
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ function getENVPrefix(iface) {
|
||||
'AccountLockoutOptions' : 'PARSE_SERVER_ACCOUNT_LOCKOUT_',
|
||||
'PasswordPolicyOptions' : 'PARSE_SERVER_PASSWORD_POLICY_',
|
||||
'FileUploadOptions' : 'PARSE_SERVER_FILE_UPLOAD_',
|
||||
'SecurityOptions': 'PARSE_SERVER_SECURITY_',
|
||||
}
|
||||
if (options[iface.id.name]) {
|
||||
return options[iface.id.name]
|
||||
@@ -167,7 +168,7 @@ function parseDefaultValue(elt, value, t) {
|
||||
if (type == 'NumberOrBoolean') {
|
||||
literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value));
|
||||
}
|
||||
const literalTypes = ['Object', 'PagesRoute', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions'];
|
||||
const literalTypes = ['Object', 'SecurityOptions', 'PagesRoute', 'IdempotencyOptions','FileUploadOptions','CustomPagesOptions', 'PagesCustomUrlsOptions', 'PagesOptions'];
|
||||
if (literalTypes.includes(type)) {
|
||||
const object = parsers.objectParser(value);
|
||||
const props = Object.keys(object).map((key) => {
|
||||
|
||||
333
spec/SecurityCheck.spec.js
Normal file
333
spec/SecurityCheck.spec.js
Normal file
@@ -0,0 +1,333 @@
|
||||
'use strict';
|
||||
|
||||
const Utils = require('../lib/Utils');
|
||||
const Config = require('../lib/Config');
|
||||
const request = require('../lib/request');
|
||||
const Definitions = require('../lib/Options/Definitions');
|
||||
const { Check, CheckState } = require('../lib/Security/Check');
|
||||
const CheckGroup = require('../lib/Security/CheckGroup');
|
||||
const CheckRunner = require('../lib/Security/CheckRunner');
|
||||
const CheckGroups = require('../lib/Security/CheckGroups/CheckGroups');
|
||||
|
||||
describe('Security Check', () => {
|
||||
let Group;
|
||||
let groupName;
|
||||
let checkSuccess;
|
||||
let checkFail;
|
||||
let config;
|
||||
const publicServerURL = 'http://localhost:8378/1';
|
||||
const securityUrl = publicServerURL + '/security';
|
||||
|
||||
async function reconfigureServerWithSecurityConfig(security) {
|
||||
config.security = security;
|
||||
await reconfigureServer(config);
|
||||
}
|
||||
|
||||
const securityRequest = (options) => request(Object.assign({
|
||||
url: securityUrl,
|
||||
headers: {
|
||||
'X-Parse-Master-Key': Parse.masterKey,
|
||||
'X-Parse-Application-Id': Parse.applicationId,
|
||||
},
|
||||
followRedirects: false,
|
||||
}, options)).catch(e => e);
|
||||
|
||||
beforeEach(async () => {
|
||||
groupName = 'Example Group Name';
|
||||
checkSuccess = new Check({
|
||||
group: 'TestGroup',
|
||||
title: 'TestTitleSuccess',
|
||||
warning: 'TestWarning',
|
||||
solution: 'TestSolution',
|
||||
check: () => {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
checkFail = new Check({
|
||||
group: 'TestGroup',
|
||||
title: 'TestTitleFail',
|
||||
warning: 'TestWarning',
|
||||
solution: 'TestSolution',
|
||||
check: () => {
|
||||
throw 'Fail';
|
||||
}
|
||||
});
|
||||
Group = class Group extends CheckGroup {
|
||||
setName() {
|
||||
return groupName;
|
||||
}
|
||||
setChecks() {
|
||||
return [ checkSuccess, checkFail ];
|
||||
}
|
||||
};
|
||||
config = {
|
||||
appId: 'test',
|
||||
appName: 'ExampleAppName',
|
||||
publicServerURL,
|
||||
security: {
|
||||
enableCheck: true,
|
||||
enableCheckLog: true,
|
||||
},
|
||||
};
|
||||
await reconfigureServer(config);
|
||||
});
|
||||
|
||||
describe('server options', () => {
|
||||
it('uses default configuration when none is set', async () => {
|
||||
await reconfigureServerWithSecurityConfig({});
|
||||
expect(Config.get(Parse.applicationId).security.enableCheck).toBe(
|
||||
Definitions.SecurityOptions.enableCheck.default
|
||||
);
|
||||
expect(Config.get(Parse.applicationId).security.enableCheckLog).toBe(
|
||||
Definitions.SecurityOptions.enableCheckLog.default
|
||||
);
|
||||
});
|
||||
|
||||
it('throws on invalid configuration', async () => {
|
||||
const options = [
|
||||
[],
|
||||
'a',
|
||||
0,
|
||||
true,
|
||||
{ enableCheck: 'a' },
|
||||
{ enableCheck: 0 },
|
||||
{ enableCheck: {} },
|
||||
{ enableCheck: [] },
|
||||
{ enableCheckLog: 'a' },
|
||||
{ enableCheckLog: 0 },
|
||||
{ enableCheckLog: {} },
|
||||
{ enableCheckLog: [] },
|
||||
];
|
||||
for (const option of options) {
|
||||
await expectAsync(reconfigureServerWithSecurityConfig(option)).toBeRejected();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-run', () => {
|
||||
it('runs security checks on server start if enabled', async () => {
|
||||
const runnerSpy = spyOn(CheckRunner.prototype, 'run').and.callThrough();
|
||||
await reconfigureServerWithSecurityConfig({ enableCheck: true, enableCheckLog: true });
|
||||
expect(runnerSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not run security checks on server start if disabled', async () => {
|
||||
const runnerSpy = spyOn(CheckRunner.prototype, 'run').and.callThrough();
|
||||
const configs = [
|
||||
{ enableCheck: true, enableCheckLog: false },
|
||||
{ enableCheck: false, enableCheckLog: false },
|
||||
{ enableCheck: false },
|
||||
{},
|
||||
];
|
||||
for (const config of configs) {
|
||||
await reconfigureServerWithSecurityConfig(config);
|
||||
expect(runnerSpy).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('security endpoint accessibility', () => {
|
||||
it('responds with 403 without masterkey', async () => {
|
||||
const response = await securityRequest({ headers: {} });
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it('responds with 409 with masterkey and security check disabled', async () => {
|
||||
await reconfigureServerWithSecurityConfig({});
|
||||
const response = await securityRequest();
|
||||
expect(response.status).toBe(409);
|
||||
});
|
||||
|
||||
it('responds with 200 with masterkey and security check enabled', async () => {
|
||||
const response = await securityRequest();
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check', () => {
|
||||
const initCheck = config => (() => new Check(config)).bind(null);
|
||||
|
||||
it('instantiates check with valid parameters', async () => {
|
||||
const configs = [
|
||||
{
|
||||
group: 'string',
|
||||
title: 'string',
|
||||
warning: 'string',
|
||||
solution: 'string',
|
||||
check: () => {}
|
||||
},
|
||||
{
|
||||
group: 'string',
|
||||
title: 'string',
|
||||
warning: 'string',
|
||||
solution: 'string',
|
||||
check: async () => {},
|
||||
},
|
||||
];
|
||||
for (const config of configs) {
|
||||
expect(initCheck(config)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws instantiating check with invalid parameters', async () => {
|
||||
const configDefinition = {
|
||||
group: [false, true, 0, 1, [], {}, () => {}],
|
||||
title: [false, true, 0, 1, [], {}, () => {}],
|
||||
warning: [false, true, 0, 1, [], {}, () => {}],
|
||||
solution: [false, true, 0, 1, [], {}, () => {}],
|
||||
check: [false, true, 0, 1, [], {}, 'string'],
|
||||
};
|
||||
const configs = Utils.getObjectKeyPermutations(configDefinition);
|
||||
|
||||
for (const config of configs) {
|
||||
expect(initCheck(config)).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('sets correct states for check success', async () => {
|
||||
const check = new Check({
|
||||
group: 'string',
|
||||
title: 'string',
|
||||
warning: 'string',
|
||||
solution: 'string',
|
||||
check: () => {},
|
||||
});
|
||||
expect(check._checkState == CheckState.none);
|
||||
check.run();
|
||||
expect(check._checkState == CheckState.success);
|
||||
});
|
||||
|
||||
it('sets correct states for check fail', async () => {
|
||||
const check = new Check({
|
||||
group: 'string',
|
||||
title: 'string',
|
||||
warning: 'string',
|
||||
solution: 'string',
|
||||
check: () => { throw 'error' },
|
||||
});
|
||||
expect(check._checkState == CheckState.none);
|
||||
check.run();
|
||||
expect(check._checkState == CheckState.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check group', () => {
|
||||
it('returns properties if subclassed correctly', async () => {
|
||||
const group = new Group();
|
||||
expect(group.name()).toBe(groupName);
|
||||
expect(group.checks().length).toBe(2);
|
||||
expect(group.checks()[0]).toEqual(checkSuccess);
|
||||
expect(group.checks()[1]).toEqual(checkFail);
|
||||
});
|
||||
|
||||
it('throws if subclassed incorrectly', async () => {
|
||||
class InvalidGroup1 extends CheckGroup {}
|
||||
expect((() => new InvalidGroup1()).bind()).toThrow('Check group has no name.');
|
||||
class InvalidGroup2 extends CheckGroup {
|
||||
setName() {
|
||||
return groupName;
|
||||
}
|
||||
}
|
||||
expect((() => new InvalidGroup2()).bind()).toThrow('Check group has no checks.');
|
||||
});
|
||||
|
||||
it('runs checks', async () => {
|
||||
const group = new Group();
|
||||
expect(group.checks()[0].checkState()).toBe(CheckState.none);
|
||||
expect(group.checks()[1].checkState()).toBe(CheckState.none);
|
||||
expect((() => group.run()).bind(null)).not.toThrow();
|
||||
expect(group.checks()[0].checkState()).toBe(CheckState.success);
|
||||
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('check runner', () => {
|
||||
const initRunner = config => (() => new CheckRunner(config)).bind(null);
|
||||
|
||||
it('instantiates runner with valid parameters', async () => {
|
||||
const configDefinition = {
|
||||
enableCheck: [false, true, undefined],
|
||||
enableCheckLog: [false, true, undefined],
|
||||
checkGroups: [[], undefined],
|
||||
};
|
||||
const configs = Utils.getObjectKeyPermutations(configDefinition);
|
||||
for (const config of configs) {
|
||||
expect(initRunner(config)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws instantiating runner with invalid parameters', async () => {
|
||||
const configDefinition = {
|
||||
enableCheck: [0, 1, [], {}, () => {}],
|
||||
enableCheckLog: [0, 1, [], {}, () => {}],
|
||||
checkGroups: [false, true, 0, 1, {}, () => {}],
|
||||
};
|
||||
const configs = Utils.getObjectKeyPermutations(configDefinition);
|
||||
|
||||
for (const config of configs) {
|
||||
expect(initRunner(config)).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('instantiates runner with default parameters', async () => {
|
||||
const runner = new CheckRunner();
|
||||
expect(runner.enableCheck).toBeFalse();
|
||||
expect(runner.enableCheckLog).toBeFalse();
|
||||
expect(runner.checkGroups).toBe(CheckGroups);
|
||||
});
|
||||
|
||||
it('runs all checks of all groups', async () => {
|
||||
const checkGroups = [ Group, Group ];
|
||||
const runner = new CheckRunner({ checkGroups });
|
||||
const report = await runner.run();
|
||||
expect(report.report.groups[0].checks[0].state).toBe(CheckState.success);
|
||||
expect(report.report.groups[0].checks[1].state).toBe(CheckState.fail);
|
||||
expect(report.report.groups[1].checks[0].state).toBe(CheckState.success);
|
||||
expect(report.report.groups[1].checks[1].state).toBe(CheckState.fail);
|
||||
});
|
||||
|
||||
it('reports correct default syntax version 1.0.0', async () => {
|
||||
const checkGroups = [ Group ];
|
||||
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
|
||||
const report = await runner.run();
|
||||
expect(report).toEqual({
|
||||
report: {
|
||||
version: "1.0.0",
|
||||
state: "fail",
|
||||
groups: [
|
||||
{
|
||||
name: "Example Group Name",
|
||||
state: "fail",
|
||||
checks: [
|
||||
{
|
||||
title: "TestTitleSuccess",
|
||||
state: "success",
|
||||
},
|
||||
{
|
||||
title: "TestTitleFail",
|
||||
state: "fail",
|
||||
warning: "TestWarning",
|
||||
solution: "TestSolution",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('logs report', async () => {
|
||||
const logger = require('../lib/logger').logger;
|
||||
const logSpy = spyOn(logger, 'warn').and.callThrough();
|
||||
const checkGroups = [ Group ];
|
||||
const runner = new CheckRunner({ checkGroups, enableCheckLog: true });
|
||||
const report = await runner.run();
|
||||
const titles = report.report.groups.flatMap(group => group.checks.map(check => check.title));
|
||||
expect(titles.length).toBe(2);
|
||||
|
||||
for (const title of titles) {
|
||||
expect(logSpy.calls.all()[0].args[0]).toContain(title);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
81
spec/SecurityCheckGroups.spec.js
Normal file
81
spec/SecurityCheckGroups.spec.js
Normal file
@@ -0,0 +1,81 @@
|
||||
'use strict';
|
||||
|
||||
const Config = require('../lib/Config');
|
||||
const { CheckState } = require('../lib/Security/Check');
|
||||
const CheckGroupServerConfig = require('../lib/Security/CheckGroups/CheckGroupServerConfig');
|
||||
const CheckGroupDatabase = require('../lib/Security/CheckGroups/CheckGroupDatabase');
|
||||
|
||||
describe('Security Check Groups', () => {
|
||||
let config;
|
||||
|
||||
beforeEach(async () => {
|
||||
config = {
|
||||
appId: 'test',
|
||||
appName: 'ExampleAppName',
|
||||
publicServerURL: 'http://localhost:8378/1',
|
||||
security: {
|
||||
enableCheck: true,
|
||||
enableCheckLog: false,
|
||||
},
|
||||
};
|
||||
await reconfigureServer(config);
|
||||
});
|
||||
|
||||
describe('CheckGroupServerConfig', () => {
|
||||
it('is subclassed correctly', async () => {
|
||||
const group = new CheckGroupServerConfig();
|
||||
expect(group.name()).toBeDefined();
|
||||
expect(group.checks().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('checks succeed correctly', async () => {
|
||||
config.masterKey = 'aMoreSecur3Passwor7!';
|
||||
config.security.enableCheckLog = false;
|
||||
config.allowClientClassCreation = false;
|
||||
await reconfigureServer(config);
|
||||
|
||||
const group = new CheckGroupServerConfig();
|
||||
await group.run();
|
||||
expect(group.checks()[0].checkState()).toBe(CheckState.success);
|
||||
expect(group.checks()[1].checkState()).toBe(CheckState.success);
|
||||
expect(group.checks()[2].checkState()).toBe(CheckState.success);
|
||||
});
|
||||
|
||||
it('checks fail correctly', async () => {
|
||||
config.masterKey = 'insecure';
|
||||
config.security.enableCheckLog = true;
|
||||
config.allowClientClassCreation = true;
|
||||
await reconfigureServer(config);
|
||||
|
||||
const group = new CheckGroupServerConfig();
|
||||
await group.run();
|
||||
expect(group.checks()[0].checkState()).toBe(CheckState.fail);
|
||||
expect(group.checks()[1].checkState()).toBe(CheckState.fail);
|
||||
expect(group.checks()[2].checkState()).toBe(CheckState.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CheckGroupDatabase', () => {
|
||||
it('is subclassed correctly', async () => {
|
||||
const group = new CheckGroupDatabase();
|
||||
expect(group.name()).toBeDefined();
|
||||
expect(group.checks().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('checks succeed correctly', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
config.database.adapter._uri = 'protocol://user:aMoreSecur3Passwor7!@example.com';
|
||||
const group = new CheckGroupDatabase();
|
||||
await group.run();
|
||||
expect(group.checks()[0].checkState()).toBe(CheckState.success);
|
||||
});
|
||||
|
||||
it('checks fail correctly', async () => {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
config.database.adapter._uri = 'protocol://user:insecure@example.com';
|
||||
const group = new CheckGroupDatabase();
|
||||
await group.run();
|
||||
expect(group.checks()[0].checkState()).toBe(CheckState.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FileUploadOptions,
|
||||
AccountLockoutOptions,
|
||||
PagesOptions,
|
||||
SecurityOptions,
|
||||
} from './Options/Definitions';
|
||||
import { isBoolean, isString } from 'lodash';
|
||||
|
||||
@@ -79,6 +80,7 @@ export class Config {
|
||||
emailVerifyTokenReuseIfValid,
|
||||
fileUpload,
|
||||
pages,
|
||||
security,
|
||||
}) {
|
||||
if (masterKey === readOnlyMasterKey) {
|
||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||
@@ -114,6 +116,23 @@ export class Config {
|
||||
this.validateAllowHeaders(allowHeaders);
|
||||
this.validateIdempotencyOptions(idempotencyOptions);
|
||||
this.validatePagesOptions(pages);
|
||||
this.validateSecurityOptions(security);
|
||||
}
|
||||
|
||||
static validateSecurityOptions(security) {
|
||||
if (Object.prototype.toString.call(security) !== '[object Object]') {
|
||||
throw 'Parse Server option security must be an object.';
|
||||
}
|
||||
if (security.enableCheck === undefined) {
|
||||
security.enableCheck = SecurityOptions.enableCheck.default;
|
||||
} else if (!isBoolean(security.enableCheck)) {
|
||||
throw 'Parse Server option security.enableCheck must be a boolean.';
|
||||
}
|
||||
if (security.enableCheckLog === undefined) {
|
||||
security.enableCheckLog = SecurityOptions.enableCheckLog.default;
|
||||
} else if (!isBoolean(security.enableCheckLog)) {
|
||||
throw 'Parse Server option security.enableCheckLog must be a boolean.';
|
||||
}
|
||||
}
|
||||
|
||||
static validatePagesOptions(pages) {
|
||||
|
||||
@@ -373,6 +373,12 @@ module.exports.ParseServerOptions = {
|
||||
action: parsers.numberParser('schemaCacheTTL'),
|
||||
default: 5000,
|
||||
},
|
||||
security: {
|
||||
env: 'PARSE_SERVER_SECURITY',
|
||||
help: 'The security options to identify and report weak security settings.',
|
||||
action: parsers.objectParser,
|
||||
default: {},
|
||||
},
|
||||
serverCloseComplete: {
|
||||
env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE',
|
||||
help: 'Callback when server has closed',
|
||||
@@ -424,6 +430,27 @@ module.exports.ParseServerOptions = {
|
||||
help: 'Key sent with outgoing webhook calls',
|
||||
},
|
||||
};
|
||||
module.exports.SecurityOptions = {
|
||||
checkGroups: {
|
||||
env: 'PARSE_SERVER_SECURITY_CHECK_GROUPS',
|
||||
help:
|
||||
'The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.',
|
||||
action: parsers.arrayParser,
|
||||
},
|
||||
enableCheck: {
|
||||
env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK',
|
||||
help: 'Is true if Parse Server should check for weak security settings.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
enableCheckLog: {
|
||||
env: 'PARSE_SERVER_SECURITY_ENABLE_CHECK_LOG',
|
||||
help:
|
||||
'Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
module.exports.PagesOptions = {
|
||||
customRoutes: {
|
||||
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
* @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.
|
||||
* @property {Boolean} scheduledPush Configuration for push scheduling, defaults to false.
|
||||
* @property {Number} schemaCacheTTL The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.
|
||||
* @property {SecurityOptions} security The security options to identify and report weak security settings.
|
||||
* @property {Function} serverCloseComplete Callback when server has closed
|
||||
* @property {Function} serverStartComplete Callback when server has started
|
||||
* @property {String} serverURL URL to your parse server with http:// or https://.
|
||||
@@ -80,6 +81,13 @@
|
||||
* @property {String} webhookKey Key sent with outgoing webhook calls
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface SecurityOptions
|
||||
* @property {CheckGroup[]} checkGroups The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`.
|
||||
* @property {Boolean} enableCheck Is true if Parse Server should check for weak security settings.
|
||||
* @property {Boolean} enableCheckLog Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @interface PagesOptions
|
||||
* @property {PagesRoute[]} customRoutes The custom routes.
|
||||
|
||||
@@ -6,6 +6,7 @@ import { CacheAdapter } from '../Adapters/Cache/CacheAdapter';
|
||||
import { MailAdapter } from '../Adapters/Email/MailAdapter';
|
||||
import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter';
|
||||
import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter';
|
||||
import { CheckGroup } from '../Security/CheckGroup';
|
||||
|
||||
// @flow
|
||||
type Adapter<T> = string | any | T;
|
||||
@@ -227,6 +228,20 @@ export interface ParseServerOptions {
|
||||
serverStartComplete: ?(error: ?Error) => void;
|
||||
/* Callback when server has closed */
|
||||
serverCloseComplete: ?() => void;
|
||||
/* The security options to identify and report weak security settings.
|
||||
:DEFAULT: {} */
|
||||
security: ?SecurityOptions;
|
||||
}
|
||||
|
||||
export interface SecurityOptions {
|
||||
/* Is true if Parse Server should check for weak security settings.
|
||||
:DEFAULT: false */
|
||||
enableCheck: ?boolean;
|
||||
/* Is true if the security check report should be written to logs. This should only be enabled temporarily to not expose weak security settings in logs.
|
||||
:DEFAULT: false */
|
||||
enableCheckLog: ?boolean;
|
||||
/* The security check groups to run. This allows to add custom security checks or override existing ones. Default are the groups defined in `CheckGroups.js`. */
|
||||
checkGroups: ?(CheckGroup[]);
|
||||
}
|
||||
|
||||
export interface PagesOptions {
|
||||
|
||||
@@ -41,6 +41,8 @@ import { AggregateRouter } from './Routers/AggregateRouter';
|
||||
import { ParseServerRESTController } from './ParseServerRESTController';
|
||||
import * as controllers from './Controllers';
|
||||
import { ParseGraphQLServer } from './GraphQL/ParseGraphQLServer';
|
||||
import { SecurityRouter } from './Routers/SecurityRouter';
|
||||
import CheckRunner from './Security/CheckRunner';
|
||||
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
@@ -58,6 +60,7 @@ class ParseServer {
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
cloud,
|
||||
security,
|
||||
javascriptKey,
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
serverStartComplete,
|
||||
@@ -101,6 +104,10 @@ class ParseServer {
|
||||
throw "argument 'cloud' must either be a string or a function";
|
||||
}
|
||||
}
|
||||
|
||||
if (security && security.enableCheck && security.enableCheckLog) {
|
||||
new CheckRunner(options.security).run();
|
||||
}
|
||||
}
|
||||
|
||||
get app() {
|
||||
@@ -219,6 +226,7 @@ class ParseServer {
|
||||
new CloudCodeRouter(),
|
||||
new AudiencesRouter(),
|
||||
new AggregateRouter(),
|
||||
new SecurityRouter(),
|
||||
];
|
||||
|
||||
const routes = routers.reduce((memo, router) => {
|
||||
|
||||
31
src/Routers/SecurityRouter.js
Normal file
31
src/Routers/SecurityRouter.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from '../middlewares';
|
||||
import CheckRunner from '../Security/CheckRunner';
|
||||
|
||||
export class SecurityRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET', '/security',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this._enforceSecurityCheckEnabled,
|
||||
async (req) => {
|
||||
const report = await new CheckRunner(req.config.security).run();
|
||||
return {
|
||||
status: 200,
|
||||
response: report,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _enforceSecurityCheckEnabled(req) {
|
||||
const config = req.config;
|
||||
if (!config.security || !config.security.enableCheck) {
|
||||
const error = new Error();
|
||||
error.status = 409;
|
||||
error.message = 'Enable Parse Server option `security.enableCheck` to run security check.';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SecurityRouter;
|
||||
85
src/Security/Check.js
Normal file
85
src/Security/Check.js
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @module SecurityCheck
|
||||
*/
|
||||
|
||||
import Utils from '../Utils';
|
||||
import { isFunction, isString } from 'lodash';
|
||||
|
||||
/**
|
||||
* A security check.
|
||||
* @class Check
|
||||
*/
|
||||
class Check {
|
||||
/**
|
||||
* Constructs a new security check.
|
||||
* @param {Object} params The parameters.
|
||||
* @param {String} params.title The title.
|
||||
* @param {String} params.warning The warning message if the check fails.
|
||||
* @param {String} params.solution The solution to fix the check.
|
||||
* @param {Promise} params.check The check as synchronous or asynchronous function.
|
||||
*/
|
||||
constructor(params) {
|
||||
this._validateParams(params);
|
||||
const { title, warning, solution, check } = params;
|
||||
|
||||
this.title = title;
|
||||
this.warning = warning;
|
||||
this.solution = solution;
|
||||
this.check = check;
|
||||
|
||||
// Set default properties
|
||||
this._checkState = CheckState.none;
|
||||
this.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current check state.
|
||||
* @return {CheckState} The check state.
|
||||
*/
|
||||
checkState() {
|
||||
return this._checkState;
|
||||
}
|
||||
|
||||
async run() {
|
||||
// Get check as synchronous or asynchronous function
|
||||
const check = this.check instanceof Promise ? await this.check : this.check;
|
||||
|
||||
// Run check
|
||||
try {
|
||||
check();
|
||||
this._checkState = CheckState.success;
|
||||
} catch (e) {
|
||||
this.stateFailError = e;
|
||||
this._checkState = CheckState.fail;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the constructor parameters.
|
||||
* @param {Object} params The parameters to validate.
|
||||
*/
|
||||
_validateParams(params) {
|
||||
Utils.validateParams(params, {
|
||||
group: { t: 'string', v: isString },
|
||||
title: { t: 'string', v: isString },
|
||||
warning: { t: 'string', v: isString },
|
||||
solution: { t: 'string', v: isString },
|
||||
check: { t: 'function', v: isFunction },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The check state.
|
||||
*/
|
||||
const CheckState = Object.freeze({
|
||||
none: "none",
|
||||
fail: "fail",
|
||||
success: "success",
|
||||
});
|
||||
|
||||
export default Check;
|
||||
module.exports = {
|
||||
Check,
|
||||
CheckState,
|
||||
};
|
||||
45
src/Security/CheckGroup.js
Normal file
45
src/Security/CheckGroup.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @module SecurityCheck
|
||||
*/
|
||||
|
||||
/**
|
||||
* A group of security checks.
|
||||
* @interface CheckGroup
|
||||
*/
|
||||
class CheckGroup {
|
||||
constructor() {
|
||||
this._name = this.setName();
|
||||
this._checks = this.setChecks();
|
||||
}
|
||||
|
||||
/**
|
||||
* The security check group name; to be overridden by child class.
|
||||
*/
|
||||
setName() {
|
||||
throw `Check group has no name.`;
|
||||
}
|
||||
name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The security checks; to be overridden by child class.
|
||||
*/
|
||||
setChecks() {
|
||||
throw `Check group has no checks.`;
|
||||
}
|
||||
checks() {
|
||||
return this._checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all checks.
|
||||
*/
|
||||
async run() {
|
||||
for (const check of this._checks) {
|
||||
check.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CheckGroup;
|
||||
47
src/Security/CheckGroups/CheckGroupDatabase.js
Normal file
47
src/Security/CheckGroups/CheckGroupDatabase.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @module SecurityCheck
|
||||
*/
|
||||
|
||||
import { Check } from '../Check';
|
||||
import CheckGroup from '../CheckGroup';
|
||||
import Config from '../../Config';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
/**
|
||||
* The security checks group for Parse Server configuration.
|
||||
* Checks common Parse Server parameters such as access keys.
|
||||
*/
|
||||
class CheckGroupDatabase extends CheckGroup {
|
||||
setName() {
|
||||
return 'Database';
|
||||
}
|
||||
setChecks() {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const databaseAdapter = config.database.adapter;
|
||||
const databaseUrl = databaseAdapter._uri;
|
||||
return [
|
||||
new Check({
|
||||
title: 'Secure database password',
|
||||
warning: 'The database password is insecure and vulnerable to brute force attacks.',
|
||||
solution: 'Choose a longer and/or more complex password with a combination of upper- and lowercase characters, numbers and special characters.',
|
||||
check: () => {
|
||||
const password = databaseUrl.match(/\/\/\S+:(\S+)@/)[1];
|
||||
const hasUpperCase = /[A-Z]/.test(password);
|
||||
const hasLowerCase = /[a-z]/.test(password);
|
||||
const hasNumbers = /\d/.test(password);
|
||||
const hasNonAlphasNumerics = /\W/.test(password);
|
||||
// Ensure length
|
||||
if (password.length < 14) {
|
||||
throw 1;
|
||||
}
|
||||
// Ensure at least 3 out of 4 requirements passed
|
||||
if (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphasNumerics < 3) {
|
||||
throw 1;
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CheckGroupDatabase;
|
||||
65
src/Security/CheckGroups/CheckGroupServerConfig.js
Normal file
65
src/Security/CheckGroups/CheckGroupServerConfig.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @module SecurityCheck
|
||||
*/
|
||||
|
||||
import { Check } from '../Check';
|
||||
import CheckGroup from '../CheckGroup';
|
||||
import Config from '../../Config';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
/**
|
||||
* The security checks group for Parse Server configuration.
|
||||
* Checks common Parse Server parameters such as access keys.
|
||||
*/
|
||||
class CheckGroupServerConfig extends CheckGroup {
|
||||
setName() {
|
||||
return 'Parse Server Configuration';
|
||||
}
|
||||
setChecks() {
|
||||
const config = Config.get(Parse.applicationId);
|
||||
return [
|
||||
new Check({
|
||||
title: 'Secure master key',
|
||||
warning: 'The Parse Server master key is insecure and vulnerable to brute force attacks.',
|
||||
solution: 'Choose a longer and/or more complex master key with a combination of upper- and lowercase characters, numbers and special characters.',
|
||||
check: () => {
|
||||
const masterKey = config.masterKey;
|
||||
const hasUpperCase = /[A-Z]/.test(masterKey);
|
||||
const hasLowerCase = /[a-z]/.test(masterKey);
|
||||
const hasNumbers = /\d/.test(masterKey);
|
||||
const hasNonAlphasNumerics = /\W/.test(masterKey);
|
||||
// Ensure length
|
||||
if (masterKey.length < 14) {
|
||||
throw 1;
|
||||
}
|
||||
// Ensure at least 3 out of 4 requirements passed
|
||||
if (hasUpperCase + hasLowerCase + hasNumbers + hasNonAlphasNumerics < 3) {
|
||||
throw 1;
|
||||
}
|
||||
},
|
||||
}),
|
||||
new Check({
|
||||
title: 'Security log disabled',
|
||||
warning: 'Security checks in logs may expose vulnerabilities to anyone access to logs.',
|
||||
solution: 'Change Parse Server configuration to \'security.enableCheckLog: false\'.',
|
||||
check: () => {
|
||||
if (config.security && config.security.enableCheckLog) {
|
||||
throw 1;
|
||||
}
|
||||
},
|
||||
}),
|
||||
new Check({
|
||||
title: 'Client class creation disabled',
|
||||
warning: 'Attackers are allowed to create new classes without restriction and flood the database.',
|
||||
solution: 'Change Parse Server configuration to \'allowClientClassCreation: false\'.',
|
||||
check: () => {
|
||||
if (config.allowClientClassCreation || config.allowClientClassCreation == null) {
|
||||
throw 1;
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CheckGroupServerConfig;
|
||||
9
src/Security/CheckGroups/CheckGroups.js
Normal file
9
src/Security/CheckGroups/CheckGroups.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @module SecurityCheck
|
||||
*/
|
||||
|
||||
/**
|
||||
* The list of security check groups.
|
||||
*/
|
||||
export { default as CheckGroupDatabase } from './CheckGroupDatabase';
|
||||
export { default as CheckGroupServerConfig } from './CheckGroupServerConfig';
|
||||
205
src/Security/CheckRunner.js
Normal file
205
src/Security/CheckRunner.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @module SecurityCheck
|
||||
*/
|
||||
|
||||
import Utils from '../Utils';
|
||||
import { CheckState } from './Check';
|
||||
import * as CheckGroups from './CheckGroups/CheckGroups';
|
||||
import logger from '../logger';
|
||||
import { isArray, isBoolean } from 'lodash';
|
||||
|
||||
/**
|
||||
* The security check runner.
|
||||
*/
|
||||
class CheckRunner {
|
||||
/**
|
||||
* The security check runner.
|
||||
* @param {Object} [config] The configuration options.
|
||||
* @param {Boolean} [config.enableCheck=false] Is true if Parse Server should report weak security settings.
|
||||
* @param {Boolean} [config.enableCheckLog=false] Is true if the security check report should be written to logs.
|
||||
* @param {Object} [config.checkGroups] The check groups to run. Default are the groups defined in `./CheckGroups/CheckGroups.js`.
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
this._validateParams(config);
|
||||
const { enableCheck = false, enableCheckLog = false, checkGroups = CheckGroups } = config;
|
||||
this.enableCheck = enableCheck;
|
||||
this.enableCheckLog = enableCheckLog;
|
||||
this.checkGroups = checkGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all security checks and returns the results.
|
||||
* @params
|
||||
* @returns {Object} The security check report.
|
||||
*/
|
||||
async run({ version = '1.0.0' } = {}) {
|
||||
// Instantiate check groups
|
||||
const groups = Object.values(this.checkGroups)
|
||||
.filter(c => typeof c === 'function')
|
||||
.map(CheckGroup => new CheckGroup());
|
||||
|
||||
// Run checks
|
||||
groups.forEach(group => group.run());
|
||||
|
||||
// Generate JSON report
|
||||
const report = this._generateReport({ groups, version });
|
||||
|
||||
// If report should be written to logs
|
||||
if (this.enableCheckLog) {
|
||||
this._logReport(report)
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a security check report in JSON format with schema:
|
||||
* ```
|
||||
* {
|
||||
* report: {
|
||||
* version: "1.0.0", // The report version, defines the schema
|
||||
* state: "fail" // The disjunctive indicator of failed checks in all groups.
|
||||
* groups: [ // The check groups
|
||||
* {
|
||||
* name: "House", // The group name
|
||||
* state: "fail" // The disjunctive indicator of failed checks in this group.
|
||||
* checks: [ // The checks
|
||||
* title: "Door locked", // The check title
|
||||
* state: "fail" // The check state
|
||||
* warning: "Anyone can enter your house." // The warning.
|
||||
* solution: "Lock your door." // The solution.
|
||||
* ]
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
* @param {Object} params The parameters.
|
||||
* @param {Array<CheckGroup>} params.groups The check groups.
|
||||
* @param {String} params.version: The report schema version.
|
||||
* @returns {Object} The report.
|
||||
*/
|
||||
_generateReport({ groups, version }) {
|
||||
// Create report template
|
||||
const report = {
|
||||
report: {
|
||||
version,
|
||||
state: CheckState.success,
|
||||
groups: []
|
||||
}
|
||||
};
|
||||
|
||||
// Identify report version
|
||||
switch (version) {
|
||||
case '1.0.0':
|
||||
default:
|
||||
// For each check group
|
||||
for (const group of groups) {
|
||||
|
||||
// Create group report
|
||||
const groupReport = {
|
||||
name: group.name(),
|
||||
state: CheckState.success,
|
||||
checks: [],
|
||||
}
|
||||
|
||||
// Create check reports
|
||||
groupReport.checks = group.checks().map(check => {
|
||||
const checkReport = {
|
||||
title: check.title,
|
||||
state: check.checkState(),
|
||||
};
|
||||
if (check.checkState() == CheckState.fail) {
|
||||
checkReport.warning = check.warning;
|
||||
checkReport.solution = check.solution;
|
||||
report.report.state = CheckState.fail;
|
||||
groupReport.state = CheckState.fail;
|
||||
}
|
||||
return checkReport;
|
||||
});
|
||||
|
||||
report.report.groups.push(groupReport);
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the security check report.
|
||||
* @param {Object} report The report to log.
|
||||
*/
|
||||
_logReport(report) {
|
||||
|
||||
// Determine log level depending on whether any check failed
|
||||
const log = report.report.state == CheckState.success ? (s) => logger.info(s) : (s) => logger.warn(s);
|
||||
|
||||
// Declare output
|
||||
const indent = ' ';
|
||||
let output = '';
|
||||
let checksCount = 0;
|
||||
let failedChecksCount = 0;
|
||||
let skippedCheckCount = 0;
|
||||
|
||||
// Traverse all groups and checks for compose output
|
||||
for (const group of report.report.groups) {
|
||||
output += `\n- ${group.name}`
|
||||
|
||||
for (const check of group.checks) {
|
||||
checksCount++;
|
||||
output += `\n${indent}${this._getLogIconForState(check.state)} ${check.title}`;
|
||||
|
||||
if (check.state == CheckState.fail) {
|
||||
failedChecksCount++;
|
||||
output += `\n${indent}${indent}Warning: ${check.warning}`;
|
||||
output += ` ${check.solution}`;
|
||||
} else if (check.state == CheckState.none) {
|
||||
skippedCheckCount++;
|
||||
output += `\n${indent}${indent}Test did not execute, this is likely an internal server issue, please report.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output =
|
||||
`\n###################################` +
|
||||
`\n# #` +
|
||||
`\n# Parse Server Security Check #` +
|
||||
`\n# #` +
|
||||
`\n###################################` +
|
||||
`\n` +
|
||||
`\n${failedChecksCount > 0 ? 'Warning: ' : ''}${failedChecksCount} weak security setting(s) found${failedChecksCount > 0 ? '!' : ''}` +
|
||||
`\n${checksCount} check(s) executed` +
|
||||
`\n${skippedCheckCount} check(s) skipped` +
|
||||
`\n` +
|
||||
`${output}`;
|
||||
|
||||
// Write log
|
||||
log(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an icon for use in the report log output.
|
||||
* @param {CheckState} state The check state.
|
||||
* @returns {String} The icon.
|
||||
*/
|
||||
_getLogIconForState(state) {
|
||||
switch (state) {
|
||||
case CheckState.success: return '✅';
|
||||
case CheckState.fail: return '❌';
|
||||
default: return 'ℹ️';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the constructor parameters.
|
||||
* @param {Object} params The parameters to validate.
|
||||
*/
|
||||
_validateParams(params) {
|
||||
Utils.validateParams(params, {
|
||||
enableCheck: { t: 'boolean', v: isBoolean, o: true },
|
||||
enableCheckLog: { t: 'boolean', v: isBoolean, o: true },
|
||||
checkGroups: { t: 'array', v: isArray, o: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CheckRunner;
|
||||
65
src/Utils.js
65
src/Utils.js
@@ -118,6 +118,71 @@ class Utils {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether an object is a Promise.
|
||||
* @param {any} object The object to validate.
|
||||
* @returns {Boolean} Returns true if the object is a promise.
|
||||
*/
|
||||
static isPromise(object) {
|
||||
return object instanceof Promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an object with all permutations of the original keys.
|
||||
* @param {Object} object The object to permutate.
|
||||
* @param {Integer} [index=0] The current key index.
|
||||
* @param {Object} [current={}] The current result entry being composed.
|
||||
* @param {Array} [results=[]] The resulting array of permutations.
|
||||
*/
|
||||
static getObjectKeyPermutations(object, index = 0, current = {}, results = []) {
|
||||
const keys = Object.keys(object);
|
||||
const key = keys[index];
|
||||
const values = object[key];
|
||||
|
||||
for (const value of values) {
|
||||
current[key] = value;
|
||||
const nextIndex = index + 1;
|
||||
|
||||
if (nextIndex < keys.length) {
|
||||
this.getObjectKeyPermutations(object, nextIndex, current, results);
|
||||
} else {
|
||||
const result = Object.assign({}, current);
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates parameters and throws if a parameter is invalid.
|
||||
* Example parameter types syntax:
|
||||
* ```
|
||||
* {
|
||||
* parameterName: {
|
||||
* t: 'boolean',
|
||||
* v: isBoolean,
|
||||
* o: true
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* @param {Object} params The parameters to validate.
|
||||
* @param {Array<Object>} types The parameter types used for validation.
|
||||
* @param {Object} types.t The parameter type; used for error message, not for validation.
|
||||
* @param {Object} types.v The function to validate the parameter value.
|
||||
* @param {Boolean} [types.o=false] Is true if the parameter is optional.
|
||||
*/
|
||||
static validateParams(params, types) {
|
||||
for (const key of Object.keys(params)) {
|
||||
const type = types[key];
|
||||
const isOptional = !!type.o;
|
||||
const param = params[key];
|
||||
if (!(isOptional && param == null) && (!type.v(param))) {
|
||||
throw `Invalid parameter ${key} must be of type ${type.t} but is ${typeof param}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Utils;
|
||||
|
||||
Reference in New Issue
Block a user