From cd4d4404ea86e63e33d733d5ef1da6d80f935f08 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 1 Feb 2021 00:35:18 +1100 Subject: [PATCH 1/6] new: allow options to be async on Cloud Validator --- spec/CloudCode.Validator.spec.js | 74 ++++++++++++++++++++++++++++++++ src/triggers.js | 14 +++--- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index d15bc2479d..1450f2dca8 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -1266,4 +1266,78 @@ describe('cloud validator', () => { done(); } }); + + it('set params options function async', async () => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + required: true, + options: async val => { + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + return val === 'f'; + }, + error: 'Validation failed.', + }, + }, + } + ); + try { + await Parse.Cloud.run('hello', { data: 'd' }); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Validation failed.'); + } + await Parse.Cloud.run('hello', { data: 'f' }); + }); + + it('basic beforeSave requireUserKey as custom async function', async () => { + Parse.Cloud.beforeSave(Parse.User, () => {}, { + fields: { + accType: { + default: 'normal', + constant: true, + }, + }, + }); + Parse.Cloud.define( + 'secureFunction', + () => { + return "Here's all the secure data!"; + }, + { + requireUserKeys: { + accType: { + options: async val => { + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + return ['admin', 'admin2'].includes(val); + }, + error: 'Unauthorized.', + }, + }, + } + ); + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'p@ssword'); + user.set('accType', 'admin'); + await user.signUp(); + expect(user.get('accType')).toBe('normal'); + try { + await Parse.Cloud.run('secureFunction'); + fail('function should only be available to admin users'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Unauthorized.'); + } + }); }); diff --git a/src/triggers.js b/src/triggers.js index 47331675b0..6318e339df 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -617,7 +617,7 @@ export function maybeRunValidator(request, functionName) { }); }); } -function builtInTriggerValidator(options, request) { +async function builtInTriggerValidator(options, request) { if (request.master && !options.validateMasterKey) { return; } @@ -647,11 +647,11 @@ function builtInTriggerValidator(options, request) { } }; - const validateOptions = (opt, key, val) => { + const validateOptions = async (opt, key, val) => { let opts = opt.options; if (typeof opts === 'function') { try { - const result = opts(val); + const result = await opts(val); if (!result && result != null) { throw opt.error || `Validation failed. Invalid value for ${key}.`; } @@ -684,6 +684,7 @@ function builtInTriggerValidator(options, request) { requiredParam(key); } } else { + const optionPromises = []; for (const key in options.fields) { const opt = options.fields[key]; let val = params[key]; @@ -717,10 +718,11 @@ function builtInTriggerValidator(options, request) { } } if (opt.options) { - validateOptions(opt, key, val); + optionPromises.push(validateOptions(opt, key, val)); } } } + await Promise.all(optionPromises); } const userKeys = options.requireUserKeys || []; if (Array.isArray(userKeys)) { @@ -734,12 +736,14 @@ function builtInTriggerValidator(options, request) { } } } else if (typeof userKeys === 'object') { + const optionPromises = []; for (const key in options.requireUserKeys) { const opt = options.requireUserKeys[key]; if (opt.options) { - validateOptions(opt, key, reqUser.get(key)); + optionPromises.push(validateOptions(opt, key, reqUser.get(key))); } } + await Promise.all(optionPromises); } } From 123898d63ca2b0e526f6f1f0e19ff94cfa7a22cd Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 1 Feb 2021 00:38:44 +1100 Subject: [PATCH 2/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a12739f78..706b9dd28d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,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). ___ +- IMPROVE: Allow Cloud Validator `options` to be async [#7155](https://github.com/parse-community/parse-server/pull/7155). 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) - NEW: Added convenience method Parse.Cloud.sendEmail(...) to send email via email adapter in Cloud Code. [#7089](https://github.com/parse-community/parse-server/pull/7089). Thanks to [dblythy](https://github.com/dblythy) From f1394a93c2500344b917d8a6dd56903458077b5e Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sat, 20 Feb 2021 15:17:15 -0600 Subject: [PATCH 3/6] Ensure pushStatus is properly running (#7213) * Ensure pushStatus is properly running * remove duplicate test --- spec/PushController.spec.js | 13 ------------- spec/PushWorker.spec.js | 2 ++ src/StatusHandler.js | 3 +-- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 2468ab2123..251f242230 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -79,19 +79,6 @@ describe('PushController', () => { done(); }); - it('can throw on validateDeviceType when single invalid device type is set', done => { - // Make query condition - const where = { - deviceType: 'osx', - }; - const validPushTypes = ['ios', 'android']; - - expect(function () { - validatePushType(where, validPushTypes); - }).toThrow(); - done(); - }); - it('can get expiration time in string format', done => { // Make mock request const timeStr = '2015-03-19T22:05:08Z'; diff --git a/spec/PushWorker.spec.js b/spec/PushWorker.spec.js index 6f9852091b..422cdf592a 100644 --- a/spec/PushWorker.spec.js +++ b/spec/PushWorker.spec.js @@ -314,6 +314,7 @@ describe('PushWorker', () => { amount: 1, }, count: { __op: 'Increment', amount: -1 }, + status: 'running', }); const query = new Parse.Query('_PushStatus'); return query.get(handler.objectId, { useMasterKey: true }); @@ -409,6 +410,7 @@ describe('PushWorker', () => { amount: 1, }, count: { __op: 'Increment', amount: -1 }, + status: 'running', }); done(); }); diff --git a/src/StatusHandler.js b/src/StatusHandler.js index 45010f3847..69b5af880e 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -295,9 +295,8 @@ export function pushStatusHandler(config, existingObjectId) { } ); } - - // indicate this batch is complete incrementOp(update, 'count', -1); + update.status = 'running'; return handler.update({ objectId }, update).then(res => { if (res && res.count === 0) { From 0fdad61edfdb2113871fb3d05a5d9cab5ec02fe9 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 1 Feb 2021 00:35:18 +1100 Subject: [PATCH 4/6] new: allow options to be async on Cloud Validator --- spec/CloudCode.Validator.spec.js | 74 ++++++++++++++++++++++++++++++++ src/triggers.js | 12 ++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 36a7fc96b1..43b9b760d8 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -1428,4 +1428,78 @@ describe('cloud validator', () => { done(); } }); + + it('set params options function async', async () => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + required: true, + options: async val => { + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + return val === 'f'; + }, + error: 'Validation failed.', + }, + }, + } + ); + try { + await Parse.Cloud.run('hello', { data: 'd' }); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Validation failed.'); + } + await Parse.Cloud.run('hello', { data: 'f' }); + }); + + it('basic beforeSave requireUserKey as custom async function', async () => { + Parse.Cloud.beforeSave(Parse.User, () => {}, { + fields: { + accType: { + default: 'normal', + constant: true, + }, + }, + }); + Parse.Cloud.define( + 'secureFunction', + () => { + return "Here's all the secure data!"; + }, + { + requireUserKeys: { + accType: { + options: async val => { + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + return ['admin', 'admin2'].includes(val); + }, + error: 'Unauthorized.', + }, + }, + } + ); + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'p@ssword'); + user.set('accType', 'admin'); + await user.signUp(); + expect(user.get('accType')).toBe('normal'); + try { + await Parse.Cloud.run('secureFunction'); + fail('function should only be available to admin users'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Unauthorized.'); + } + }); }); diff --git a/src/triggers.js b/src/triggers.js index a9f08052c3..0a9e234224 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -662,11 +662,11 @@ async function builtInTriggerValidator(options, request, auth) { } }; - const validateOptions = (opt, key, val) => { + const validateOptions = async (opt, key, val) => { let opts = opt.options; if (typeof opts === 'function') { try { - const result = opts(val); + const result = await opts(val); if (!result && result != null) { throw opt.error || `Validation failed. Invalid value for ${key}.`; } @@ -699,6 +699,7 @@ async function builtInTriggerValidator(options, request, auth) { requiredParam(key); } } else { + const optionPromises = []; for (const key in options.fields) { const opt = options.fields[key]; let val = params[key]; @@ -731,10 +732,11 @@ async function builtInTriggerValidator(options, request, auth) { } } if (opt.options) { - validateOptions(opt, key, val); + optionPromises.push(validateOptions(opt, key, val)); } } } + await Promise.all(optionPromises); } let userRoles = options.requireAnyUserRoles; let requireAllRoles = options.requireAllUserRoles; @@ -780,12 +782,14 @@ async function builtInTriggerValidator(options, request, auth) { } } } else if (typeof userKeys === 'object') { + const optionPromises = []; for (const key in options.requireUserKeys) { const opt = options.requireUserKeys[key]; if (opt.options) { - validateOptions(opt, key, reqUser.get(key)); + optionPromises.push(validateOptions(opt, key, reqUser.get(key))); } } + await Promise.all(optionPromises); } } From e33ce02d3ebfbc0d5cf3e1414638cbd09e6db304 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 1 Feb 2021 00:38:44 +1100 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffcb4e4e37..6d40d3f0d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ ___ - IMPROVE: Added new account lockout policy option `accountLockout.unlockOnPasswordReset` to automatically unlock account on password reset. [#7146](https://github.com/parse-community/parse-server/pull/7146). Thanks to [Manuel Trezza](https://github.com/mtrezza). - IMPROVE: Parse Server is from now on continuously tested against all recent MongoDB versions that have not reached their end-of-life support date. Added MongoDB compatibility table to Parse Server docs. [7161](https://github.com/parse-community/parse-server/pull/7161). Thanks to [Manuel Trezza](https://github.com/mtrezza). - IMPROVE: Parse Server is from now on continuously tested against all recent Node.js versions that have not reached their end-of-life support date. [7161](https://github.com/parse-community/parse-server/pull/7177). Thanks to [Manuel Trezza](https://github.com/mtrezza). +- IMPROVE: Allow Cloud Validator `options` to be async [#7155](https://github.com/parse-community/parse-server/pull/7155). 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) - IMPROVE: Parse Server will from now on be continuously tested against all relevant Postgres versions (minor versions). Added Postgres compatibility table to Parse Server docs. [#7176](https://github.com/parse-community/parse-server/pull/7176). Thanks to [Corey Baker](https://github.com/cbaker6). - FIX: Fix error when a not yet inserted job is updated [#7196](https://github.com/parse-community/parse-server/pull/7196). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). From 2bd44ba009ca47ec001345f6cc25ca3164db8898 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 21 Feb 2021 14:16:51 +1100 Subject: [PATCH 6/6] Update CloudCode.Validator.spec.js --- spec/CloudCode.Validator.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 43b9b760d8..2a1cd42796 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -1453,11 +1453,13 @@ describe('cloud validator', () => { ); try { await Parse.Cloud.run('hello', { data: 'd' }); + fail('validation should have failed'); } catch (error) { expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); expect(error.message).toEqual('Validation failed.'); } - await Parse.Cloud.run('hello', { data: 'f' }); + const result = await Parse.Cloud.run('hello', { data: 'f' }); + expect(result).toBe('Hello world!'); }); it('basic beforeSave requireUserKey as custom async function', async () => {