From 65745dcbf7e4aaf9d476f8a224caa331a8d99b57 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 15 Dec 2020 00:06:58 +1100 Subject: [PATCH 01/12] new: defaultACL options for server config --- resources/buildConfigDefinitions.js | 7 ++ spec/ParseACL.spec.js | 173 ++++++++++++++++++++++++++++ spec/helper.js | 1 + src/Options/Definitions.js | 6 + src/Options/docs.js | 1 + src/Options/index.js | 5 + src/RestWrite.js | 125 ++++++++++++++++++++ 7 files changed, 318 insertions(+) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 99b57b1379..f840e88ea3 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -163,6 +163,13 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } + if (type == 'StringOrAny') { + if (value == '""' || value == "''") { + literalValue = t.stringLiteral(''); + } else { + literalValue = t.stringLiteral(value); + } + } if (type == 'CustomPagesOptions') { const object = parsers.objectParser(value); const props = Object.keys(object).map((key) => { diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index a55f40cd42..6f8b8490ba 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -931,4 +931,177 @@ describe('Parse.ACL', () => { rest.create(config, auth.nobody(config), '_User', anonUser); }); + + it('defaultACL private', async function (done) { + await reconfigureServer({ + defaultACL: 'private', + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*']).toBeUndefined(); + expect(acl[user.id].write).toBeTrue(); + expect(acl[user.id].read).toBeTrue(); + done(); + }); + + it('defaultACL publicRead', async function (done) { + await reconfigureServer({ + defaultACL: 'publicRead', + }); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*'].read).toBe(true); + expect(acl['*'].write).toBeUndefined(); + done(); + }); + + it('defaultACL publicWrite', async function (done) { + await reconfigureServer({ + defaultACL: 'publicWrite', + }); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*'].write).toBe(true); + expect(acl['*'].read).toBeUndefined(); + done(); + }); + + it('defaultACL roleRead', async function (done) { + await reconfigureServer({ + defaultACL: 'roleRead:Administrator', + }); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*']).toBeUndefined(); + expect(acl['role:Administrator'].read).toBe(true); + expect(acl['role:Administrator'].write).toBeUndefined(); + done(); + }); + + it('defaultACL roleWrite', async function (done) { + await reconfigureServer({ + defaultACL: 'roleWrite:Administrator', + }); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*']).toBeUndefined(); + expect(acl['role:Administrator'].read).toBeUndefined(); + expect(acl['role:Administrator'].write).toBe(true); + done(); + }); + + it('defaultACL roleReadWrite', async function (done) { + await reconfigureServer({ + defaultACL: 'roleReadWrite:Administrator', + }); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*']).toBeUndefined(); + expect(acl['role:Administrator'].read).toBe(true); + expect(acl['role:Administrator'].write).toBe(true); + done(); + }); + + it('defaultACL object readWrite', async function (done) { + await reconfigureServer({ + defaultACL: { + TestObject: { + public: 'readWrite', + private: 'readWrite', + 'role:Administrator': 'readWrite', + customId: 'readWrite', + }, + }, + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*'].read).toBe(true); + expect(acl['*'].write).toBe(true); + expect(acl[user.id].read).toBe(true); + expect(acl[user.id].write).toBe(true); + expect(acl['role:Administrator'].read).toBe(true); + expect(acl['role:Administrator'].write).toBe(true); + expect(acl['customId'].read).toBe(true); + expect(acl['customId'].write).toBe(true); + done(); + }); + + it('defaultACL object read', async function (done) { + await reconfigureServer({ + defaultACL: { + TestObject: { + public: 'read', + private: 'read', + 'role:Administrator': 'read', + customId: 'read', + }, + }, + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*'].read).toBe(true); + expect(acl['*'].write).toBeUndefined(); + expect(acl[user.id].read).toBe(true); + expect(acl[user.id].write).toBeUndefined(); + expect(acl['role:Administrator'].read).toBe(true); + expect(acl['role:Administrator'].write).toBeUndefined(); + expect(acl['customId'].read).toBe(true); + expect(acl['customId'].write).toBeUndefined(); + done(); + }); + + it('defaultACL object write', async function (done) { + await reconfigureServer({ + defaultACL: { + TestObject: { + public: 'write', + private: 'write', + 'role:Administrator': 'write', + customId: 'write', + }, + }, + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*'].write).toBe(true); + expect(acl['*'].read).toBeUndefined(); + expect(acl[user.id].write).toBe(true); + expect(acl[user.id].read).toBeUndefined(); + expect(acl['role:Administrator'].write).toBe(true); + expect(acl['role:Administrator'].read).toBeUndefined(); + expect(acl['customId'].write).toBe(true); + expect(acl['customId'].read).toBeUndefined(); + done(); + }); }); diff --git a/spec/helper.js b/spec/helper.js index a7f6cf2280..7daaede240 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -94,6 +94,7 @@ const defaultConfiguration = { apiKey: 'yolo', }, }, + defaultACL: 'publicReadWrite', auth: { // Override the facebook provider custom: mockCustom(), diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index bce3756360..85d61a18ec 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -109,6 +109,12 @@ module.exports.ParseServerOptions = { required: true, default: 'mongodb://localhost:27017/parse', }, + defaultACL: { + env: 'PARSE_SERVER_DEFAULT_ACL', + help: 'Options for default ACL on classes', + action: parsers.objectParser, + default: 'private', + }, directAccess: { env: 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index 576ff60a14..4ac1024c96 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -20,6 +20,7 @@ * @property {Adapter} databaseAdapter Adapter module for the database * @property {Any} databaseOptions Options to pass to the mongodb client * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. + * @property {StringOrAny} defaultACL Options for default ACL on classes * @property {Boolean} directAccess Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production. * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending diff --git a/src/Options/index.js b/src/Options/index.js index d2237e08a8..7f267a20b4 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -11,6 +11,7 @@ import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter'; type Adapter = string | any | T; type NumberOrBoolean = number | boolean; type NumberOrString = number | string; +type StringOrAny = string | any; type ProtectedFields = any; export interface ParseServerOptions { @@ -198,6 +199,10 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS :DEFAULT: false */ idempotencyOptions: ?IdempotencyOptions; + /* Options for default ACL on classes + :ENV: PARSE_SERVER_DEFAULT_ACL + :DEFAULT: private */ + defaultACL: ?StringOrAny; /* Full path to your GraphQL custom schema.graphql file */ graphQLSchema: ?string; /* Mounts the GraphQL endpoint diff --git a/src/RestWrite.js b/src/RestWrite.js index 38b318100c..ddc6495831 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -104,6 +104,9 @@ RestWrite.prototype.execute = function () { .then(() => { return this.validateAuthData(); }) + .then(() => { + return this.buildDefaultACL(); + }) .then(() => { return this.runBeforeSaveTrigger(); }) @@ -197,6 +200,128 @@ RestWrite.prototype.validateSchema = function () { ); }; +// builds the default ACL depending on the className +RestWrite.prototype.buildDefaultACL = function () { + if (this.data.ACL) { + return; + } + let aclOptions = this.config.defaultACL; + const getRoleName = roleStr => { + return aclOptions.split(`${roleStr}:`)[1]; + }; + const reqUser = this.auth.user && this.auth.user.id; + const acl = new Parse.ACL(); + if (typeof aclOptions === 'string') { + if (aclOptions === 'private') { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + if (reqUser) { + acl.setReadAccess(reqUser, true); + acl.setWriteAccess(reqUser, true); + } + } else if (aclOptions === 'publicRead') { + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(false); + } else if (aclOptions === 'publicWrite') { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(true); + } else if (aclOptions === 'publicReadWrite') { + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(true); + } else if (aclOptions.includes('roleRead:')) { + const roleName = getRoleName('roleRead'); + if (roleName) { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + acl.setRoleReadAccess(roleName, true); + acl.setRoleWriteAccess(roleName, false); + } + } else if (aclOptions.includes('roleWrite:')) { + const roleName = getRoleName('roleWrite'); + if (roleName) { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + acl.setRoleReadAccess(roleName, false); + acl.setRoleWriteAccess(roleName, true); + } + } else if (aclOptions.includes('roleReadWrite:')) { + const roleName = getRoleName('roleReadWrite'); + if (roleName) { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + acl.setRoleReadAccess(roleName, true); + acl.setRoleWriteAccess(roleName, true); + } + } + } else { + aclOptions = aclOptions[this.className] || aclOptions['*']; + if (!aclOptions) { + return; + } + if (aclOptions.public) { + if (aclOptions.public === 'readWrite') { + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(true); + } else if (aclOptions.public === 'read') { + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(false); + } else if (aclOptions.public === 'write') { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(true); + } + } else { + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + } + if (aclOptions['private'] && reqUser) { + if (aclOptions['private'] === 'readWrite') { + acl.setReadAccess(reqUser, true); + acl.setWriteAccess(reqUser, true); + } else if (aclOptions['private'] === 'read') { + acl.setReadAccess(reqUser, true); + acl.setWriteAccess(reqUser, false); + } else if (aclOptions['private'] === 'write') { + acl.setReadAccess(reqUser, false); + acl.setWriteAccess(reqUser, true); + } + } + delete aclOptions['private']; + delete aclOptions['public']; + for (const user in aclOptions) { + const aclValue = aclOptions[user]; + if (aclValue.includes('role:')) { + const roleName = getRoleName('role'); + if (aclValue === 'readWrite') { + acl.setRoleReadAccess(roleName, true); + acl.setRoleWriteAccess(roleName, true); + } else if (aclValue === 'read') { + acl.setRoleReadAccess(roleName, true); + acl.setRoleWriteAccess(roleName, false); + } else if (aclValue === 'write') { + acl.setRoleReadAccess(roleName, false); + acl.setRoleWriteAccess(roleName, true); + } + } else { + if (aclValue === 'readWrite') { + acl.setReadAccess(user, true); + acl.setWriteAccess(user, true); + } else if (aclValue === 'read') { + acl.setReadAccess(user, true); + acl.setWriteAccess(user, false); + } else if (aclValue === 'write') { + acl.setReadAccess(user, false); + acl.setWriteAccess(user, true); + } + } + } + } + this.data.ACL = acl.toJSON(); + this.storage.fieldsChangedByTrigger = this.storage.fieldsChangedByTrigger || []; + if (this.storage.fieldsChangedByTrigger.indexOf('ACL') < 0) { + this.storage.fieldsChangedByTrigger.push('ACL'); + } +}; + // Runs any beforeSave triggers against this operation. // Any change leads to our data being mutated. RestWrite.prototype.runBeforeSaveTrigger = function () { From 443b4c6771d15e5426603ee2405ef46f67d1a7d6 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 16:08:34 +1100 Subject: [PATCH 02/12] check default configuration --- spec/ParseACL.spec.js | 15 +++++++++++++++ src/RestWrite.js | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 6f8b8490ba..cc0d99df16 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -932,6 +932,21 @@ describe('Parse.ACL', () => { rest.create(config, auth.nobody(config), '_User', anonUser); }); + it('defaultACL should be private', async () => { + await reconfigureServer({ + defaultACL: null, + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*']).toBeUndefined(); + expect(acl[user.id].write).toBeTrue(); + expect(acl[user.id].read).toBeTrue(); + }); + it('defaultACL private', async function (done) { await reconfigureServer({ defaultACL: 'private', diff --git a/src/RestWrite.js b/src/RestWrite.js index ddc6495831..2dfccf427b 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -205,7 +205,7 @@ RestWrite.prototype.buildDefaultACL = function () { if (this.data.ACL) { return; } - let aclOptions = this.config.defaultACL; + let aclOptions = this.config.defaultACL || 'private'; const getRoleName = roleStr => { return aclOptions.split(`${roleStr}:`)[1]; }; From d88047ed9b1ce92235daedb622cefb9440cb352e Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 16:19:18 +1100 Subject: [PATCH 03/12] Update RestWrite.js --- src/RestWrite.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 2dfccf427b..ebaa2a037b 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -215,10 +215,6 @@ RestWrite.prototype.buildDefaultACL = function () { if (aclOptions === 'private') { acl.setPublicReadAccess(false); acl.setPublicWriteAccess(false); - if (reqUser) { - acl.setReadAccess(reqUser, true); - acl.setWriteAccess(reqUser, true); - } } else if (aclOptions === 'publicRead') { acl.setPublicReadAccess(true); acl.setPublicWriteAccess(false); @@ -253,6 +249,10 @@ RestWrite.prototype.buildDefaultACL = function () { acl.setRoleWriteAccess(roleName, true); } } + if (reqUser) { + acl.setReadAccess(reqUser, true); + acl.setWriteAccess(reqUser, true); + } } else { aclOptions = aclOptions[this.className] || aclOptions['*']; if (!aclOptions) { From 2bdeca5b2d7546b026a51f3776e5c94a1e97c0e1 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 16:31:24 +1100 Subject: [PATCH 04/12] run defintions --- src/Options/Definitions.js | 387 +++++++++++++++++++------------------ 1 file changed, 201 insertions(+), 186 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index bea7f7a140..b11ee69985 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -3,7 +3,7 @@ This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js */ -var parsers = require("./parsers"); +var parsers = require('./parsers'); module.exports.ParseServerOptions = { accountLockout: { @@ -181,6 +181,11 @@ module.exports.ParseServerOptions = { help: 'Adapter module for the files sub-system', action: parsers.moduleOrObjectParser, }, + fileUpload: { + env: 'PARSE_SERVER_FILE_UPLOAD_OPTIONS', + help: 'Options for file uploads', + action: parsers.objectParser, + }, graphQLPath: { env: 'PARSE_SERVER_GRAPHQL_PATH', help: 'Mount path for the GraphQL endpoint, defaults to /graphql', @@ -418,201 +423,211 @@ module.exports.ParseServerOptions = { }, }; module.exports.CustomPagesOptions = { - "choosePassword": { - "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", - "help": "choose password page path" - }, - "invalidLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", - "help": "invalid link page path" - }, - "invalidVerificationLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK", - "help": "invalid verification link page path" - }, - "linkSendFail": { - "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL", - "help": "verification link send fail page path" - }, - "linkSendSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS", - "help": "verification link send success page path" - }, - "parseFrameURL": { - "env": "PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL", - "help": "for masking user-facing pages" - }, - "passwordResetSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS", - "help": "password reset success page path" - }, - "verifyEmailSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS", - "help": "verify email success page path" - } + choosePassword: { + env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', + help: 'choose password page path', + }, + invalidLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', + help: 'invalid link page path', + }, + invalidVerificationLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK', + help: 'invalid verification link page path', + }, + linkSendFail: { + env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL', + help: 'verification link send fail page path', + }, + linkSendSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS', + help: 'verification link send success page path', + }, + parseFrameURL: { + env: 'PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL', + help: 'for masking user-facing pages', + }, + passwordResetSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', + help: 'password reset success page path', + }, + verifyEmailSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', + help: 'verify email success page path', + }, }; module.exports.LiveQueryOptions = { - "classNames": { - "env": "PARSE_SERVER_LIVEQUERY_CLASSNAMES", - "help": "parse-server's LiveQuery classNames", - "action": parsers.arrayParser - }, - "pubSubAdapter": { - "env": "PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER", - "help": "LiveQuery pubsub adapter", - "action": parsers.moduleOrObjectParser - }, - "redisOptions": { - "env": "PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS", - "help": "parse-server's LiveQuery redisOptions", - "action": parsers.objectParser - }, - "redisURL": { - "env": "PARSE_SERVER_LIVEQUERY_REDIS_URL", - "help": "parse-server's LiveQuery redisURL" - }, - "wssAdapter": { - "env": "PARSE_SERVER_LIVEQUERY_WSS_ADAPTER", - "help": "Adapter module for the WebSocketServer", - "action": parsers.moduleOrObjectParser - } + classNames: { + env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', + help: "parse-server's LiveQuery classNames", + action: parsers.arrayParser, + }, + pubSubAdapter: { + env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', + help: 'LiveQuery pubsub adapter', + action: parsers.moduleOrObjectParser, + }, + redisOptions: { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS', + help: "parse-server's LiveQuery redisOptions", + action: parsers.objectParser, + }, + redisURL: { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', + help: "parse-server's LiveQuery redisURL", + }, + wssAdapter: { + env: 'PARSE_SERVER_LIVEQUERY_WSS_ADAPTER', + help: 'Adapter module for the WebSocketServer', + action: parsers.moduleOrObjectParser, + }, }; module.exports.LiveQueryServerOptions = { - "appId": { - "env": "PARSE_LIVE_QUERY_SERVER_APP_ID", - "help": "This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId." - }, - "cacheTimeout": { - "env": "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT", - "help": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", - "action": parsers.numberParser("cacheTimeout") - }, - "keyPairs": { - "env": "PARSE_LIVE_QUERY_SERVER_KEY_PAIRS", - "help": "A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.", - "action": parsers.objectParser - }, - "logLevel": { - "env": "PARSE_LIVE_QUERY_SERVER_LOG_LEVEL", - "help": "This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO." - }, - "masterKey": { - "env": "PARSE_LIVE_QUERY_SERVER_MASTER_KEY", - "help": "This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey." - }, - "port": { - "env": "PARSE_LIVE_QUERY_SERVER_PORT", - "help": "The port to run the LiveQuery server, defaults to 1337.", - "action": parsers.numberParser("port"), - "default": 1337 - }, - "pubSubAdapter": { - "env": "PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER", - "help": "LiveQuery pubsub adapter", - "action": parsers.moduleOrObjectParser - }, - "redisOptions": { - "env": "PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS", - "help": "parse-server's LiveQuery redisOptions", - "action": parsers.objectParser - }, - "redisURL": { - "env": "PARSE_LIVE_QUERY_SERVER_REDIS_URL", - "help": "parse-server's LiveQuery redisURL" - }, - "serverURL": { - "env": "PARSE_LIVE_QUERY_SERVER_SERVER_URL", - "help": "This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL." - }, - "websocketTimeout": { - "env": "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT", - "help": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).", - "action": parsers.numberParser("websocketTimeout") - }, - "wssAdapter": { - "env": "PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER", - "help": "Adapter module for the WebSocketServer", - "action": parsers.moduleOrObjectParser - } + appId: { + env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', + help: + 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', + }, + cacheTimeout: { + env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', + help: + "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 5 * 1000 ms (5 seconds).", + action: parsers.numberParser('cacheTimeout'), + }, + keyPairs: { + env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', + help: + 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', + action: parsers.objectParser, + }, + logLevel: { + env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', + help: + 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', + }, + masterKey: { + env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', + help: + 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', + }, + port: { + env: 'PARSE_LIVE_QUERY_SERVER_PORT', + help: 'The port to run the LiveQuery server, defaults to 1337.', + action: parsers.numberParser('port'), + default: 1337, + }, + pubSubAdapter: { + env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', + help: 'LiveQuery pubsub adapter', + action: parsers.moduleOrObjectParser, + }, + redisOptions: { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS', + help: "parse-server's LiveQuery redisOptions", + action: parsers.objectParser, + }, + redisURL: { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', + help: "parse-server's LiveQuery redisURL", + }, + serverURL: { + env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', + help: + 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', + }, + websocketTimeout: { + env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', + help: + 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', + action: parsers.numberParser('websocketTimeout'), + }, + wssAdapter: { + env: 'PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER', + help: 'Adapter module for the WebSocketServer', + action: parsers.moduleOrObjectParser, + }, }; module.exports.IdempotencyOptions = { - "paths": { - "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS", - "help": "An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.", - "action": parsers.arrayParser, - "default": [] - }, - "ttl": { - "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL", - "help": "The duration in seconds after which a request record is discarded from the database, defaults to 300s.", - "action": parsers.numberParser("ttl"), - "default": 300 - } + paths: { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS', + help: + 'An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.', + action: parsers.arrayParser, + default: [], + }, + ttl: { + env: 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL', + help: + 'The duration in seconds after which a request record is discarded from the database, defaults to 300s.', + action: parsers.numberParser('ttl'), + default: 300, + }, }; module.exports.AccountLockoutOptions = { - "duration": { - "env": "PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION", - "help": "number of minutes that a locked-out account remains locked out before automatically becoming unlocked.", - "action": parsers.numberParser("duration") - }, - "threshold": { - "env": "PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD", - "help": "number of failed sign-in attempts that will cause a user account to be locked", - "action": parsers.numberParser("threshold") - } + duration: { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', + help: + 'number of minutes that a locked-out account remains locked out before automatically becoming unlocked.', + action: parsers.numberParser('duration'), + }, + threshold: { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_THRESHOLD', + help: 'number of failed sign-in attempts that will cause a user account to be locked', + action: parsers.numberParser('threshold'), + }, }; module.exports.PasswordPolicyOptions = { - "doNotAllowUsername": { - "env": "PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME", - "help": "disallow username in passwords", - "action": parsers.booleanParser - }, - "maxPasswordAge": { - "env": "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE", - "help": "days for password expiry", - "action": parsers.numberParser("maxPasswordAge") - }, - "maxPasswordHistory": { - "env": "PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY", - "help": "setting to prevent reuse of previous n passwords", - "action": parsers.numberParser("maxPasswordHistory") - }, - "resetTokenReuseIfValid": { - "env": "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID", - "help": "resend token if it's still valid", - "action": parsers.booleanParser - }, - "resetTokenValidityDuration": { - "env": "PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION", - "help": "time for token to expire", - "action": parsers.numberParser("resetTokenValidityDuration") - }, - "validatorCallback": { - "env": "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK", - "help": "a callback function to be invoked to validate the password" - }, - "validatorPattern": { - "env": "PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN", - "help": "a RegExp object or a regex string representing the pattern to enforce" - } + doNotAllowUsername: { + env: 'PARSE_SERVER_PASSWORD_POLICY_DO_NOT_ALLOW_USERNAME', + help: 'disallow username in passwords', + action: parsers.booleanParser, + }, + maxPasswordAge: { + env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_AGE', + help: 'days for password expiry', + action: parsers.numberParser('maxPasswordAge'), + }, + maxPasswordHistory: { + env: 'PARSE_SERVER_PASSWORD_POLICY_MAX_PASSWORD_HISTORY', + help: 'setting to prevent reuse of previous n passwords', + action: parsers.numberParser('maxPasswordHistory'), + }, + resetTokenReuseIfValid: { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_REUSE_IF_VALID', + help: "resend token if it's still valid", + action: parsers.booleanParser, + }, + resetTokenValidityDuration: { + env: 'PARSE_SERVER_PASSWORD_POLICY_RESET_TOKEN_VALIDITY_DURATION', + help: 'time for token to expire', + action: parsers.numberParser('resetTokenValidityDuration'), + }, + validatorCallback: { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_CALLBACK', + help: 'a callback function to be invoked to validate the password', + }, + validatorPattern: { + env: 'PARSE_SERVER_PASSWORD_POLICY_VALIDATOR_PATTERN', + help: 'a RegExp object or a regex string representing the pattern to enforce', + }, }; module.exports.FileUploadOptions = { - "enableForAnonymousUser": { - "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER", - "help": "Is true if file upload should be allowed for anonymous users.", - "action": parsers.booleanParser, - "default": false - }, - "enableForAuthenticatedUser": { - "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER", - "help": "Is true if file upload should be allowed for authenticated users.", - "action": parsers.booleanParser, - "default": true - }, - "enableForPublic": { - "env": "PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC", - "help": "Is true if file upload should be allowed for anyone, regardless of user authentication.", - "action": parsers.booleanParser, - "default": false - } + enableForAnonymousUser: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_ANONYMOUS_USER', + help: 'Is true if file upload should be allowed for anonymous users.', + action: parsers.booleanParser, + default: false, + }, + enableForAuthenticatedUser: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_AUTHENTICATED_USER', + help: 'Is true if file upload should be allowed for authenticated users.', + action: parsers.booleanParser, + default: true, + }, + enableForPublic: { + env: 'PARSE_SERVER_FILE_UPLOAD_ENABLE_FOR_PUBLIC', + help: 'Is true if file upload should be allowed for anyone, regardless of user authentication.', + action: parsers.booleanParser, + default: false, + }, }; From 80829a0b896d182f65df85fd33634e87d9baf18f Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 16:47:19 +1100 Subject: [PATCH 05/12] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d928e41ffb..98a634b0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ __BREAKING CHANGES:__ - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy). +- NEW: Added default ACL options. Parse.Objects now default to Public Read and Write false for improved security. This only affects saving new Parse.Objects that do not have ACL set. To allow default public read and write, set the `defaultACL` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) to `publicReadWrite`. [#7111](https://github.com/parse-community/parse-server/pull/7111). Thanks to [dblythy](https://github.com/dblythy). ___ - IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz) - FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy) From 2d3a6b83b6c48cdf0e939b6f2131f4a0b385122f Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 18:25:19 +1100 Subject: [PATCH 06/12] fix failing tests --- spec/ParseACL.spec.js | 25 +++++++++++++++++++++++++ src/RestWrite.js | 12 ++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index cc0d99df16..52235e2186 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -963,6 +963,31 @@ describe('Parse.ACL', () => { done(); }); + it('defaultACL prevents other users', async function (done) { + await reconfigureServer({ + defaultACL: 'private', + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const user2 = await Parse.User.signUp('testuser2', 'p@ssword'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.getACL()).toBeDefined(); + const acl = obj.getACL().toJSON(); + expect(acl['*']).toBeUndefined(); + expect(acl[user.id].write).toBeTrue(); + expect(acl[user.id].read).toBeTrue(); + const objQuery = new Parse.Query('TestObject'); + try { + await objQuery.get(obj.id, { sessionToken: user2.getSessionToken() }); + fail('should not have been able to get this object'); + } catch (e) { + expect(e.code).toBe(101); + } + + done(); + }); + it('defaultACL publicRead', async function (done) { await reconfigureServer({ defaultACL: 'publicRead', diff --git a/src/RestWrite.js b/src/RestWrite.js index ebaa2a037b..4f41c2bea6 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -105,10 +105,10 @@ RestWrite.prototype.execute = function () { return this.validateAuthData(); }) .then(() => { - return this.buildDefaultACL(); + return this.runBeforeSaveTrigger(); }) .then(() => { - return this.runBeforeSaveTrigger(); + return this.buildDefaultACL(); }) .then(() => { return this.deleteEmailResetTokenIfNeeded(); @@ -202,7 +202,7 @@ RestWrite.prototype.validateSchema = function () { // builds the default ACL depending on the className RestWrite.prototype.buildDefaultACL = function () { - if (this.data.ACL) { + if (this.data.ACL || (this.originalData && this.originalData.ACL)) { return; } let aclOptions = this.config.defaultACL || 'private'; @@ -212,6 +212,9 @@ RestWrite.prototype.buildDefaultACL = function () { const reqUser = this.auth.user && this.auth.user.id; const acl = new Parse.ACL(); if (typeof aclOptions === 'string') { + if (aclOptions === 'publicReadWrite') { + return; + } if (aclOptions === 'private') { acl.setPublicReadAccess(false); acl.setPublicWriteAccess(false); @@ -221,9 +224,6 @@ RestWrite.prototype.buildDefaultACL = function () { } else if (aclOptions === 'publicWrite') { acl.setPublicReadAccess(false); acl.setPublicWriteAccess(true); - } else if (aclOptions === 'publicReadWrite') { - acl.setPublicReadAccess(true); - acl.setPublicWriteAccess(true); } else if (aclOptions.includes('roleRead:')) { const roleName = getRoleName('roleRead'); if (roleName) { From d9b6b2ac8643ce56b331ae06b67692b4214c3d2e Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 20:26:21 +1100 Subject: [PATCH 07/12] change to currentUser --- resources/buildConfigDefinitions.js | 9 ++ spec/ParseACL.spec.js | 98 +++++++++++++++++----- spec/helper.js | 7 +- src/Options/Definitions.js | 7 +- src/Options/docs.js | 2 +- src/Options/index.js | 6 +- src/RestWrite.js | 124 +++------------------------- 7 files changed, 114 insertions(+), 139 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 3300310797..dcaf06f286 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -187,6 +187,15 @@ function parseDefaultValue(elt, value, t) { ); literalValue = t.objectExpression([prop]); } + if (type == 'ACLOptions') { + const prop = t.objectProperty( + t.stringLiteral('currentUser'), t.objectPattern([ + t.objectProperty(t.stringLiteral('read'), t.booleanLiteral(true)), + t.objectProperty(t.stringLiteral('write'), t.booleanLiteral(true)), + ]) + ); + literalValue = t.objectExpression([prop]); + } } return literalValue; } diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 52235e2186..4ae7f78440 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -949,7 +949,12 @@ describe('Parse.ACL', () => { it('defaultACL private', async function (done) { await reconfigureServer({ - defaultACL: 'private', + defaultACL: { + currentUser: { + read: true, + write: true, + }, + }, }); const user = await Parse.User.signUp('testuser', 'p@ssword'); const obj = new Parse.Object('TestObject'); @@ -965,7 +970,12 @@ describe('Parse.ACL', () => { it('defaultACL prevents other users', async function (done) { await reconfigureServer({ - defaultACL: 'private', + defaultACL: { + currentUser: { + read: true, + write: true, + }, + }, }); const user = await Parse.User.signUp('testuser', 'p@ssword'); const user2 = await Parse.User.signUp('testuser2', 'p@ssword'); @@ -990,7 +1000,12 @@ describe('Parse.ACL', () => { it('defaultACL publicRead', async function (done) { await reconfigureServer({ - defaultACL: 'publicRead', + defaultACL: { + '*': { + read: true, + write: true, + }, + }, }); const obj = new Parse.Object('TestObject'); obj.set('foo', 'bar'); @@ -1004,7 +1019,11 @@ describe('Parse.ACL', () => { it('defaultACL publicWrite', async function (done) { await reconfigureServer({ - defaultACL: 'publicWrite', + defaultACL: { + '*': { + write: true, + }, + }, }); const obj = new Parse.Object('TestObject'); obj.set('foo', 'bar'); @@ -1018,7 +1037,11 @@ describe('Parse.ACL', () => { it('defaultACL roleRead', async function (done) { await reconfigureServer({ - defaultACL: 'roleRead:Administrator', + defaultACL: { + 'role:Administrator': { + read: true, + }, + }, }); const obj = new Parse.Object('TestObject'); obj.set('foo', 'bar'); @@ -1033,7 +1056,11 @@ describe('Parse.ACL', () => { it('defaultACL roleWrite', async function (done) { await reconfigureServer({ - defaultACL: 'roleWrite:Administrator', + defaultACL: { + 'role:Administrator': { + write: true, + }, + }, }); const obj = new Parse.Object('TestObject'); obj.set('foo', 'bar'); @@ -1048,7 +1075,12 @@ describe('Parse.ACL', () => { it('defaultACL roleReadWrite', async function (done) { await reconfigureServer({ - defaultACL: 'roleReadWrite:Administrator', + defaultACL: { + 'role:Administrator': { + read: true, + write: true, + }, + }, }); const obj = new Parse.Object('TestObject'); obj.set('foo', 'bar'); @@ -1064,11 +1096,21 @@ describe('Parse.ACL', () => { it('defaultACL object readWrite', async function (done) { await reconfigureServer({ defaultACL: { - TestObject: { - public: 'readWrite', - private: 'readWrite', - 'role:Administrator': 'readWrite', - customId: 'readWrite', + '*': { + read: true, + write: true, + }, + currentUser: { + read: true, + write: true, + }, + 'role:Administrator': { + read: true, + write: true, + }, + customId: { + read: true, + write: true, }, }, }); @@ -1092,11 +1134,17 @@ describe('Parse.ACL', () => { it('defaultACL object read', async function (done) { await reconfigureServer({ defaultACL: { - TestObject: { - public: 'read', - private: 'read', - 'role:Administrator': 'read', - customId: 'read', + '*': { + read: true, + }, + currentUser: { + read: true, + }, + 'role:Administrator': { + read: true, + }, + customId: { + read: true, }, }, }); @@ -1120,11 +1168,17 @@ describe('Parse.ACL', () => { it('defaultACL object write', async function (done) { await reconfigureServer({ defaultACL: { - TestObject: { - public: 'write', - private: 'write', - 'role:Administrator': 'write', - customId: 'write', + '*': { + write: true, + }, + currentUser: { + write: true, + }, + 'role:Administrator': { + write: true, + }, + customId: { + write: true, }, }, }); diff --git a/spec/helper.js b/spec/helper.js index 69de232304..ab6e617215 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -99,7 +99,12 @@ const defaultConfiguration = { apiKey: 'yolo', }, }, - defaultACL: 'publicReadWrite', + defaultACL: { + '*': { + read: true, + write: true, + }, + }, auth: { // Override the facebook provider custom: mockCustom(), diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index b11ee69985..160e6af744 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -113,7 +113,12 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_DEFAULT_ACL', help: 'Options for default ACL on classes', action: parsers.objectParser, - default: 'private', + default: { + currentUser: { + read: true, + write: true, + }, + }, }, directAccess: { env: 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS', diff --git a/src/Options/docs.js b/src/Options/docs.js index c64b2eea3a..7bf49405e4 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -20,7 +20,7 @@ * @property {Adapter} databaseAdapter Adapter module for the database * @property {Any} databaseOptions Options to pass to the mongodb client * @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres. - * @property {StringOrAny} defaultACL Options for default ACL on classes + * @property {ACLOptions} defaultACL Options for default ACL on classes * @property {Boolean} directAccess Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production. * @property {String} dotNetKey Key for Unity and .Net SDK * @property {Adapter} emailAdapter Adapter module for email sending diff --git a/src/Options/index.js b/src/Options/index.js index 1cdb878fed..aef2d0cc5a 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -11,8 +11,8 @@ import { WSSAdapter } from '../Adapters/WebSocketServer/WSSAdapter'; type Adapter = string | any | T; type NumberOrBoolean = number | boolean; type NumberOrString = number | string; -type StringOrAny = string | any; type ProtectedFields = any; +type ACLOptions = any; export interface ParseServerOptions { /* Your Parse Application ID @@ -201,8 +201,8 @@ export interface ParseServerOptions { idempotencyOptions: ?IdempotencyOptions; /* Options for default ACL on classes :ENV: PARSE_SERVER_DEFAULT_ACL - :DEFAULT: private */ - defaultACL: ?StringOrAny; + :DEFAULT: {'currentUser':{'read':true,'write':true}} */ + defaultACL: ?ACLOptions; /* Options for file uploads :ENV: PARSE_SERVER_FILE_UPLOAD_OPTIONS */ fileUpload: ?FileUploadOptions; diff --git a/src/RestWrite.js b/src/RestWrite.js index 4f41c2bea6..f4f975517c 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -105,10 +105,10 @@ RestWrite.prototype.execute = function () { return this.validateAuthData(); }) .then(() => { - return this.runBeforeSaveTrigger(); + return this.buildDefaultACL(); }) .then(() => { - return this.buildDefaultACL(); + return this.runBeforeSaveTrigger(); }) .then(() => { return this.deleteEmailResetTokenIfNeeded(); @@ -202,120 +202,22 @@ RestWrite.prototype.validateSchema = function () { // builds the default ACL depending on the className RestWrite.prototype.buildDefaultACL = function () { - if (this.data.ACL || (this.originalData && this.originalData.ACL)) { + if ((this.query && this.query.objectId) || this.data.ACL) { return; } - let aclOptions = this.config.defaultACL || 'private'; - const getRoleName = roleStr => { - return aclOptions.split(`${roleStr}:`)[1]; + const defaultACL = { + currentUser: { + read: true, + write: true, + }, }; + const aclOptions = this.config.defaultACL || defaultACL; const reqUser = this.auth.user && this.auth.user.id; - const acl = new Parse.ACL(); - if (typeof aclOptions === 'string') { - if (aclOptions === 'publicReadWrite') { - return; - } - if (aclOptions === 'private') { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - } else if (aclOptions === 'publicRead') { - acl.setPublicReadAccess(true); - acl.setPublicWriteAccess(false); - } else if (aclOptions === 'publicWrite') { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(true); - } else if (aclOptions.includes('roleRead:')) { - const roleName = getRoleName('roleRead'); - if (roleName) { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - acl.setRoleReadAccess(roleName, true); - acl.setRoleWriteAccess(roleName, false); - } - } else if (aclOptions.includes('roleWrite:')) { - const roleName = getRoleName('roleWrite'); - if (roleName) { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - acl.setRoleReadAccess(roleName, false); - acl.setRoleWriteAccess(roleName, true); - } - } else if (aclOptions.includes('roleReadWrite:')) { - const roleName = getRoleName('roleReadWrite'); - if (roleName) { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - acl.setRoleReadAccess(roleName, true); - acl.setRoleWriteAccess(roleName, true); - } - } - if (reqUser) { - acl.setReadAccess(reqUser, true); - acl.setWriteAccess(reqUser, true); - } - } else { - aclOptions = aclOptions[this.className] || aclOptions['*']; - if (!aclOptions) { - return; - } - if (aclOptions.public) { - if (aclOptions.public === 'readWrite') { - acl.setPublicReadAccess(true); - acl.setPublicWriteAccess(true); - } else if (aclOptions.public === 'read') { - acl.setPublicReadAccess(true); - acl.setPublicWriteAccess(false); - } else if (aclOptions.public === 'write') { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(true); - } - } else { - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - } - if (aclOptions['private'] && reqUser) { - if (aclOptions['private'] === 'readWrite') { - acl.setReadAccess(reqUser, true); - acl.setWriteAccess(reqUser, true); - } else if (aclOptions['private'] === 'read') { - acl.setReadAccess(reqUser, true); - acl.setWriteAccess(reqUser, false); - } else if (aclOptions['private'] === 'write') { - acl.setReadAccess(reqUser, false); - acl.setWriteAccess(reqUser, true); - } - } - delete aclOptions['private']; - delete aclOptions['public']; - for (const user in aclOptions) { - const aclValue = aclOptions[user]; - if (aclValue.includes('role:')) { - const roleName = getRoleName('role'); - if (aclValue === 'readWrite') { - acl.setRoleReadAccess(roleName, true); - acl.setRoleWriteAccess(roleName, true); - } else if (aclValue === 'read') { - acl.setRoleReadAccess(roleName, true); - acl.setRoleWriteAccess(roleName, false); - } else if (aclValue === 'write') { - acl.setRoleReadAccess(roleName, false); - acl.setRoleWriteAccess(roleName, true); - } - } else { - if (aclValue === 'readWrite') { - acl.setReadAccess(user, true); - acl.setWriteAccess(user, true); - } else if (aclValue === 'read') { - acl.setReadAccess(user, true); - acl.setWriteAccess(user, false); - } else if (aclValue === 'write') { - acl.setReadAccess(user, false); - acl.setWriteAccess(user, true); - } - } - } + if (reqUser && aclOptions.currentUser) { + aclOptions[reqUser] = aclOptions.currentUser; } - this.data.ACL = acl.toJSON(); + delete aclOptions.currentUser; + this.data.ACL = aclOptions; this.storage.fieldsChangedByTrigger = this.storage.fieldsChangedByTrigger || []; if (this.storage.fieldsChangedByTrigger.indexOf('ACL') < 0) { this.storage.fieldsChangedByTrigger.push('ACL'); From e3a1426d776e2e1adaa52f909dcc442401e68011 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 20:42:36 +1100 Subject: [PATCH 08/12] fix tests --- CHANGELOG.md | 2 +- spec/ParseACL.spec.js | 3 +-- src/RestWrite.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a634b0db..55130aff3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ __BREAKING CHANGES:__ - NEW: Added file upload restriction. File upload is now only allowed for authenticated users by default for improved security. To allow file upload also for Anonymous Users or Public, set the `fileUpload` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html). [#7071](https://github.com/parse-community/parse-server/pull/7071). Thanks to [dblythy](https://github.com/dblythy). -- NEW: Added default ACL options. Parse.Objects now default to Public Read and Write false for improved security. This only affects saving new Parse.Objects that do not have ACL set. To allow default public read and write, set the `defaultACL` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) to `publicReadWrite`. [#7111](https://github.com/parse-community/parse-server/pull/7111). Thanks to [dblythy](https://github.com/dblythy). +- NEW: Added default ACL options. New Parse.Objects now default to Public Read and Write false for improved security. This only affects saving new Parse.Objects that do not have ACL set. To allow default public read and write, set the `defaultACL` parameter in the [Parse Server Options](https://parseplatform.org/parse-server/api/master/ParseServerOptions.html) to `{'*':{'read':true,'write':true}}`. [#7111](https://github.com/parse-community/parse-server/pull/7111). Thanks to [dblythy](https://github.com/dblythy). ___ - IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz) - FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 4ae7f78440..48706d7da5 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -979,6 +979,7 @@ describe('Parse.ACL', () => { }); const user = await Parse.User.signUp('testuser', 'p@ssword'); const user2 = await Parse.User.signUp('testuser2', 'p@ssword'); + Parse.User.logOut(); const obj = new Parse.Object('TestObject'); obj.set('foo', 'bar'); await obj.save(null, { sessionToken: user.getSessionToken() }); @@ -994,7 +995,6 @@ describe('Parse.ACL', () => { } catch (e) { expect(e.code).toBe(101); } - done(); }); @@ -1003,7 +1003,6 @@ describe('Parse.ACL', () => { defaultACL: { '*': { read: true, - write: true, }, }, }); diff --git a/src/RestWrite.js b/src/RestWrite.js index f4f975517c..4b39d24872 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -211,7 +211,7 @@ RestWrite.prototype.buildDefaultACL = function () { write: true, }, }; - const aclOptions = this.config.defaultACL || defaultACL; + const aclOptions = Object.assign({}, this.config.defaultACL || defaultACL); const reqUser = this.auth.user && this.auth.user.id; if (reqUser && aclOptions.currentUser) { aclOptions[reqUser] = aclOptions.currentUser; From 487e95eb259086f9c946faf611e32a478db7dfa7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 20:56:00 +1100 Subject: [PATCH 09/12] validate config --- spec/ParseACL.spec.js | 20 ++++++++++++++++++++ src/Config.js | 22 +++++++++++++++++----- src/RestWrite.js | 4 ++-- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 48706d7da5..e45d316857 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -947,6 +947,26 @@ describe('Parse.ACL', () => { expect(acl[user.id].read).toBeTrue(); }); + it('set default ACL to invalid', async () => { + try { + await reconfigureServer({ + defaultACL: 'foo', + }); + fail('should not have been able to set up invalid ACL string'); + } catch (e) { + expect(e).toBe('defaultACL must be an object'); + } + + try { + await reconfigureServer({ + defaultACL: { foo: ['bar', 'xyz'], ['']: [] }, + }); + fail('should not have been able to set up invalid ACL object'); + } catch (e) { + expect(e).toBe('Could not validate default ACL object. Error: invalid permission type.'); + } + }); + it('defaultACL private', async function (done) { await reconfigureServer({ defaultACL: { diff --git a/src/Config.js b/src/Config.js index 50a92b4b0f..0336d9cb23 100644 --- a/src/Config.js +++ b/src/Config.js @@ -6,10 +6,8 @@ import AppCache from './cache'; import SchemaCache from './Controllers/SchemaCache'; import DatabaseController from './Controllers/DatabaseController'; import net from 'net'; -import { - IdempotencyOptions, - FileUploadOptions, -} from './Options/Definitions'; +import Parse from 'parse/node'; +import { IdempotencyOptions, FileUploadOptions } from './Options/Definitions'; function removeTrailingSlash(str) { if (!str) { @@ -75,6 +73,7 @@ export class Config { idempotencyOptions, emailVerifyTokenReuseIfValid, fileUpload, + defaultACL, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); @@ -109,6 +108,7 @@ export class Config { this.validateMaxLimit(maxLimit); this.validateAllowHeaders(allowHeaders); this.validateIdempotencyOptions(idempotencyOptions); + this.validateDefaultACLOptions(defaultACL); } static validateIdempotencyOptions(idempotencyOptions) { @@ -128,7 +128,19 @@ export class Config { throw 'idempotency paths must be of an array of strings'; } } - + static validateDefaultACLOptions(aclOptions) { + if (!aclOptions) { + return; + } + if (typeof aclOptions !== 'object') { + throw 'defaultACL must be an object'; + } + try { + new Parse.ACL(aclOptions); + } catch (e) { + throw 'Could not validate default ACL object. Error: invalid permission type.'; + } + } static validateAccountLockoutPolicy(accountLockout) { if (accountLockout) { if ( diff --git a/src/RestWrite.js b/src/RestWrite.js index 4b39d24872..141d100f27 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -105,10 +105,10 @@ RestWrite.prototype.execute = function () { return this.validateAuthData(); }) .then(() => { - return this.buildDefaultACL(); + return this.runBeforeSaveTrigger(); }) .then(() => { - return this.runBeforeSaveTrigger(); + return this.buildDefaultACL(); }) .then(() => { return this.deleteEmailResetTokenIfNeeded(); From b60f8a36231b42aba9a3732466b9aeef298f5c1d Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 20:59:51 +1100 Subject: [PATCH 10/12] Update buildConfigDefinitions.js --- resources/buildConfigDefinitions.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index dcaf06f286..ad834a08ab 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -164,13 +164,6 @@ function parseDefaultValue(elt, value, t) { if (type == 'NumberOrBoolean') { literalValue = t.numericLiteral(parsers.numberOrBoolParser('')(value)); } - if (type == 'StringOrAny') { - if (value == '""' || value == "''") { - literalValue = t.stringLiteral(''); - } else { - literalValue = t.stringLiteral(value); - } - } const literalTypes = ['IdempotencyOptions','FileUploadOptions','CustomPagesOptions']; if (literalTypes.includes(type)) { const object = parsers.objectParser(value); From 5259957ff1db693c7b4d007f31a5e6824fc2da1f Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 21:16:16 +1100 Subject: [PATCH 11/12] Update RestWrite.js --- src/RestWrite.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/RestWrite.js b/src/RestWrite.js index 141d100f27..2190644a01 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -217,6 +217,13 @@ RestWrite.prototype.buildDefaultACL = function () { aclOptions[reqUser] = aclOptions.currentUser; } delete aclOptions.currentUser; + const publicACL = new Parse.ACL(); + publicACL.setPublicReadAccess(true); + publicACL.setPublicWriteAccess(true); + const newACL = new Parse.ACL(aclOptions); + if (publicACL.equals(newACL)) { + return; + } this.data.ACL = aclOptions; this.storage.fieldsChangedByTrigger = this.storage.fieldsChangedByTrigger || []; if (this.storage.fieldsChangedByTrigger.indexOf('ACL') < 0) { From fec2f5423b5aaf4a5a5933011d8d0b47ec52d525 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 6 Jan 2021 22:40:20 +1100 Subject: [PATCH 12/12] Update ParseACL.spec.js --- spec/ParseACL.spec.js | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index e45d316857..90bbc6529f 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -3,6 +3,7 @@ const rest = require('../lib/rest'); const Config = require('../lib/Config'); const auth = require('../lib/Auth'); +const request = require('../lib/request'); describe('Parse.ACL', () => { it('acl must be valid', done => { @@ -1217,4 +1218,53 @@ describe('Parse.ACL', () => { expect(acl['customId'].read).toBeUndefined(); done(); }); + it('works with Parse REST API', async () => { + await reconfigureServer({ + defaultACL: { + '*': { + read: true, + }, + currentUser: { + read: true, + }, + 'role:Administrator': { + read: true, + }, + customId: { + read: true, + }, + }, + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': user.getSessionToken(), + 'Content-Type': 'application/json', + }; + const { data } = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + body: { + foo: 'bar', + }, + json: true, + }); + const result = await request({ + method: 'GET', + headers: headers, + url: `http://localhost:8378/1/classes/TestObject/${data.objectId}`, + }); + const { ACL, objectId } = result.data; + expect(objectId).toBe(data.objectId); + expect(ACL['*'].read).toBe(true); + expect(ACL['*'].write).toBeUndefined(); + expect(ACL[user.id].read).toBe(true); + expect(ACL[user.id].write).toBeUndefined(); + expect(ACL['role:Administrator'].read).toBe(true); + expect(ACL['role:Administrator'].write).toBeUndefined(); + expect(ACL['customId'].read).toBe(true); + expect(ACL['customId'].write).toBeUndefined(); + }); });