* 6641: Implement support for user impersonation: master key clients can log in as any user, without access to the user's credentials, and without presuming the user already has a session * reworded changelog * rebuilt package lock * fit test * using lodash flatMap * bump to node 12 for postgres test * revert test fit * add node version to postgres CI * revert package-lock Co-authored-by: gormanfletcher <git@gormanfletcher.com> Co-authored-by: Manuel <5673677+mtrezza@users.noreply.github.com>
342 lines
11 KiB
JavaScript
342 lines
11 KiB
JavaScript
'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);
|
|
}
|
|
});
|
|
});
|
|
});
|