From 170dd40d3c14a465d6c0bcfbbf169a9ee93aa355 Mon Sep 17 00:00:00 2001 From: Ilya Diallo Date: Mon, 22 Feb 2016 12:57:18 +0100 Subject: [PATCH 01/12] Accept subdocuments keys ("object.subobject"), to allow atomic updates of an object field. --- src/Schema.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Schema.js b/src/Schema.js index 54aa3d9e..eb8f1c5b 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -426,6 +426,12 @@ Schema.prototype.validateField = function(className, key, type, freeze) { // Just to check that the key is valid transform.transformKey(this, className, key); + if( key.indexOf(".") > 0 ) { + // subdocument key (x.y) => ok if x is of type 'object' + key = key.split(".")[ 0 ]; + type = 'object'; + } + var expected = this.data[className][key]; if (expected) { expected = (expected === 'map' ? 'object' : expected); From 831c2ee3decff98be12468ab43bfce09a34d596a Mon Sep 17 00:00:00 2001 From: Ilya Diallo Date: Mon, 22 Feb 2016 16:45:41 +0100 Subject: [PATCH 02/12] Update the tests --- spec/RestCreate.spec.js | 34 +++++++++++++++++++++++++++++++++- spec/Schema.spec.js | 11 +++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index f4055974..1e79d2bf 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -1,4 +1,4 @@ -// These tests check the "create" functionality of the REST API. +// These tests check the "create" / "update" functionality of the REST API. var auth = require('../src/Auth'); var cache = require('../src/cache'); var Config = require('../src/Config'); @@ -41,6 +41,38 @@ describe('rest create', () => { }); }); + it('handles object and subdocument', (done) => { + var obj = { + subdoc: {foo: 'bar', wu: 'tan'}, + }; + rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => { + return database.mongoFind('MyClass', {}, {}); + }).then((results) => { + expect(results.length).toEqual(1); + var mob = results[0]; + expect(typeof mob.subdoc).toBe('object'); + expect(mob.subdoc.foo).toBe('bar'); + expect(mob.subdoc.wu).toBe('tan'); + expect(typeof mob._id).toEqual('string'); + + var obj = { + 'subdoc.wu': 'clan', + }; + + rest.update(config, auth.nobody(config), 'MyClass', mob._id, obj).then(() => { + return database.mongoFind('MyClass', {}, {}); + }).then((results) => { + expect(results.length).toEqual(1); + var mob = results[0]; + expect(typeof mob.subdoc).toBe('object'); + expect(mob.subdoc.foo).toBe('bar'); + expect(mob.subdoc.wu).toBe('clan'); + done(); + }); + + }); + }); + it('handles user signup', (done) => { var user = { username: 'asdf', diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index a5c28c5b..8be0a02e 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -32,6 +32,17 @@ describe('Schema', () => { }); }); + it('can validate one object with dot notation', (done) => { + config.database.loadSchema().then((schema) => { + return schema.validateObject('TestObjectWithSubDoc', {x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue'}); + }).then((schema) => { + done(); + }, (error) => { + fail(error); + done(); + }); + }); + it('can validate two objects in a row', (done) => { config.database.loadSchema().then((schema) => { return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0}); From 7da7c43fcecdea4d40c573f5aba33fada93f4dae Mon Sep 17 00:00:00 2001 From: Ilya Diallo Date: Mon, 22 Feb 2016 22:40:01 +0100 Subject: [PATCH 03/12] #510 Detect when the port you are trying to run the server on is already in use. --- src/index.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/index.js b/src/index.js index 36f94284..344bac89 100644 --- a/src/index.js +++ b/src/index.js @@ -188,6 +188,17 @@ function ParseServer({ api.use(middlewares.handleParseErrors); + + process.on('uncaughtException', (err) => { + if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error + console.log(`Unable to listen on port ${err.port}. The port is already in use.`); + process.exit(0); + } + else { + throw err; + } + }); + return api; } From 193f368686872d850260201d0e622f4a33f2198e Mon Sep 17 00:00:00 2001 From: Ilya Diallo Date: Mon, 22 Feb 2016 22:45:00 +0100 Subject: [PATCH 04/12] #510 Detect when the port you are trying to run the server on is already in use. (reverted from commit 7da7c43fcecdea4d40c573f5aba33fada93f4dae) --- src/index.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/index.js b/src/index.js index 344bac89..36f94284 100644 --- a/src/index.js +++ b/src/index.js @@ -188,17 +188,6 @@ function ParseServer({ api.use(middlewares.handleParseErrors); - - process.on('uncaughtException', (err) => { - if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error - console.log(`Unable to listen on port ${err.port}. The port is already in use.`); - process.exit(0); - } - else { - throw err; - } - }); - return api; } From 1b83d9c6213abc0ffccbd7d45ba52233faac6807 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Fri, 26 Feb 2016 05:05:15 -0800 Subject: [PATCH 05/12] Fix for #413 - support empty authData on signup --- spec/ParseUser.spec.js | 12 +++++++++++- src/RestWrite.js | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 0e5f3a78..424e4207 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1720,7 +1720,17 @@ describe('Parse.User testing', () => { expect(e.code).toEqual(Parse.Error.SESSION_MISSING); done(); }); - }) + }); + + it('support user/password signup with empty authData block', (done) => { + // The android SDK can send an empty authData object along with username and password. + Parse.User.signUp('artof', 'thedeal', { authData: {} }).then((user) => { + done(); + }, (error) => { + fail('Signup should have succeeded.'); + done(); + }); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index d23912a7..c5551e7e 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -176,7 +176,7 @@ RestWrite.prototype.validateAuthData = function() { } } - if (!this.data.authData) { + if (!this.data.authData || !Object.keys(this.data.authData).length) { return; } From 6a17eca69c7fcdd945215ac25912cfc0f1e265cb Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Fri, 26 Feb 2016 06:00:41 -0800 Subject: [PATCH 06/12] Fix for JS SDK needing a key --- src/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index a08b4314..2a08d03e 100644 --- a/src/index.js +++ b/src/index.js @@ -79,10 +79,10 @@ function ParseServer({ databaseURI, cloud, collectionPrefix = '', - clientKey = '', - javascriptKey = randomString(20), - dotNetKey = '', - restAPIKey = '', + clientKey, + javascriptKey, + dotNetKey, + restAPIKey, fileKey = 'invalid-file-key', facebookAppIds = [], enableAnonymousUsers = true, @@ -92,7 +92,7 @@ function ParseServer({ }) { // Initialize the node client SDK automatically - Parse.initialize(appId, javascriptKey, masterKey); + Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; if (databaseAdapter) { From 97489106b4b760181874ee89d171a52f140d20dc Mon Sep 17 00:00:00 2001 From: Marco129 Date: Fri, 26 Feb 2016 22:55:39 +0800 Subject: [PATCH 07/12] Add allowClientClassCreation option --- spec/RestCreate.spec.js | 14 ++++++++++++++ spec/RestQuery.spec.js | 14 ++++++++++++++ src/Config.js | 1 + src/RestQuery.js | 21 +++++++++++++++++++++ src/RestWrite.js | 21 +++++++++++++++++++++ src/cli/cli-definitions.js | 10 ++++++++++ src/index.js | 2 ++ 7 files changed, 83 insertions(+) diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index 1e79d2bf..7ccd39d3 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -73,6 +73,20 @@ describe('rest create', () => { }); }); + it('handles create on non-existent class when disabled client class creation', (done) => { + var customConfig = Object.assign({}, config, {allowClientClassCreation: false}); + rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}) + .then(() => { + fail('Should throw an error'); + done(); + }, (err) => { + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(err.message).toEqual('This user is not allowed to access ' + + 'non-existent class: ClientClassCreation'); + done(); + }); + }); + it('handles user signup', (done) => { var user = { username: 'asdf', diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 279f45f6..59ed70f0 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -95,6 +95,20 @@ describe('rest query', () => { }).catch((error) => { console.log(error); }); }); + it('query non-existent class when disabled client class creation', (done) => { + var customConfig = Object.assign({}, config, {allowClientClassCreation: false}); + rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}) + .then(() => { + fail('Should throw an error'); + done(); + }, (err) => { + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(err.message).toEqual('This user is not allowed to access ' + + 'non-existent class: ClientClassCreation'); + done(); + }); + }); + it('query with wrongly encoded parameter', (done) => { rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'} ).then(() => { diff --git a/src/Config.js b/src/Config.js index 510ebf00..639c36b6 100644 --- a/src/Config.js +++ b/src/Config.js @@ -26,6 +26,7 @@ export class Config { this.fileKey = cacheInfo.fileKey; this.facebookAppIds = cacheInfo.facebookAppIds; this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers; + this.allowClientClassCreation = cacheInfo.allowClientClassCreation; this.database = DatabaseAdapter.getDatabaseConnection(applicationId); this.hooksController = cacheInfo.hooksController; this.filesController = cacheInfo.filesController; diff --git a/src/RestQuery.js b/src/RestQuery.js index b5bec1fb..86562e9d 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -115,6 +115,8 @@ RestQuery.prototype.execute = function() { return this.getUserAndRoleACL(); }).then(() => { return this.redirectClassNameForKey(); + }).then(() => { + return this.validateClientClassCreation(); }).then(() => { return this.replaceSelect(); }).then(() => { @@ -161,6 +163,25 @@ RestQuery.prototype.redirectClassNameForKey = function() { }); }; +// Validates this operation against the allowClientClassCreation config. +RestQuery.prototype.validateClientClassCreation = function() { + if (this.config.allowClientClassCreation === false && !this.auth.isMaster) { + return this.config.database.loadSchema().then((schema) => { + return schema.hasClass(this.className) + }).then((hasClass) => { + if (hasClass === true) { + return Promise.resolve(); + } + + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, + 'This user is not allowed to access ' + + 'non-existent class: ' + this.className); + }); + } else { + return Promise.resolve(); + } +}; + // Replaces a $inQuery clause by running the subquery, if there is an // $inQuery clause. // The $inQuery clause turns into an $in with values that are just diff --git a/src/RestWrite.js b/src/RestWrite.js index d23912a7..565bf86d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -59,6 +59,8 @@ function RestWrite(config, auth, className, query, data, originalData) { RestWrite.prototype.execute = function() { return Promise.resolve().then(() => { return this.getUserAndRoleACL(); + }).then(() => { + return this.validateClientClassCreation(); }).then(() => { return this.validateSchema(); }).then(() => { @@ -105,6 +107,25 @@ RestWrite.prototype.getUserAndRoleACL = function() { } }; +// Validates this operation against the allowClientClassCreation config. +RestWrite.prototype.validateClientClassCreation = function() { + if (this.config.allowClientClassCreation === false && !this.auth.isMaster) { + return this.config.database.loadSchema().then((schema) => { + return schema.hasClass(this.className) + }).then((hasClass) => { + if (hasClass === true) { + return Promise.resolve(); + } + + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, + 'This user is not allowed to access ' + + 'non-existent class: ' + this.className); + }); + } else { + return Promise.resolve(); + } +}; + // Validates this operation against the schema. RestWrite.prototype.validateSchema = function() { return this.config.database.validateObject(this.className, this.data, this.query); diff --git a/src/cli/cli-definitions.js b/src/cli/cli-definitions.js index 8f6e18ec..7da93c38 100644 --- a/src/cli/cli-definitions.js +++ b/src/cli/cli-definitions.js @@ -85,6 +85,16 @@ export default { return false; } }, + "allowClientClassCreation": { + env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", + help: "Enable (or disable) client class creation, defaults to true", + action: function(opt) { + if (opt == "true" || opt == "1") { + return true; + } + return false; + } + }, "mountPath": { env: "PARSE_SERVER_MOUNT_PATH", help: "Mount path for the server, defaults to /parse", diff --git a/src/index.js b/src/index.js index 2a08d03e..06ceb19a 100644 --- a/src/index.js +++ b/src/index.js @@ -86,6 +86,7 @@ function ParseServer({ fileKey = 'invalid-file-key', facebookAppIds = [], enableAnonymousUsers = true, + allowClientClassCreation = true, oauth = {}, serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb' @@ -139,6 +140,7 @@ function ParseServer({ loggerController: loggerController, hooksController: hooksController, enableAnonymousUsers: enableAnonymousUsers, + allowClientClassCreation: allowClientClassCreation, oauth: oauth, }; From ea2de87b1ab07b2ba05a0e186101d177104f1320 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 26 Feb 2016 09:42:32 -0500 Subject: [PATCH 08/12] Improves key matching algorithm --- src/middlewares.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/middlewares.js b/src/middlewares.js index e6fcc9a9..939489d3 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -99,20 +99,20 @@ function handleParseHeaders(req, res, next) { // Client keys are not required in parse-server, but if any have been configured in the server, validate them // to preserve original behavior. - var keyRequired = (req.config.clientKey - || req.config.javascriptKey - || req.config.dotNetKey - || req.config.restAPIKey); - var keyHandled = false; - if (keyRequired - && ((info.clientKey && req.config.clientKey && info.clientKey === req.config.clientKey) - || (info.javascriptKey && req.config.javascriptKey && info.javascriptKey === req.config.javascriptKey) - || (info.dotNetKey && req.config.dotNetKey && info.dotNetKey === req.config.dotNetKey) - || (info.restAPIKey && req.config.restAPIKey && info.restAPIKey === req.config.restAPIKey) - )) { - keyHandled = true; - } - if (keyRequired && !keyHandled) { + let keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"]; + + // We do it with mismatching keys to support no-keys config + var keyMismatch = keys.reduce(function(mismatch, key){ + // check the info key + // increment the mismatch if different + if (info[key] !== req.config[key]) { + mismatch++; + } + return mismatch; + }, 0); + + // All keys mismatch + if (keyMismatch == keys.length) { return invalidRequest(req, res); } From 4e5cc1feb0441a73a446ce032e4f32ee5dca4160 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 26 Feb 2016 10:21:52 -0500 Subject: [PATCH 09/12] Fixes handing of no keys set in config --- src/middlewares.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/middlewares.js b/src/middlewares.js index 939489d3..8acece2d 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -103,9 +103,9 @@ function handleParseHeaders(req, res, next) { // We do it with mismatching keys to support no-keys config var keyMismatch = keys.reduce(function(mismatch, key){ - // check the info key - // increment the mismatch if different - if (info[key] !== req.config[key]) { + + // check if set in the config and compare + if (req.config[key] && info[key] !== req.config[key]) { mismatch++; } return mismatch; From dd771d429eb90dba077e851627f75646f4ad8103 Mon Sep 17 00:00:00 2001 From: "Peter J. Shin" Date: Fri, 26 Feb 2016 10:50:27 -0800 Subject: [PATCH 10/12] Create ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..29a1b42c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,13 @@ +Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server! + +-[ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites). +-[ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server. +-[ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before. + +#### Environment Setup + + +#### Steps to reproduce + + +#### Logs/Trace From cdb82c20bb897dc4906f1741904a34cb54d76660 Mon Sep 17 00:00:00 2001 From: "Peter J. Shin" Date: Fri, 26 Feb 2016 10:56:45 -0800 Subject: [PATCH 11/12] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 29a1b42c..0fb61964 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,9 @@ Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server! -[ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites). + -[ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server. + -[ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before. #### Environment Setup From aee968a8536f19cced1b67646cc18afc302132d1 Mon Sep 17 00:00:00 2001 From: stephentuso Date: Fri, 26 Feb 2016 21:05:46 -0500 Subject: [PATCH 12/12] Add duplicate device token test --- spec/ParseInstallation.spec.js | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index cef6871e..880497e3 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -445,6 +445,52 @@ describe('Installations', () => { }); }); + it('update android device token with duplicate device token', (done) => { + var installId1 = '11111111-abcd-abcd-abcd-123456789abc'; + var installId2 = '22222222-abcd-abcd-abcd-123456789abc'; + var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + var input = { + 'installationId': installId1, + 'deviceToken': t, + 'deviceType': 'android' + }; + var firstObject; + var secondObject; + rest.create(config, auth.nobody(config), '_Installation', input) + .then(() => { + input = { + 'installationId': installId2, + 'deviceType': 'android' + }; + return rest.create(config, auth.nobody(config), '_Installation', input); + }).then(() => { + return database.mongoFind('_Installation', + {installationId: installId1}, {}); + }).then((results) => { + expect(results.length).toEqual(1); + firstObject = results[0]; + return database.mongoFind('_Installation', + {installationId: installId2}, {}); + }).then((results) => { + expect(results.length).toEqual(1); + secondObject = results[0]; + // Update second installation to conflict with first installation + input = { + 'objectId': secondObject._id, + 'deviceToken': t + }; + return rest.update(config, auth.nobody(config), '_Installation', + secondObject._id, input); + }).then(() => { + // The first object should have been deleted + return database.mongoFind('_Installation', {_id: firstObject._id}, {}); + }).then((results) => { + expect(results.length).toEqual(0); + done(); + }).catch((error) => { console.log(error); }); + }); + + it('update ios device token with duplicate device token', (done) => { var installId1 = '11111111-abcd-abcd-abcd-123456789abc'; var installId2 = '22222222-abcd-abcd-abcd-123456789abc';