diff --git a/CHANGELOG.md b/CHANGELOG.md index 395ac39f82..aae19282fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,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: Throw error on invalid Cloud Function validation configuration. [#7154](https://github.com/parse-community/parse-server/pull/7154). 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) - 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). diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 2a1cd42796..0ee0debbae 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -92,9 +92,7 @@ describe('cloud validator', () => { }, async () => { await new Promise(resolve => { - setTimeout(() => { - resolve(); - }, 1000); + setTimeout(resolve, 1000); }); throw 'async error'; } @@ -132,7 +130,7 @@ describe('cloud validator', () => { await Parse.Cloud.run('myFunction'); }); - it('require user on cloud functions', done => { + it('require user on cloud functions', async done => { Parse.Cloud.define( 'hello1', () => { @@ -142,16 +140,14 @@ describe('cloud validator', () => { requireUser: true, } ); - - Parse.Cloud.run('hello1', {}) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual('Validation failed. Please login to continue.'); - done(); - }); + try { + await Parse.Cloud.run('hello1', {}); + fail('function should have failed.'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Validation failed. Please login to continue.'); + done(); + } }); it('require master on cloud functions', done => { @@ -605,16 +601,10 @@ describe('cloud validator', () => { expect(obj.get('foo')).toBe('bar'); const query = new Parse.Query('beforeFind'); - try { - const first = await query.first({ useMasterKey: true }); - expect(first).toBeDefined(); - expect(first.id).toBe(obj.id); - done(); - } catch (e) { - console.log(e); - console.log(e.code); - throw e; - } + const first = await query.first({ useMasterKey: true }); + expect(first).toBeDefined(); + expect(first.id).toBe(obj.id); + done(); }); it('basic beforeDelete skipWithMasterKey', async function (done) { @@ -1429,6 +1419,156 @@ describe('cloud validator', () => { } }); + it('does not log on valid config', () => { + Parse.Cloud.define('myFunction', () => {}, { + requireUser: true, + requireMaster: true, + validateMasterKey: false, + skipWithMasterKey: true, + requireUserKeys: { + Acc: { + constant: true, + options: ['A', 'B'], + required: true, + default: 'f', + error: 'a', + type: String, + }, + }, + fields: { + Acc: { + constant: true, + options: ['A', 'B'], + required: true, + default: 'f', + error: 'a', + type: String, + }, + }, + }); + }); + it('Logs on invalid config', () => { + const fields = [ + { + field: 'requiredUser', + value: true, + error: 'requiredUser is not a supported parameter for Cloud Function validations.', + }, + { + field: 'requireUser', + value: [], + error: + 'Invalid type for Cloud Function validation key requireUser. Expected boolean, actual array', + }, + { + field: 'requireMaster', + value: [], + error: + 'Invalid type for Cloud Function validation key requireMaster. Expected boolean, actual array', + }, + { + field: 'validateMasterKey', + value: [], + error: + 'Invalid type for Cloud Function validation key validateMasterKey. Expected boolean, actual array', + }, + { + field: 'skipWithMasterKey', + value: [], + error: + 'Invalid type for Cloud Function validation key skipWithMasterKey. Expected boolean, actual array', + }, + { + field: 'requireAllUserRoles', + value: true, + error: + 'Invalid type for Cloud Function validation key requireAllUserRoles. Expected array|function, actual boolean', + }, + { + field: 'requireAnyUserRoles', + value: true, + error: + 'Invalid type for Cloud Function validation key requireAnyUserRoles. Expected array|function, actual boolean', + }, + { + field: 'fields', + value: true, + error: + 'Invalid type for Cloud Function validation key fields. Expected array|object, actual boolean', + }, + { + field: 'requireUserKeys', + value: true, + error: + 'Invalid type for Cloud Function validation key requireUserKeys. Expected array|object, actual boolean', + }, + ]; + for (const field of fields) { + try { + Parse.Cloud.define('myFunction', () => {}, { + [field.field]: field.value, + }); + fail(`Expected error registering invalid Cloud Function validation ${field.field}.`); + } catch (e) { + expect(e).toBe(field.error); + } + } + }); + + it('Logs on invalid config', () => { + const fields = [ + { + field: 'otherKey', + value: true, + error: 'otherKey is not a supported parameter for Cloud Function validations.', + }, + { + field: 'constant', + value: [], + error: + 'Invalid type for Cloud Function validation key constant. Expected boolean, actual array', + }, + { + field: 'required', + value: [], + error: + 'Invalid type for Cloud Function validation key required. Expected boolean, actual array', + }, + { + field: 'error', + value: [], + error: + 'Invalid type for Cloud Function validation key error. Expected string, actual array', + }, + ]; + for (const field of fields) { + try { + Parse.Cloud.define('myFunction', () => {}, { + fields: { + name: { + [field.field]: field.value, + }, + }, + }); + fail(`Expected error registering invalid Cloud Function validation ${field.field}.`); + } catch (e) { + expect(e).toBe(field.error); + } + try { + Parse.Cloud.define('myFunction', () => {}, { + requireUserKeys: { + name: { + [field.field]: field.value, + }, + }, + }); + fail(`Expected error registering invalid Cloud Function validation ${field.field}.`); + } catch (e) { + expect(e).toBe(field.error); + } + } + }); + it('set params options function async', async () => { Parse.Cloud.define( 'hello', diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 5ed6aa728f..d16fe28ad4 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -13,6 +13,71 @@ function getClassName(parseClass) { return parseClass; } +function validateValidator(validator) { + if (!validator || typeof validator === 'function') { + return; + } + const fieldOptions = { + type: ['Any'], + constant: [Boolean], + default: ['Any'], + options: [Array, 'function', 'Any'], + required: [Boolean], + error: [String], + }; + const allowedKeys = { + requireUser: [Boolean], + requireAnyUserRoles: [Array, 'function'], + requireAllUserRoles: [Array, 'function'], + requireMaster: [Boolean], + validateMasterKey: [Boolean], + skipWithMasterKey: [Boolean], + requireUserKeys: [Array, Object], + fields: [Array, Object], + }; + const getType = fn => { + if (Array.isArray(fn)) { + return 'array'; + } + if (fn === 'Any' || fn === 'function') { + return fn; + } + const type = typeof fn; + if (typeof fn === 'function') { + const match = fn && fn.toString().match(/^\s*function (\w+)/); + return (match ? match[1] : 'function').toLowerCase(); + } + return type; + }; + const checkKey = (key, data, validatorParam) => { + const parameter = data[key]; + if (!parameter) { + throw `${key} is not a supported parameter for Cloud Function validations.`; + } + const types = parameter.map(type => getType(type)); + const type = getType(validatorParam); + if (!types.includes(type) && !types.includes('Any')) { + throw `Invalid type for Cloud Function validation key ${key}. Expected ${types.join( + '|' + )}, actual ${type}`; + } + }; + for (const key in validator) { + checkKey(key, allowedKeys, validator[key]); + if (key === 'fields' || key === 'requireUserKeys') { + const values = validator[key]; + if (Array.isArray(values)) { + continue; + } + for (const value in values) { + const data = values[value]; + for (const subKey in data) { + checkKey(subKey, fieldOptions, data[subKey]); + } + } + } + } +} /** @namespace * @name Parse * @description The Parse SDK. @@ -50,6 +115,7 @@ var ParseCloud = {}; * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.define = function (functionName, handler, validationHandler) { + validateValidator(validationHandler); triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId); }; @@ -96,6 +162,7 @@ ParseCloud.job = function (functionName, handler) { */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeSave, className, @@ -131,6 +198,7 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { */ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeDelete, className, @@ -260,6 +328,7 @@ ParseCloud.afterLogout = function (handler) { */ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterSave, className, @@ -295,6 +364,7 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { */ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterDelete, className, @@ -330,6 +400,7 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { */ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeFind, className, @@ -365,6 +436,7 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { */ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterFind, className, @@ -397,6 +469,7 @@ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSaveFile = function (handler, validationHandler) { + validateValidator(validationHandler); triggers.addFileTrigger( triggers.Types.beforeSaveFile, handler, @@ -428,6 +501,7 @@ ParseCloud.beforeSaveFile = function (handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSaveFile = function (handler, validationHandler) { + validateValidator(validationHandler); triggers.addFileTrigger( triggers.Types.afterSaveFile, handler, @@ -459,6 +533,7 @@ ParseCloud.afterSaveFile = function (handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { + validateValidator(validationHandler); triggers.addFileTrigger( triggers.Types.beforeDeleteFile, handler, @@ -490,6 +565,7 @@ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDeleteFile = function (handler, validationHandler) { + validateValidator(validationHandler); triggers.addFileTrigger( triggers.Types.afterDeleteFile, handler, @@ -521,6 +597,7 @@ ParseCloud.afterDeleteFile = function (handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.ConnectTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeConnect = function (handler, validationHandler) { + validateValidator(validationHandler); triggers.addConnectTrigger( triggers.Types.beforeConnect, handler, @@ -585,6 +662,7 @@ ParseCloud.sendEmail = function (data) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { + validateValidator(validationHandler); var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeSubscribe, @@ -624,6 +702,7 @@ ParseCloud.onLiveQueryEvent = function (handler) { */ ParseCloud.afterLiveQueryEvent = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); + validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterEvent, className,