From bf3db15bf924cfbf271c249b6beff3a63237e140 Mon Sep 17 00:00:00 2001 From: cnorris Date: Tue, 25 Jul 2017 12:49:32 -0700 Subject: [PATCH 1/2] Add maxLimit server configuration --- README.md | 1 + spec/ParseQuery.spec.js | 32 +++++++++++++++++++++++++++++ spec/index.spec.js | 8 ++++++++ src/Config.js | 12 +++++++++++ src/ParseServer.js | 3 +++ src/Routers/ClassesRouter.js | 9 ++++++-- src/cli/definitions/parse-server.js | 5 +++++ 7 files changed, 68 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 93c4086dd7..2f88f34d03 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `loggerAdapter` - The default behavior/transport (File) can be changed by creating an adapter class (see [`LoggerAdapter.js`](https://github.com/parse-community/parse-server/blob/master/src/Adapters/Logger/LoggerAdapter.js)). * `logLevel` - Set the specific level you want to log. Defaults to `info`. The default logger uses the npm log levels as defined by the underlying winston logger. Check [Winston logging levels](https://github.com/winstonjs/winston#logging-levels) for details on values to specify. * `sessionLength` - The length of time in seconds that a session should be valid for. Defaults to 31536000 seconds (1 year). +* `maxLimit` - The maximum value supported for the limit option on queries. Defaults to unlimited. * `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. * `accountLockout` - Lock account when a malicious user is attempting to determine an account password by trial and error. * `passwordPolicy` - Optional password policy rules to enforce. diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index b7b6621e76..9ce98acb0d 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -239,6 +239,38 @@ describe('Parse.Query testing', () => { }); }); + it("query with limit equal to maxlimit", function(done) { + var baz = new TestObject({ foo: 'baz' }); + var qux = new TestObject({ foo: 'qux' }); + reconfigureServer({ maxLimit: 1 }) + Parse.Object.saveAll([baz, qux], function() { + var query = new Parse.Query(TestObject); + query.limit(1); + query.find({ + success: function(results) { + equal(results.length, 1); + done(); + } + }); + }); + }); + + it("query with limit exceeding maxlimit", function(done) { + var baz = new TestObject({ foo: 'baz' }); + var qux = new TestObject({ foo: 'qux' }); + reconfigureServer({ maxLimit: 1 }) + Parse.Object.saveAll([baz, qux], function() { + var query = new Parse.Query(TestObject); + query.limit(2); + query.find({ + success: function(results) { + equal(results.length, 1); + done(); + } + }); + }); + }); + it("containedIn object array queries", function(done) { var messageList = []; for (var i = 0; i < 4; ++i) { diff --git a/spec/index.spec.js b/spec/index.spec.js index 46e57e26d6..5b6d3aa0c8 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -415,6 +415,14 @@ describe('server', () => { .then(done); }) + it('fails if maxLimit is negative', (done) => { + reconfigureServer({ maxLimit: -100 }) + .catch(error => { + expect(error).toEqual('Max limit must be a value greater than 0.'); + done(); + }); + }); + it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }) .catch(done); diff --git a/src/Config.js b/src/Config.js index 2678b6a000..c347c659a1 100644 --- a/src/Config.js +++ b/src/Config.js @@ -71,6 +71,7 @@ export class Config { this.mount = removeTrailingSlash(mount); this.liveQueryController = cacheInfo.liveQueryController; this.sessionLength = cacheInfo.sessionLength; + this.maxLimit = cacheInfo.maxLimit; this.expireInactiveSessions = cacheInfo.expireInactiveSessions; this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); this.generateEmailVerifyTokenExpiresAt = this.generateEmailVerifyTokenExpiresAt.bind(this); @@ -86,6 +87,7 @@ export class Config { revokeSessionOnPasswordReset, expireInactiveSessions, sessionLength, + maxLimit, emailVerifyTokenValidityDuration, accountLockout, passwordPolicy, @@ -113,6 +115,8 @@ export class Config { this.validateSessionConfiguration(sessionLength, expireInactiveSessions); this.validateMasterKeyIps(masterKeyIps); + + this.validateMaxLimit(maxLimit); } static validateAccountLockoutPolicy(accountLockout) { @@ -220,6 +224,14 @@ export class Config { } } + static validateMaxLimit(maxLimit) { + if (maxLimit) { + if (maxLimit <= 0) { + throw 'Max limit must be a value greater than 0.' + } + } + } + generateEmailVerifyTokenExpiresAt() { if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { return undefined; diff --git a/src/ParseServer.js b/src/ParseServer.js index 252c3cbde7..ab9cfd9fad 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -86,6 +86,7 @@ addParseCloud(); // "javascriptKey": optional key from Parse dashboard // "push": optional key from configure push // "sessionLength": optional length in seconds for how long Sessions should be valid for +// "maxLimit": optional upper bound for what can be specified for the 'limit' parameter on queries class ParseServer { @@ -138,6 +139,7 @@ class ParseServer { }, liveQuery = {}, sessionLength = defaults.sessionLength, // 1 Year in seconds + maxLimit, expireInactiveSessions = defaults.expireInactiveSessions, revokeSessionOnPasswordReset = defaults.revokeSessionOnPasswordReset, schemaCacheTTL = defaults.schemaCacheTTL, // cache for 5s @@ -263,6 +265,7 @@ class ParseServer { maxUploadSize: maxUploadSize, liveQueryController: liveQueryController, sessionLength: Number(sessionLength), + maxLimit: Number(maxLimit), expireInactiveSessions: expireInactiveSessions, jsonLogs, revokeSessionOnPasswordReset, diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index 6cfb8eb6c8..9f97ad6607 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -13,7 +13,6 @@ export class ClassesRouter extends PromiseRouter { const options = {}; const allowConstraints = ['skip', 'limit', 'order', 'count', 'keys', 'include', 'redirectClassNameForKey', 'where']; - for (const key of Object.keys(body)) { if (allowConstraints.indexOf(key) === -1) { throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid parameter for query: ${key}`); @@ -23,8 +22,14 @@ export class ClassesRouter extends PromiseRouter { if (body.skip) { options.skip = Number(body.skip); } + if (body.limit || body.limit === 0) { - options.limit = Number(body.limit); + if (req.config.maxLimit && (body.limit > req.config.maxLimit)) { + // Silently replace the limit on the query with the max configured + options.limit = Number(req.config.maxLimit); + } else { + options.limit = Number(body.limit); + } } else { options.limit = Number(100); } diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index 63762239a8..689bb93d6a 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -194,6 +194,11 @@ export default { help: "Session duration, defaults to 1 year", action: numberParser("sessionLength") }, + "maxLimit": { + env: "PARSE_SERVER_MAX_LIMIT", + help: "Max value for limit option on queries, defaults to unlimited", + action: numberParser("maxLimit") + }, "verbose": { env: "VERBOSE", help: "Set the logging to verbose" From efa9e7b2405e5efb398ab6e5b13b50e25c47a06c Mon Sep 17 00:00:00 2001 From: cnorris Date: Wed, 26 Jul 2017 12:12:27 -0700 Subject: [PATCH 2/2] Fix maxlimit validation logic to correctly handle maxLimit:0 case --- src/Config.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Config.js b/src/Config.js index c347c659a1..675b24ac57 100644 --- a/src/Config.js +++ b/src/Config.js @@ -225,10 +225,8 @@ export class Config { } static validateMaxLimit(maxLimit) { - if (maxLimit) { - if (maxLimit <= 0) { - throw 'Max limit must be a value greater than 0.' - } + if (maxLimit <= 0) { + throw 'Max limit must be a value greater than 0.' } }