Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Prevent Parse Server start in case of unknown option in server configuration #8987

Merged
merged 33 commits into from Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b3924bd
feat: Log warning in case of a unknown options
vivekjoshi556 Mar 4, 2024
70e7a02
Merge branch 'parse-community:alpha' into issue_8938
vivekjoshi556 Mar 5, 2024
99a03d2
fix: splitting test cases | ignored internal configs | covered all co…
vivekjoshi556 Mar 14, 2024
a9e7875
fix: resolve conflicts
vivekjoshi556 Mar 14, 2024
946d4b4
Merge branch 'alpha' into issue_8938
mtrezza Mar 14, 2024
b70592e
doc: included documentation for using this feature
vivekjoshi556 Mar 16, 2024
e791e40
Merge branch 'issue_8938' of https://github.com/vivekjoshi556/parse-s…
vivekjoshi556 Mar 16, 2024
d4ab4f0
Apply suggestions from code review
vivekjoshi556 Mar 16, 2024
e9311b3
fix: CacheAdapter does not connect when using a CacheAdapter with a J…
dblythy Mar 15, 2024
8aaa172
chore(release): 7.0.0-alpha.27 [skip ci]
semantic-release-bot Mar 15, 2024
8ef8f54
test: Fix flaky tests for comment in MongoDB query (#9015)
Meglali20 Mar 15, 2024
80fb070
refactor: Security upgrade follow-redirects from 1.15.5 to 1.15.6 (#9…
parseplatformorg Mar 16, 2024
79f41fc
Update src/ParseServer.js
vivekjoshi556 Mar 16, 2024
93022fe
fix: error message on server crash
vivekjoshi556 Mar 16, 2024
b754139
Merge branch 'alpha' into issue_8938
mtrezza Mar 16, 2024
7819bdb
refactor: removing duplication
vivekjoshi556 Mar 18, 2024
744fb9e
Merge branch 'alpha' into issue_8938
mtrezza Mar 21, 2024
05ce806
refactor: removing duplication
vivekjoshi556 Mar 18, 2024
49fdbf4
Merge branch 'issue_8938' of https://github.com/vivekjoshi556/parse-s…
vivekjoshi556 Mar 21, 2024
c5c2b5d
fix: import error
vivekjoshi556 Mar 21, 2024
53f1d93
nits
mtrezza Mar 24, 2024
e333d73
suggestion
mtrezza Mar 24, 2024
2297f47
Update ParseServer.js
mtrezza Mar 24, 2024
3ce2ae6
Merge branch 'alpha' into issue_8938
mtrezza Apr 5, 2024
753bbd8
refactor
mtrezza Apr 5, 2024
2c058d8
remove centralized logic suggestion
mtrezza Apr 5, 2024
024c154
refactor
mtrezza Apr 5, 2024
7699453
Merge branch 'alpha' into issue_8938
mtrezza Apr 6, 2024
b25cfef
refactor: v2
vivekjoshi556 Apr 6, 2024
adfed9d
Merge branch 'issue_8938' of https://github.com/vivekjoshi556/parse-s…
vivekjoshi556 Apr 6, 2024
5756cca
docs: reverting contributing guidelines
vivekjoshi556 Apr 6, 2024
f9ed251
Merge branch 'alpha' into issue_8938
mtrezza Apr 6, 2024
7ac43a5
removing empty newlines
mtrezza Apr 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions resources/buildConfigDefinitions.js
Expand Up @@ -254,6 +254,23 @@ function inject(t, list) {
if (action) {
props.push(t.objectProperty(t.stringLiteral('action'), action));
}

if (t.isGenericTypeAnnotation(elt)) {
if (elt.typeAnnotation.id.name in nestedOptionEnvPrefix) {
props.push(
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elt.typeAnnotation.id.name))
);
}
} else if (t.isArrayTypeAnnotation(elt)) {
const elementType = elt.typeAnnotation.elementType;
if (t.isGenericTypeAnnotation(elementType)) {
if (elementType.id.name in nestedOptionEnvPrefix) {
props.push(
t.objectProperty(t.stringLiteral('type'), t.stringLiteral(elementType.id.name + '[]'))
);
}
}
}
if (elt.defaultValue) {
let parsedValue = parseDefaultValue(elt, elt.defaultValue, t);
if (!parsedValue) {
Expand Down
52 changes: 52 additions & 0 deletions spec/ParseConfigKey.spec.js
@@ -0,0 +1,52 @@
const Config = require('../lib/Config');
const ParseServer = require('../lib/index').ParseServer;

describe('Config Keys', () => {
const tests = [
{
name: 'Invalid Root Keys',
options: { unknow: 'val', masterKeyIPs: '' },
error: 'unknow, masterKeyIPs',
},
{ name: 'Invalid Schema Keys', options: { schema: { Strict: 'val' } }, error: 'schema.Strict' },
{
name: 'Invalid Pages Keys',
options: { pages: { customUrls: { EmailVerificationSendFail: 'val' } } },
error: 'pages.customUrls.EmailVerificationSendFail',
},
{
name: 'Invalid LiveQueryServerOptions Keys',
options: { liveQueryServerOptions: { MasterKey: 'value' } },
error: 'liveQueryServerOptions.MasterKey',
},
{
name: 'Invalid RateLimit Keys - Array Item',
options: { rateLimit: [{ RequestPath: '' }, { RequestTimeWindow: '' }] },
error: 'rateLimit[0].RequestPath, rateLimit[1].RequestTimeWindow',
},
];

tests.forEach(test => {
it(test.name, async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callThrough();
spyOn(Config, 'validateOptions').and.callFake(() => {});

new ParseServer({
...defaultConfiguration,
...test.options,
});
expect(logger.error).toHaveBeenCalledWith(`Invalid Option Keys Found: ${test.error}`);
});
});

it('should run fine', async () => {
try {
await reconfigureServer({
...defaultConfiguration,
});
} catch (err) {
fail('Should run without error');
}
});
});
11 changes: 11 additions & 0 deletions src/Config.js
Expand Up @@ -64,6 +64,7 @@
}

static validateOptions({
customPages,
publicServerURL,
revokeSessionOnPasswordReset,
expireInactiveSessions,
Expand Down Expand Up @@ -133,9 +134,18 @@
this.validateRateLimit(rateLimit);
this.validateLogLevels(logLevels);
this.validateDatabaseOptions(databaseOptions);
this.validateCustomPages(customPages);
this.validateAllowClientClassCreation(allowClientClassCreation);
}

static validateCustomPages(customPages) {
if (!customPages) return;

if (Object.prototype.toString.call(customPages) !== '[object Object]') {
throw Error('Parse Server option customPages must be an object.');

Check warning on line 145 in src/Config.js

View check run for this annotation

Codecov / codecov/patch

src/Config.js#L145

Added line #L145 was not covered by tests
}
}

static validateControllers({
verifyUserEmails,
userController,
Expand Down Expand Up @@ -569,6 +579,7 @@
if (Object.prototype.toString.call(databaseOptions) !== '[object Object]') {
throw `databaseOptions must be an object`;
}

if (databaseOptions.enableSchemaHooks === undefined) {
databaseOptions.enableSchemaHooks = DatabaseOptions.enableSchemaHooks.default;
} else if (typeof databaseOptions.enableSchemaHooks !== 'boolean') {
Expand Down
4 changes: 1 addition & 3 deletions src/Deprecator/Deprecations.js
Expand Up @@ -15,6 +15,4 @@
*
* If there are no deprecations, this must return an empty array.
*/
module.exports = [
{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' },
];
module.exports = [{ optionKey: 'encodeParseObjectInCloudFunction', changeNewDefault: 'true' }];
15 changes: 15 additions & 0 deletions src/Options/Definitions.js
Expand Up @@ -54,6 +54,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
help: 'The account lockout policy for failed login attempts.',
action: parsers.objectParser,
type: 'AccountLockoutOptions',
},
allowClientClassCreation: {
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
Expand Down Expand Up @@ -157,6 +158,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_CUSTOM_PAGES',
help: 'custom pages for password validation and reset',
action: parsers.objectParser,
type: 'CustomPagesOptions',
default: {},
},
databaseAdapter: {
Expand All @@ -169,6 +171,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_DATABASE_OPTIONS',
help: 'Options to pass to the database client',
action: parsers.objectParser,
type: 'DatabaseOptions',
},
databaseURI: {
env: 'PARSE_SERVER_DATABASE_URI',
Expand Down Expand Up @@ -273,6 +276,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS',
help: 'Options for file uploads',
action: parsers.objectParser,
type: 'FileUploadOptions',
default: {},
},
graphQLPath: {
Expand All @@ -294,6 +298,7 @@ module.exports.ParseServerOptions = {
help:
'Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.',
action: parsers.objectParser,
type: 'IdempotencyOptions',
default: {},
},
javascriptKey: {
Expand All @@ -309,11 +314,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LIVE_QUERY',
help: "parse-server's LiveQuery configuration object",
action: parsers.objectParser,
type: 'LiveQueryOptions',
},
liveQueryServerOptions: {
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
help: 'Live query server configuration options (will start the liveQuery server)',
action: parsers.objectParser,
type: 'LiveQueryServerOptions',
},
loggerAdapter: {
env: 'PARSE_SERVER_LOGGER_ADAPTER',
Expand All @@ -328,6 +335,7 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_LOG_LEVELS',
help: '(Optional) Overrides the log levels used internally by Parse Server to log events.',
action: parsers.objectParser,
type: 'LogLevels',
default: {},
},
logsFolder: {
Expand Down Expand Up @@ -408,12 +416,14 @@ module.exports.ParseServerOptions = {
help:
'The options for pages such as password reset and email verification. Caution, this is an experimental feature that may not be appropriate for production.',
action: parsers.objectParser,
type: 'PagesOptions',
default: {},
},
passwordPolicy: {
env: 'PARSE_SERVER_PASSWORD_POLICY',
help: 'The password policy for enforcing password related rules.',
action: parsers.objectParser,
type: 'PasswordPolicyOptions',
},
playgroundPath: {
env: 'PARSE_SERVER_PLAYGROUND_PATH',
Expand Down Expand Up @@ -471,6 +481,7 @@ module.exports.ParseServerOptions = {
help:
"Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.<br><br>\u2139\uFE0F Mind the following limitations:<br>- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses<br>- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable<br>- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case.",
action: parsers.arrayParser,
type: 'RateLimitOptions[]',
default: [],
},
readOnlyMasterKey: {
Expand Down Expand Up @@ -516,11 +527,13 @@ module.exports.ParseServerOptions = {
env: 'PARSE_SERVER_SCHEMA',
help: 'Defined schema',
action: parsers.objectParser,
type: 'SchemaOptions',
},
security: {
env: 'PARSE_SERVER_SECURITY',
help: 'The security options to identify and report weak security settings.',
action: parsers.objectParser,
type: 'SecurityOptions',
default: {},
},
sendUserEmailVerification: {
Expand Down Expand Up @@ -665,12 +678,14 @@ module.exports.PagesOptions = {
env: 'PARSE_SERVER_PAGES_CUSTOM_ROUTES',
help: 'The custom routes.',
action: parsers.arrayParser,
type: 'PagesRoute[]',
default: [],
},
customUrls: {
env: 'PARSE_SERVER_PAGES_CUSTOM_URLS',
help: 'The URLs to the custom pages.',
action: parsers.objectParser,
type: 'PagesCustomUrlsOptions',
default: {},
},
enableLocalization: {
Expand Down
55 changes: 54 additions & 1 deletion src/ParseServer.js
Expand Up @@ -45,6 +45,7 @@ import { SecurityRouter } from './Routers/SecurityRouter';
import CheckRunner from './Security/CheckRunner';
import Deprecator from './Deprecator/Deprecator';
import { DefinedSchemas } from './SchemaMigrations/DefinedSchemas';
import OptionsDefinitions from './Options/Definitions';

// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
Expand All @@ -59,6 +60,58 @@ class ParseServer {
constructor(options: ParseServerOptions) {
// Scan for deprecated Parse Server options
Deprecator.scanParseServerOptions(options);

const interfaces = JSON.parse(JSON.stringify(OptionsDefinitions));

function getValidObject(root) {
const result = {};
for (const key in root) {
if (Object.prototype.hasOwnProperty.call(root[key], 'type')) {
if (root[key].type.endsWith('[]')) {
result[key] = [getValidObject(interfaces[root[key].type.slice(0, -2)])];
} else {
result[key] = getValidObject(interfaces[root[key].type]);
}
} else {
result[key] = '';
}
}
return result;
}

const optionsBlueprint = getValidObject(interfaces['ParseServerOptions']);

function validateKeyNames(original, ref, name = '') {
let result = [];
const prefix = name + (name !== '' ? '.' : '');
for (const key in original) {
if (!Object.prototype.hasOwnProperty.call(ref, key)) {
result.push(prefix + key);
} else {
if (ref[key] === '') continue;
let res = [];
if (Array.isArray(original[key]) && Array.isArray(ref[key])) {
const type = ref[key][0];
original[key].forEach((item, idx) => {
if (typeof item === 'object' && item !== null) {
res = res.concat(validateKeyNames(item, type, prefix + key + `[${idx}]`));
}
});
} else if (typeof original[key] === 'object' && typeof ref[key] === 'object') {
res = validateKeyNames(original[key], ref[key], prefix + key);
}
result = result.concat(res);
}
}
return result;
}

const diff = validateKeyNames(options, optionsBlueprint);
if (diff.length > 0) {
const logger = logging.logger;
logger.error(`Invalid Option Keys Found: ${diff.join(', ')}`);
}

// Set option defaults
injectDefaults(options);
const {
Expand All @@ -70,9 +123,9 @@ class ParseServer {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;

Config.validateOptions(options);
const allControllers = controllers.getControllers(options);

options.state = 'initialized';
this.config = Config.put(Object.assign({}, options, allControllers));
this.config.masterKeyIpsStore = new Map();
Expand Down