From e67edd1d1a126535ddaf4f90ac695945ca2ee88a Mon Sep 17 00:00:00 2001 From: Cedric Kemp Date: Thu, 7 Jun 2018 12:10:04 -0400 Subject: [PATCH 1/2] Don't merge JSON fields after save() when using Postgres to keep same behaviour as MongoDB (#4808) --- spec/ParseObject.spec.js | 27 +++++++++++++++++++ .../Postgres/PostgresStorageAdapter.js | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index aedf251118..954d21a6f4 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -1980,4 +1980,31 @@ describe('Parse.Object testing', () => { done(); }) }); + + it ('Update object field should store exactly same sent object', async (done) => { + let object = new TestObject(); + + // Set initial data + object.set("jsonData", { a: "b" }); + object = await object.save(); + equal(object.get('jsonData'), { a: "b" }); + + // Set empty JSON + object.set("jsonData", {}); + object = await object.save(); + equal(object.get('jsonData'), {}); + + // Set new JSON data + object.unset('jsonData') + object.set("jsonData", { c: "d" }); + object = await object.save(); + equal(object.get('jsonData'), { c: "d" }); + + // Fetch object from server + object = await object.fetch() + console.log(object.id, object.get('jsonData')) + equal(object.get('jsonData'), { c: "d" }); + + done(); + }); }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 540516d37d..0b9ed5ec06 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1324,7 +1324,7 @@ export class PostgresStorageAdapter implements StorageAdapter { return p + ` - '$${index + 1 + i}:value'`; }, ''); - updatePatterns.push(`$${index}:name = ( COALESCE($${index}:name, '{}'::jsonb) ${deletePatterns} ${incrementPatterns} || $${index + 1 + keysToDelete.length}::jsonb )`); + updatePatterns.push(`$${index}:name = ('{}'::jsonb ${deletePatterns} ${incrementPatterns} || $${index + 1 + keysToDelete.length}::jsonb )`); values.push(fieldName, ...keysToDelete, JSON.stringify(fieldValue)); index += 2 + keysToDelete.length; From 5f0597d3d21b5e74b00ba81223f249eda07b1359 Mon Sep 17 00:00:00 2001 From: Cedric Kemp Date: Sun, 10 Jun 2018 14:10:33 -0400 Subject: [PATCH 2/2] Added file creation policy --- README.md | 1 + spec/ParseFile.spec.js | 89 ++++++++++++++++++++++++++++++++++++++ src/Routers/FilesRouter.js | 15 +++++++ 3 files changed, 105 insertions(+) diff --git a/README.md b/README.md index 0b6a2ea132..1d90073d47 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,7 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo * `masterKeyIps` - The array of ip addresses where masterKey usage will be restricted to only these ips. (Default to [] which means allow all ips). If you're using this feature and have `useMasterKey: true` in cloudcode, make sure that you put your own ip in this list. * `readOnlyMasterKey` - A masterKey that has full read access to the data, but no write access. This key should be treated the same way as your masterKey, keeping it private. * `objectIdSize` - The string length of the newly generated object's ids. +* `fileCreationPolicy` - Permissions for file creation. Set `readonly` to prevent anyone including master to create new files. Set `master` to only allow master to create files. Set `user` to allow master and users to create files. By default, anonymous users are allowed to create new files. ##### Logging diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 9f19a87119..c1895b1add 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -363,6 +363,95 @@ describe('Parse.File testing', () => { }); }); + it("save file with 'readonly' file creation policy", async () => { + await reconfigureServer({ + fileCreationPolicy: 'readonly' + }) + + const file = new Parse.File("hello.txt", data, "text/plain"); + try { + await file.save(); + fail('file.save() should not be allowed') + } catch (error) { + expect(error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + } + }); + + it("save file with 'master' file creation policy", async done => { + await reconfigureServer({ + fileCreationPolicy: 'master' + }) + + // Save as anonymous + let file = new Parse.File("hello.txt", data, "text/plain"); + try { + await file.save(); + fail('file.save() should not be allowed for anonymous users') + } catch (error) { + expect(error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + } + + // Save as master + file = new Parse.File("hello.txt", data, "text/plain"); + await file.save({ useMasterKey: true }); + + // Save as user + let user = new Parse.User(); + user.set('username', `user${Math.floor(Math.random() * 10000)}`); + user.set('password', 'p@ssw0rd'); + user = await user.signUp(); + request.post({ + headers: { + 'Content-Type': 'text/html', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: 'http://localhost:8378/1/files/file', + body: 'fee fi fo', + }, (error, response, body) => { + const b = JSON.parse(body); + expect(b.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + done(); + }); + }); + + it("save file with 'user' file creation policy", async done => { + await reconfigureServer({ + fileCreationPolicy: 'user' + }) + + // Save as anonymous + let file = new Parse.File("hello.txt", data, "text/plain"); + try { + await file.save(); + fail('file.save() should not be allowed for anonymous users') + } catch (error) { + expect(error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + } + + // Save as master + file = new Parse.File("hello.txt", data, "text/plain"); + await file.save({ useMasterKey: true }); + + // Save as user + let user = new Parse.User(); + user.set('username', `user${Math.floor(Math.random() * 10000)}`); + user.set('password', 'p@ssw0rd'); + user = await user.signUp(); + request.post({ + headers: { + 'Content-Type': 'text/html', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: 'http://localhost:8378/1/files/file', + body: 'fee fi fo', + }, (error) => { + expect(error).toBe(null); + done(); + }); + }); + it("file toJSON testing", done => { const file = new Parse.File("hello.txt", data, "text/plain"); ok(!file.url()); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 8312526f30..ff5bf0004e 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -61,6 +61,21 @@ export class FilesRouter { } createHandler(req, res, next) { + if (req.config.fileCreationPolicy === 'readonly') { + next(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to perform the file create operation.')); + return; + } + + if (req.config.fileCreationPolicy === 'master' && !req.auth.isMaster) { + next(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Only master is allowed to create new files.')); + return; + } + + if (req.config.fileCreationPolicy === 'user' && !req.auth.isMaster && !req.auth.user) { + next(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Only master and registred users aire allowed to create new files.')); + return; + } + if (!req.body || !req.body.length) { next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.'));