From ca7cec8e51bcb2951edcd7f189e0b29396963b16 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 2 Nov 2020 03:25:50 +1100 Subject: [PATCH 01/10] Initial Commit --- spec/CloudCode.spec.js | 56 ++++++++++++++++++++++++ spec/cloud/cloudCodeRequireFiles.js | 2 + src/Routers/CloudCodeRouter.js | 66 +++++++++++++++++++++++++++++ src/Routers/FeaturesRouter.js | 1 + 4 files changed, 125 insertions(+) create mode 100644 spec/cloud/cloudCodeRequireFiles.js diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 995d09b6cc..706faece16 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -38,6 +38,62 @@ describe('Cloud Code', () => { }); }); }); + const masterKeyHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'rest', + 'X-Parse-Master-Key': 'test', + 'Content-Type': 'application/json', + }; + const masterKeyOptions = { + headers: masterKeyHeaders, + json: true, + }; + it('can load cloud code file from dashboard', async done => { + const cloudDir = './spec/cloud/cloudCodeAbsoluteFile.js'; + await reconfigureServer({ cloud: cloudDir }); + const options = Object.assign({}, masterKeyOptions, { + method: 'GET', + url: Parse.serverURL + '/releases/latest', + }); + request(options) + .then(res => { + expect(Array.isArray(res.data)).toBe(true); + const first = res.data[0]; + expect(first.userFiles).toBeDefined(); + expect(first.checksums).toBeDefined(); + expect(first.userFiles).toContain(cloudDir); + expect(first.checksums).toContain(cloudDir); + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeAbsoluteFile.js'; + return request(options); + }) + .then(res => { + const response = res.data; + expect(response).toContain('It is possible to define cloud code in a file.'); + done(); + }); + }); + + it('can load multiple cloud code files from dashboard', async done => { + const cloudDir = './spec/cloud/cloudCodeRequireFiles.js'; + await reconfigureServer({ cloud: cloudDir }); + const options = Object.assign({}, masterKeyOptions, { + method: 'GET', + url: Parse.serverURL + '/releases/latest', + }); + request(options).then(res => { + expect(Array.isArray(res.data)).toBe(true); + const first = res.data[0]; + expect(first.userFiles).toBeDefined(); + expect(first.checksums).toBeDefined(); + expect(first.userFiles).toContain(cloudDir); + expect(first.checksums).toContain(cloudDir); + expect(first.userFiles).toContain('spec/cloud/cloudCodeAbsoluteFile.js'); + expect(first.checksums).toContain('spec/cloud/cloudCodeAbsoluteFile.js'); + expect(first.userFiles).toContain('spec/cloud/cloudCodeRelativeFile.js'); + expect(first.checksums).toContain('spec/cloud/cloudCodeRelativeFile.js'); + done(); + }); + }); it('can create functions', done => { Parse.Cloud.define('hello', () => { diff --git a/spec/cloud/cloudCodeRequireFiles.js b/spec/cloud/cloudCodeRequireFiles.js new file mode 100644 index 0000000000..086b63c525 --- /dev/null +++ b/spec/cloud/cloudCodeRequireFiles.js @@ -0,0 +1,2 @@ +require('./cloudCodeAbsoluteFile.js'); +require('./cloudCodeRelativeFile.js'); diff --git a/src/Routers/CloudCodeRouter.js b/src/Routers/CloudCodeRouter.js index 327353123a..34727c97ec 100644 --- a/src/Routers/CloudCodeRouter.js +++ b/src/Routers/CloudCodeRouter.js @@ -3,6 +3,8 @@ import Parse from 'parse/node'; import rest from '../rest'; const triggers = require('../triggers'); const middleware = require('../middlewares'); +const fs = require('fs'); +const path = require('path'); function formatJobSchedule(job_schedule) { if (typeof job_schedule.startAfter === 'undefined') { @@ -53,6 +55,18 @@ export class CloudCodeRouter extends PromiseRouter { middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.deleteJob ); + this.route( + 'GET', + '/releases/latest', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.getCloudCode + ); + this.route( + 'GET', + '/scripts/*', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.getCloudFile + ); } static getJobs(req) { @@ -120,4 +134,56 @@ export class CloudCodeRouter extends PromiseRouter { }; }); } + static getCloudFile(req) { + const file = req.url.replace('/scripts', ''); + const dirName = __dirname.split('lib')[0]; + const filePath = path.join(dirName, file); + if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid file url.'); + } + return { + response: fs.readFileSync(filePath, 'utf8'), + }; + } + static getCloudCode(req) { + const config = req.config; + const dirName = __dirname.split('node_modules')[0]; + const cloudLocation = ('' + config.cloud).replace(dirName, ''); + const cloudFiles = []; + const getRequiredFromFile = (file, directory) => { + try { + const fileData = fs.readFileSync(file, 'utf8'); + const requireStatements = fileData.split('require('); + for (let reqStatement of requireStatements) { + reqStatement = reqStatement.split(')')[0].slice(1, -1); + const filePath = path.join(directory, reqStatement); + if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) { + continue; + } + const requireData = fs.readFileSync(filePath, 'utf8'); + const newFilePath = filePath.replace(dirName, ''); + cloudFiles.push(newFilePath); + if (requireData.includes('require(')) { + getRequiredFromFile(newFilePath, path.dirname(filePath)); + } + } + } catch (e) { + /* */ + } + }; + cloudFiles.push(cloudLocation); + getRequiredFromFile(cloudLocation, path.dirname(config.cloud)); + const response = {}; + for (const file of cloudFiles) { + response[file] = new Date(); + } + return { + response: [ + { + checksums: JSON.stringify({ cloud: response }), + userFiles: JSON.stringify({ cloud: response }), + }, + ], + }; + } } diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index 649cefcb3a..a70928bab0 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -21,6 +21,7 @@ export class FeaturesRouter extends PromiseRouter { }, cloudCode: { jobs: true, + viewCode: true, }, logs: { level: true, From 6309d470caffdc0e9dbe36d880978b9b34e251f4 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 2 Nov 2020 03:30:59 +1100 Subject: [PATCH 02/10] Update CloudCodeRouter.js --- src/Routers/CloudCodeRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Routers/CloudCodeRouter.js b/src/Routers/CloudCodeRouter.js index 34727c97ec..befff754da 100644 --- a/src/Routers/CloudCodeRouter.js +++ b/src/Routers/CloudCodeRouter.js @@ -136,7 +136,7 @@ export class CloudCodeRouter extends PromiseRouter { } static getCloudFile(req) { const file = req.url.replace('/scripts', ''); - const dirName = __dirname.split('lib')[0]; + const dirName = __dirname.split('lib')[0].split('node_modules')[0]; const filePath = path.join(dirName, file); if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) { throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Invalid file url.'); From 0ca8e369d9dab8b8e92b66cc646de9db430cee23 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 2 Nov 2020 14:23:31 +1100 Subject: [PATCH 03/10] Allow saving cloud files from Dashboard --- spec/CloudCode.spec.js | 45 ++++++++++++++++++++++++++++++++++ src/Routers/CloudCodeRouter.js | 19 ++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 706faece16..62990804cc 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -95,6 +95,51 @@ describe('Cloud Code', () => { }); }); + it('can edit cloud code file from dashboard', async done => { + const cloudDir = './spec/cloud/cloudCodeRequireFiles.js'; + await reconfigureServer({ cloud: cloudDir }); + const options = Object.assign({}, masterKeyOptions, { + method: 'GET', + url: Parse.serverURL + '/releases/latest', + }); + let originalFile = ''; + request(options) + .then(res => { + expect(Array.isArray(res.data)).toBe(true); + const first = res.data[0]; + expect(first.userFiles).toBeDefined(); + expect(first.checksums).toBeDefined(); + expect(first.userFiles).toContain(cloudDir); + expect(first.checksums).toContain(cloudDir); + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + return request(options); + }) + .then(res => { + originalFile = res.data; + let response = res.data; + expect(response).toContain(`require('./cloudCodeAbsoluteFile.js`); + response = response + '\nconst additionalData;\n'; + options.method = 'POST'; + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + options.body = { + data: response, + }; + return request(options); + }) + .then(res => { + expect(res.data).toBe('This file has been saved.'); + options.method = 'POST'; + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + options.body = { + data: originalFile, + }; + return request(options); + }) + .then(() => { + done(); + }); + }); + it('can create functions', done => { Parse.Cloud.define('hello', () => { return 'Hello world!'; diff --git a/src/Routers/CloudCodeRouter.js b/src/Routers/CloudCodeRouter.js index befff754da..6fb2f1dc66 100644 --- a/src/Routers/CloudCodeRouter.js +++ b/src/Routers/CloudCodeRouter.js @@ -67,6 +67,12 @@ export class CloudCodeRouter extends PromiseRouter { middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.getCloudFile ); + this.route( + 'POST', + '/scripts/*', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.saveCloudFile + ); } static getJobs(req) { @@ -134,6 +140,19 @@ export class CloudCodeRouter extends PromiseRouter { }; }); } + static saveCloudFile(req) { + const file = req.url.replace('/scripts', ''); + const dirName = __dirname.split('lib')[0].split('node_modules')[0]; + const filePath = path.join(dirName, file); + const data = req.body.data; + if (!data) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No data to save.'); + } + fs.writeFileSync(filePath, data); + return { + response: 'This file has been saved.', + }; + } static getCloudFile(req) { const file = req.url.replace('/scripts', ''); const dirName = __dirname.split('lib')[0].split('node_modules')[0]; From 35ae973b5d2bf04fdc9e6fb5f3392d4b48a5134b Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 4 Feb 2021 18:46:48 +1100 Subject: [PATCH 04/10] disable by default --- resources/buildConfigDefinitions.js | 10 ++ spec/CloudCode.spec.js | 133 +++++++++++++++++++++++- spec/GridFSBucketStorageAdapter.spec.js | 88 ++++------------ src/Options/Definitions.js | 22 ++++ src/Options/docs.js | 7 ++ src/Options/index.js | 13 +++ src/Routers/CloudCodeRouter.js | 16 ++- src/Routers/FeaturesRouter.js | 3 +- 8 files changed, 217 insertions(+), 75 deletions(-) diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index a640e1c3c7..d3ab4c1e67 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -55,6 +55,9 @@ function getENVPrefix(iface) { if (iface.id.name === 'IdempotencyOptions') { return 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_'; } + if (iface.id.name === 'DashboardOptions') { + return 'PARSE_SERVER_DASHBOARD_OPTIONS_'; + } } function processProperty(property, iface) { @@ -180,6 +183,13 @@ function parseDefaultValue(elt, value, t) { }); literalValue = t.objectExpression(props); } + if (type == 'DashboardOptions') { + const object = parsers.objectParser(value); + const props = Object.keys(object).map((key) => { + return t.objectProperty(key, object[value]); + }); + literalValue = t.objectExpression(props); + } if (type == 'ProtectedFields') { const prop = t.objectProperty( t.stringLiteral('_User'), t.objectPattern([ diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 62990804cc..2caba7e521 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -50,7 +50,12 @@ describe('Cloud Code', () => { }; it('can load cloud code file from dashboard', async done => { const cloudDir = './spec/cloud/cloudCodeAbsoluteFile.js'; - await reconfigureServer({ cloud: cloudDir }); + await reconfigureServer({ + cloud: cloudDir, + dashboardOptions: { + cloudFileView: true, + }, + }); const options = Object.assign({}, masterKeyOptions, { method: 'GET', url: Parse.serverURL + '/releases/latest', @@ -75,7 +80,12 @@ describe('Cloud Code', () => { it('can load multiple cloud code files from dashboard', async done => { const cloudDir = './spec/cloud/cloudCodeRequireFiles.js'; - await reconfigureServer({ cloud: cloudDir }); + await reconfigureServer({ + cloud: cloudDir, + dashboardOptions: { + cloudFileView: true, + }, + }); const options = Object.assign({}, masterKeyOptions, { method: 'GET', url: Parse.serverURL + '/releases/latest', @@ -95,9 +105,126 @@ describe('Cloud Code', () => { }); }); + it('can server info for for file options', async () => { + const cloudDir = './spec/cloud/cloudCodeRequireFiles.js'; + await reconfigureServer({ + cloud: cloudDir, + }); + const options = Object.assign({}, masterKeyOptions, { + method: 'GET', + url: Parse.serverURL + '/serverInfo', + }); + let { data } = await request(options); + expect(data).not.toBe(null); + expect(data.features).not.toBe(null); + expect(data.features.cloudCode).not.toBe(null); + expect(data.features.cloudCode.viewCode).toBe(false); + expect(data.features.cloudCode.editCode).toBe(false); + + await reconfigureServer({ + cloud: cloudDir, + dashboardOptions: { + cloudFileView: true, + }, + }); + data = (await request(options)).data; + expect(data).not.toBe(null); + expect(data.features).not.toBe(null); + expect(data.features.cloudCode).not.toBe(null); + expect(data.features.cloudCode.viewCode).toBe(true); + expect(data.features.cloudCode.editCode).toBe(false); + await reconfigureServer({ + cloud: cloudDir, + dashboardOptions: { + cloudFileView: true, + cloudFileEdit: true, + }, + }); + data = (await request(options)).data; + expect(data).not.toBe(null); + expect(data.features).not.toBe(null); + expect(data.features.cloudCode).not.toBe(null); + expect(data.features.cloudCode.viewCode).toBe(true); + expect(data.features.cloudCode.editCode).toBe(true); + }); + + it('cannot view or edit cloud files by default', async () => { + const options = Object.assign({}, masterKeyOptions, { + method: 'GET', + url: Parse.serverURL + '/releases/latest', + }); + try { + await request(options); + fail('should not have been able to get cloud files'); + } catch (e) { + expect(e.text).toBe('{"code":101,"error":"Dashboard file viewing is not active."}'); + } + try { + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + await request(options); + fail('should not have been able to get cloud files'); + } catch (e) { + expect(e.text).toBe('{"code":101,"error":"Dashboard file viewing is not active."}'); + } + try { + options.method = 'POST'; + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + options.body = { + data: 'new file text', + }; + await request(options); + fail('should not have been able to get cloud files'); + } catch (e) { + expect(e.text).toBe('{"code":101,"error":"Dashboard file editing is not active."}'); + } + }); + + it('can view cloud code file from dashboard', async () => { + const cloudDir = './spec/cloud/cloudCodeRequireFiles.js'; + await reconfigureServer({ + cloud: cloudDir, + dashboardOptions: { + cloudFileView: true, + }, + }); + const options = Object.assign({}, masterKeyOptions, { + method: 'GET', + url: Parse.serverURL + '/releases/latest', + }); + let res = await request(options); + expect(Array.isArray(res.data)).toBe(true); + const first = res.data[0]; + expect(first.userFiles).toBeDefined(); + expect(first.checksums).toBeDefined(); + expect(first.userFiles).toContain(cloudDir); + expect(first.checksums).toContain(cloudDir); + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + res = await request(options); + let response = res.data; + expect(response).toContain(`require('./cloudCodeAbsoluteFile.js`); + response = response + '\nconst additionalData;\n'; + options.method = 'POST'; + options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js'; + options.body = { + data: response, + }; + try { + await request(options); + fail('should have failed to save'); + } catch (e) { + expect(e.text).toBe('{"code":101,"error":"Dashboard file editing is not active."}'); + } + }); + it('can edit cloud code file from dashboard', async done => { const cloudDir = './spec/cloud/cloudCodeRequireFiles.js'; - await reconfigureServer({ cloud: cloudDir }); + await reconfigureServer({ + cloud: cloudDir, + dashboardOptions: { + cloudFileView: true, + cloudFileEdit: true, + }, + }); const options = Object.assign({}, masterKeyOptions, { method: 'GET', url: Parse.serverURL + '/releases/latest', diff --git a/spec/GridFSBucketStorageAdapter.spec.js b/spec/GridFSBucketStorageAdapter.spec.js index 3b8c8016e9..92f7aae388 100644 --- a/spec/GridFSBucketStorageAdapter.spec.js +++ b/spec/GridFSBucketStorageAdapter.spec.js @@ -44,9 +44,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { await expectMissingFile(encryptedAdapter, 'myFileName'); const originalString = 'abcdefghi'; await encryptedAdapter.createFile('myFileName', originalString); - const unencryptedResult = await unencryptedAdapter.getFileData( - 'myFileName' - ); + const unencryptedResult = await unencryptedAdapter.getFileData('myFileName'); expect(unencryptedResult.toString('utf8')).not.toBe(originalString); const encryptedResult = await encryptedAdapter.getFileData('myFileName'); expect(encryptedResult.toString('utf8')).toBe(originalString); @@ -71,10 +69,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const unencryptedResult2 = await unencryptedAdapter.getFileData(fileName2); expect(unencryptedResult2.toString('utf8')).toBe(data2); //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter - const { - rotated, - notRotated, - } = await encryptedAdapter.rotateEncryptionKey(); + const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey(); expect(rotated.length).toEqual(2); expect( rotated.filter(function (value) { @@ -101,30 +96,18 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { it('should rotate key of all old encrypted GridFS files to encrypted files', async () => { const oldEncryptionKey = 'oldKeyThatILoved'; - const oldEncryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - oldEncryptionKey - ); - const encryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - 'newKeyThatILove' - ); + const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey); + const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove'); const fileName1 = 'file1.txt'; const data1 = 'hello world'; const fileName2 = 'file2.txt'; const data2 = 'hello new world'; //Store unecrypted files await oldEncryptedAdapter.createFile(fileName1, data1); - const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( - fileName1 - ); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1); expect(oldEncryptedResult1.toString('utf8')).toBe(data1); await oldEncryptedAdapter.createFile(fileName2, data2); - const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( - fileName2 - ); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2); expect(oldEncryptedResult2.toString('utf8')).toBe(data2); //Check if encrypted adapter can read data and make sure it's not the same as unEncrypted adapter const { rotated, notRotated } = await encryptedAdapter.rotateEncryptionKey({ @@ -170,11 +153,7 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { it('should rotate key of all old encrypted GridFS files to unencrypted files', async () => { const oldEncryptionKey = 'oldKeyThatILoved'; - const oldEncryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - oldEncryptionKey - ); + const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey); const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); const fileName1 = 'file1.txt'; const data1 = 'hello world'; @@ -182,20 +161,13 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const data2 = 'hello new world'; //Store unecrypted files await oldEncryptedAdapter.createFile(fileName1, data1); - const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( - fileName1 - ); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1); expect(oldEncryptedResult1.toString('utf8')).toBe(data1); await oldEncryptedAdapter.createFile(fileName2, data2); - const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( - fileName2 - ); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2); expect(oldEncryptedResult2.toString('utf8')).toBe(data2); //Check if unEncrypted adapter can read data and make sure it's not the same as oldEncrypted adapter - const { - rotated, - notRotated, - } = await unEncryptedAdapter.rotateEncryptionKey({ + const { rotated, notRotated } = await unEncryptedAdapter.rotateEncryptionKey({ oldKey: oldEncryptionKey, }); expect(rotated.length).toEqual(2); @@ -238,16 +210,8 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { it('should only encrypt specified fileNames', async () => { const oldEncryptionKey = 'oldKeyThatILoved'; - const oldEncryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - oldEncryptionKey - ); - const encryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - 'newKeyThatILove' - ); + const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey); + const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove'); const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); const fileName1 = 'file1.txt'; const data1 = 'hello world'; @@ -255,14 +219,10 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const data2 = 'hello new world'; //Store unecrypted files await oldEncryptedAdapter.createFile(fileName1, data1); - const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( - fileName1 - ); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1); expect(oldEncryptedResult1.toString('utf8')).toBe(data1); await oldEncryptedAdapter.createFile(fileName2, data2); - const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( - fileName2 - ); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2); expect(oldEncryptedResult2.toString('utf8')).toBe(data2); //Inject unecrypted file to see if causes an issue const fileName3 = 'file3.txt'; @@ -318,16 +278,8 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { it("should return fileNames of those it can't encrypt with the new key", async () => { const oldEncryptionKey = 'oldKeyThatILoved'; - const oldEncryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - oldEncryptionKey - ); - const encryptedAdapter = new GridFSBucketAdapter( - databaseURI, - {}, - 'newKeyThatILove' - ); + const oldEncryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, oldEncryptionKey); + const encryptedAdapter = new GridFSBucketAdapter(databaseURI, {}, 'newKeyThatILove'); const unEncryptedAdapter = new GridFSBucketAdapter(databaseURI); const fileName1 = 'file1.txt'; const data1 = 'hello world'; @@ -335,14 +287,10 @@ describe_only_db('mongo')('GridFSBucket and GridStore interop', () => { const data2 = 'hello new world'; //Store unecrypted files await oldEncryptedAdapter.createFile(fileName1, data1); - const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData( - fileName1 - ); + const oldEncryptedResult1 = await oldEncryptedAdapter.getFileData(fileName1); expect(oldEncryptedResult1.toString('utf8')).toBe(data1); await oldEncryptedAdapter.createFile(fileName2, data2); - const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData( - fileName2 - ); + const oldEncryptedResult2 = await oldEncryptedAdapter.getFileData(fileName2); expect(oldEncryptedResult2.toString('utf8')).toBe(data2); //Inject unecrypted file to see if causes an issue const fileName3 = 'file3.txt'; diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index c3c1271786..7879c57de7 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -93,6 +93,13 @@ module.exports.ParseServerOptions = { action: parsers.objectParser, default: {}, }, + dashboardOptions: { + env: 'PARSE_SERVER_DASHBOARD_OPTIONS', + help: + 'Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server.', + action: parsers.objectParser, + default: {}, + }, databaseAdapter: { env: 'PARSE_SERVER_DATABASE_ADAPTER', help: 'Adapter module for the database', @@ -545,3 +552,18 @@ module.exports.IdempotencyOptions = { default: 300, }, }; +module.exports.DashboardOptions = { + cloudFileEdit: { + env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_EDIT', + help: + 'Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent.', + action: parsers.booleanParser, + default: false, + }, + cloudFileView: { + env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_VIEW', + help: 'Whether the Parse Dashboard can view cloud files.', + action: parsers.booleanParser, + default: false, + }, +}; diff --git a/src/Options/docs.js b/src/Options/docs.js index febe3d77cc..d555fd695b 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -17,6 +17,7 @@ * @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length * @property {String} collectionPrefix A collection prefix for the classes * @property {CustomPagesOptions} customPages custom pages for password validation and reset + * @property {DashboardOptions} dashboardOptions Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server. * @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. @@ -118,3 +119,9 @@ * @property {String[]} paths 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. * @property {Number} ttl The duration in seconds after which a request record is discarded from the database, defaults to 300s. */ + +/** + * @interface DashboardOptions + * @property {Boolean} cloudFileEdit Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent. + * @property {Boolean} cloudFileView Whether the Parse Dashboard can view cloud files. + */ diff --git a/src/Options/index.js b/src/Options/index.js index 1283f849a7..33f207be63 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -192,6 +192,10 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS :DEFAULT: false */ idempotencyOptions: ?IdempotencyOptions; + /* Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server. + :ENV: PARSE_SERVER_DASHBOARD_OPTIONS + :DEFAULT: false */ + dashboardOptions: ?DashboardOptions; /* Full path to your GraphQL custom schema.graphql file */ graphQLSchema: ?string; /* Mounts the GraphQL endpoint @@ -285,3 +289,12 @@ export interface IdempotencyOptions { :DEFAULT: 300 */ ttl: ?number; } + +export interface DashboardOptions { + /* Whether the Parse Dashboard can view cloud files. + :DEFAULT: false */ + cloudFileView: ?boolean; + /* Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent. + :DEFAULT: false */ + cloudFileEdit: ?boolean; +} diff --git a/src/Routers/CloudCodeRouter.js b/src/Routers/CloudCodeRouter.js index 6fb2f1dc66..87d8c4fffb 100644 --- a/src/Routers/CloudCodeRouter.js +++ b/src/Routers/CloudCodeRouter.js @@ -141,6 +141,11 @@ export class CloudCodeRouter extends PromiseRouter { }); } static saveCloudFile(req) { + const config = req.config || {}; + const DashboardOptions = config.dashboardOptions || {}; + if (!DashboardOptions.cloudFileEdit) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Dashboard file editing is not active.'); + } const file = req.url.replace('/scripts', ''); const dirName = __dirname.split('lib')[0].split('node_modules')[0]; const filePath = path.join(dirName, file); @@ -154,6 +159,11 @@ export class CloudCodeRouter extends PromiseRouter { }; } static getCloudFile(req) { + const config = req.config || {}; + const DashboardOptions = config.dashboardOptions || {}; + if (!(DashboardOptions.cloudFileView || DashboardOptions.cloudFileEdit)) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Dashboard file viewing is not active.'); + } const file = req.url.replace('/scripts', ''); const dirName = __dirname.split('lib')[0].split('node_modules')[0]; const filePath = path.join(dirName, file); @@ -165,7 +175,11 @@ export class CloudCodeRouter extends PromiseRouter { }; } static getCloudCode(req) { - const config = req.config; + const config = req.config || {}; + const DashboardOptions = config.dashboardOptions || {}; + if (!(DashboardOptions.cloudFileView || DashboardOptions.cloudFileEdit)) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Dashboard file viewing is not active.'); + } const dirName = __dirname.split('node_modules')[0]; const cloudLocation = ('' + config.cloud).replace(dirName, ''); const cloudFiles = []; diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index a70928bab0..67750437b7 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -21,7 +21,8 @@ export class FeaturesRouter extends PromiseRouter { }, cloudCode: { jobs: true, - viewCode: true, + viewCode: (config.dashboardOptions || {}).cloudFileView || false, + editCode: (config.dashboardOptions || {}).cloudFileEdit || false, }, logs: { level: true, From 190349ece2ffe7d5c716c13653560a2aaa125367 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 4 Feb 2021 18:51:37 +1100 Subject: [PATCH 05/10] run definitions --- src/Options/Definitions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index e51693a758..ba6fae4c9e 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -579,7 +579,7 @@ module.exports.DashboardOptions = { action: parsers.booleanParser, default: false, }, -} +}; module.exports.AccountLockoutOptions = { duration: { env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION', From 8c04fe684a5d7572a8eec94380e40a0128c24b39 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 4 Feb 2021 18:58:47 +1100 Subject: [PATCH 06/10] Update CloudCodeRouter.js --- src/Routers/CloudCodeRouter.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Routers/CloudCodeRouter.js b/src/Routers/CloudCodeRouter.js index 87d8c4fffb..7695fcb880 100644 --- a/src/Routers/CloudCodeRouter.js +++ b/src/Routers/CloudCodeRouter.js @@ -142,8 +142,8 @@ export class CloudCodeRouter extends PromiseRouter { } static saveCloudFile(req) { const config = req.config || {}; - const DashboardOptions = config.dashboardOptions || {}; - if (!DashboardOptions.cloudFileEdit) { + const dashboardOptions = config.dashboardOptions || {}; + if (!dashboardOptions.cloudFileEdit) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Dashboard file editing is not active.'); } const file = req.url.replace('/scripts', ''); @@ -160,8 +160,8 @@ export class CloudCodeRouter extends PromiseRouter { } static getCloudFile(req) { const config = req.config || {}; - const DashboardOptions = config.dashboardOptions || {}; - if (!(DashboardOptions.cloudFileView || DashboardOptions.cloudFileEdit)) { + const dashboardOptions = config.dashboardOptions || {}; + if (!(dashboardOptions.cloudFileView || dashboardOptions.cloudFileEdit)) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Dashboard file viewing is not active.'); } const file = req.url.replace('/scripts', ''); @@ -176,8 +176,8 @@ export class CloudCodeRouter extends PromiseRouter { } static getCloudCode(req) { const config = req.config || {}; - const DashboardOptions = config.dashboardOptions || {}; - if (!(DashboardOptions.cloudFileView || DashboardOptions.cloudFileEdit)) { + const dashboardOptions = config.dashboardOptions || {}; + if (!(dashboardOptions.cloudFileView || dashboardOptions.cloudFileEdit)) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Dashboard file viewing is not active.'); } const dirName = __dirname.split('node_modules')[0]; From ed0322b09cdf06e1c02541d42003a3d067ff04a8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 6 Feb 2021 19:10:03 +1100 Subject: [PATCH 07/10] Update Definitions.js --- src/Options/Definitions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index ba6fae4c9e..55acf3708b 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -27,7 +27,6 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_ALLOW_HEADERS', help: 'Add headers to Access-Control-Allow-Headers', action: parsers.arrayParser, - }, allowOrigin: { env: 'PARSE_SERVER_ALLOW_ORIGIN', help: 'Sets the origin to Access-Control-Allow-Origin', @@ -569,7 +568,7 @@ module.exports.DashboardOptions = { cloudFileEdit: { env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_EDIT', help: - 'Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent.', + 'Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not use on multi-instance servers otherwise your cloud files will be inconsistent.', action: parsers.booleanParser, default: false, }, From 897e81b12b57dea57d769d0cd6f847e5ead9fb71 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 6 Feb 2021 19:10:34 +1100 Subject: [PATCH 08/10] Update docs.js --- src/Options/docs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/docs.js b/src/Options/docs.js index 8f99956594..c93d9fd9d2 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -125,7 +125,7 @@ /** * @interface DashboardOptions - * @property {Boolean} cloudFileEdit Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent. + * @property {Boolean} cloudFileEdit Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not use on multi-instance servers otherwise your cloud files will be inconsistent. * @property {Boolean} cloudFileView Whether the Parse Dashboard can view cloud files. */ From d4780cd178e04836dcb0dc48c129bb5014eb9749 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 6 Feb 2021 19:11:04 +1100 Subject: [PATCH 09/10] Update index.js --- src/Options/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options/index.js b/src/Options/index.js index 7717ee92ac..2437f25a32 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -304,7 +304,7 @@ export interface DashboardOptions { /* Whether the Parse Dashboard can view cloud files. :DEFAULT: false */ cloudFileView: ?boolean; - /* Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not user on multi-instance servers otherwise your cloud files will be inconsistent. + /* Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not use on multi-instance servers otherwise your cloud files will be inconsistent. :DEFAULT: false */ cloudFileEdit: ?boolean; } From 1db9c75ffc6ea764970c1bde10c3406cf07e9f6a Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 6 Feb 2021 22:47:59 +1100 Subject: [PATCH 10/10] Update Definitions.js --- src/Options/Definitions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 55acf3708b..38e2490ea5 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -27,6 +27,7 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_ALLOW_HEADERS', help: 'Add headers to Access-Control-Allow-Headers', action: parsers.arrayParser, + }, allowOrigin: { env: 'PARSE_SERVER_ALLOW_ORIGIN', help: 'Sets the origin to Access-Control-Allow-Origin',