diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f17d62e2..0296891144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ ## Parse Server Changelog + ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/4.5.0...master) @@ -6,12 +15,13 @@ __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), [Manuel Trezza](https://github.com/mtrezza). ___ -- IMPROVE: Retry transactions on MongoDB when it fails due to transient error [#7187](https://github.com/parse-community/parse-server/pull/7187). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). -- UPGRADE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). - NEW (EXPERIMENTAL): Added new page router with placeholder rendering and localization of custom and feature pages such as password reset and email verification. **Caution, this is an experimental feature that may not be appropriate for production.** [#6891](https://github.com/parse-community/parse-server/issues/6891). Thanks to [Manuel Trezza](https://github.com/mtrezza). - 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) - NEW: LiveQuery support for $and, $nor, $containedBy, $geoWithin, $geoIntersects queries [#7113](https://github.com/parse-community/parse-server/pull/7113). Thanks to [dplewis](https://github.com/dplewis) - NEW: Supporting patterns in LiveQuery server's config parameter `classNames` [#7131](https://github.com/parse-community/parse-server/pull/7131). Thanks to [Nes-si](https://github.com/Nes-si) +- NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy) +- IMPROVE: Retry transactions on MongoDB when it fails due to transient error [#7187](https://github.com/parse-community/parse-server/pull/7187). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). +- IMPROVE: Bump tests to use Mongo 4.4.4 [#7184](https://github.com/parse-community/parse-server/pull/7184). Thanks to [Antonio Davi Macedo Coelho de Castro](https://github.com/davimacedo). - 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). diff --git a/README.md b/README.md index e67d60045e..9978815c22 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,9 @@ The full documentation for Parse Server is available in the [wiki](https://githu - [Getting Started](#getting-started) - [Running Parse Server](#running-parse-server) - [Compatibility](#compatibility) - - [Node.js](#nodejs-support) - - [MongoDB](#mongodb-support) - - [PostgreSQL](#postgresql-support) + - [Node.js](#nodejs) + - [MongoDB](#mongodb) + - [PostgreSQL](#postgresql) - [Locally](#locally) - [Docker Container](#docker-container) - [Saving an Object](#saving-an-object) diff --git a/package-lock.json b/package-lock.json index 01a2bd7794..9d15c8b95e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5731,9 +5731,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", + "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" }, "for-each": { "version": "0.3.3", diff --git a/package.json b/package.json index 753795d9ee..6a4062c3dc 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "cors": "2.8.5", "deepcopy": "2.1.0", "express": "4.17.1", - "follow-redirects": "1.13.1", + "follow-redirects": "1.13.2", "graphql": "15.4.0", "graphql-list-fields": "2.0.2", "graphql-relay": "0.6.0", diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 627497148a..36a7fc96b1 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -878,6 +878,150 @@ describe('cloud validator', () => { }); }); + it('basic validator requireAnyUserRoles', async function (done) { + Parse.Cloud.define( + 'cloudFunction', + () => { + return true; + }, + { + requireUser: true, + requireAnyUserRoles: ['Admin'], + } + ); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + try { + await Parse.Cloud.run('cloudFunction'); + fail('cloud validator should have failed.'); + } catch (e) { + expect(e.message).toBe('Validation failed. User does not match the required roles.'); + } + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + const role = new Parse.Role('Admin', roleACL); + role.getUsers().add(user); + await role.save({ useMasterKey: true }); + await Parse.Cloud.run('cloudFunction'); + done(); + }); + + it('basic validator requireAllUserRoles', async function (done) { + Parse.Cloud.define( + 'cloudFunction', + () => { + return true; + }, + { + requireUser: true, + requireAllUserRoles: ['Admin', 'Admin2'], + } + ); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + try { + await Parse.Cloud.run('cloudFunction'); + fail('cloud validator should have failed.'); + } catch (e) { + expect(e.message).toBe('Validation failed. User does not match all the required roles.'); + } + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + const role = new Parse.Role('Admin', roleACL); + role.getUsers().add(user); + + const role2 = new Parse.Role('Admin2', roleACL); + role2.getUsers().add(user); + await Promise.all([role.save({ useMasterKey: true }), role2.save({ useMasterKey: true })]); + await Parse.Cloud.run('cloudFunction'); + done(); + }); + + it('allow requireAnyUserRoles to be a function', async function (done) { + Parse.Cloud.define( + 'cloudFunction', + () => { + return true; + }, + { + requireUser: true, + requireAnyUserRoles: () => { + return ['Admin Func']; + }, + } + ); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + try { + await Parse.Cloud.run('cloudFunction'); + fail('cloud validator should have failed.'); + } catch (e) { + expect(e.message).toBe('Validation failed. User does not match the required roles.'); + } + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + const role = new Parse.Role('Admin Func', roleACL); + role.getUsers().add(user); + await role.save({ useMasterKey: true }); + await Parse.Cloud.run('cloudFunction'); + done(); + }); + + it('allow requireAllUserRoles to be a function', async function (done) { + Parse.Cloud.define( + 'cloudFunction', + () => { + return true; + }, + { + requireUser: true, + requireAllUserRoles: () => { + return ['AdminA', 'AdminB']; + }, + } + ); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + try { + await Parse.Cloud.run('cloudFunction'); + fail('cloud validator should have failed.'); + } catch (e) { + expect(e.message).toBe('Validation failed. User does not match all the required roles.'); + } + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + const role = new Parse.Role('AdminA', roleACL); + role.getUsers().add(user); + + const role2 = new Parse.Role('AdminB', roleACL); + role2.getUsers().add(user); + await Promise.all([role.save({ useMasterKey: true }), role2.save({ useMasterKey: true })]); + await Parse.Cloud.run('cloudFunction'); + done(); + }); + + it('basic requireAllUserRoles but no user', async function (done) { + Parse.Cloud.define( + 'cloudFunction', + () => { + return true; + }, + { + requireAllUserRoles: ['Admin'], + } + ); + try { + await Parse.Cloud.run('cloudFunction'); + fail('cloud validator should have failed.'); + } catch (e) { + expect(e.message).toBe('Validation failed. Please login to continue.'); + } + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + const role = new Parse.Role('Admin', roleACL); + role.getUsers().add(user); + await role.save({ useMasterKey: true }); + await Parse.Cloud.run('cloudFunction'); + done(); + }); + it('basic beforeSave requireMaster', function (done) { Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { requireMaster: true, diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7f0fe70aa0..3153920d36 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1779,6 +1779,39 @@ describe('beforeSave hooks', () => { const myObject = new MyObject(); myObject.save().then(() => done()); }); + + it('should respect custom object ids (#6733)', async () => { + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.object.id).toEqual('test_6733'); + }); + + await reconfigureServer({ allowCustomObjectId: true }); + + const req = request({ + // Parse JS SDK does not currently support custom object ids (see #1097), so we do a REST request + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + objectId: 'test_6733', + foo: 'bar', + }, + }); + + { + const res = await req; + expect(res.data.objectId).toEqual('test_6733'); + } + + const query = new Parse.Query('TestObject'); + query.equalTo('objectId', 'test_6733'); + const res = await query.find(); + expect(res.length).toEqual(1); + expect(res[0].get('foo')).toEqual('bar'); + }); }); describe('afterSave hooks', () => { diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 0913f0a22d..a4a6e6e777 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -10,12 +10,7 @@ import { ParsePubSub } from './ParsePubSub'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { - runLiveQueryEventHandlers, - maybeRunConnectTrigger, - maybeRunSubscribeTrigger, - maybeRunAfterEventTrigger, -} from '../triggers'; +import { runLiveQueryEventHandlers, getTrigger, runTrigger } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; import LRU from 'lru-cache'; @@ -121,7 +116,7 @@ class ParseLiveQueryServer { // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. // Message.originalParseObject is the original ParseObject. - _onAfterDelete(message: any): void { + async _onAfterDelete(message: any): void { logger.verbose(Parse.applicationId + 'afterDelete is triggered'); let deletedParseObject = message.currentParseObject.toJSON(); @@ -135,6 +130,7 @@ class ParseLiveQueryServer { logger.debug('Can not find subscriptions under this class ' + className); return; } + for (const subscription of classSubscriptions.values()) { const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription); if (!isSubscriptionMatched) { @@ -145,63 +141,71 @@ class ParseLiveQueryServer { if (typeof client === 'undefined') { continue; } - for (const requestId of requestIds) { + requestIds.forEach(async requestId => { const acl = message.currentParseObject.getACL(); // Check CLP const op = this._getCLPOperation(subscription.query); let res = {}; - this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op) - .then(() => { - // Check ACL - return this._matchesACL(acl, client, requestId); - }) - .then(isMatched => { - if (!isMatched) { - return null; - } - res = { - event: 'delete', - sessionToken: client.sessionToken, - object: deletedParseObject, - clients: this.clients.size, - subscriptions: this.subscriptions.size, - useMasterKey: client.hasMasterKey, - installationId: client.installationId, - sendEvent: true, - }; - return maybeRunAfterEventTrigger('afterEvent', className, res); - }) - .then(() => { - if (!res.sendEvent) { - return; - } - if (res.object && typeof res.object.toJSON === 'function') { - deletedParseObject = res.object.toJSON(); - deletedParseObject.className = className; + try { + await this._matchesCLP( + classLevelPermissions, + message.currentParseObject, + client, + requestId, + op + ); + const isMatched = await this._matchesACL(acl, client, requestId); + if (!isMatched) { + return null; + } + res = { + event: 'delete', + sessionToken: client.sessionToken, + object: deletedParseObject, + clients: this.clients.size, + subscriptions: this.subscriptions.size, + useMasterKey: client.hasMasterKey, + installationId: client.installationId, + sendEvent: true, + }; + const trigger = getTrigger(className, 'afterEvent', Parse.applicationId); + if (trigger) { + const auth = await this.getAuthForSessionToken(res.sessionToken); + res.user = auth.user; + if (res.object) { + res.object = Parse.Object.fromJSON(res.object); } - client.pushDelete(requestId, deletedParseObject); - }) - .catch(error => { - Client.pushError( - client.parseWebSocket, - error.code || 141, - error.message || error, - false, - requestId - ); - logger.error( - `Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` + - JSON.stringify(error) - ); - }); - } + await runTrigger(trigger, `afterEvent.${className}`, res, auth); + } + if (!res.sendEvent) { + return; + } + if (res.object && typeof res.object.toJSON === 'function') { + deletedParseObject = res.object.toJSON(); + deletedParseObject.className = className; + } + client.pushDelete(requestId, deletedParseObject); + } catch (error) { + Client.pushError( + client.parseWebSocket, + error.code || 141, + error.message || error, + false, + requestId + ); + logger.error( + `Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` + + JSON.stringify(error) + ); + } + }); } } } // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. // Message.originalParseObject is the original ParseObject. - _onAfterSave(message: any): void { + async _onAfterSave(message: any): void { logger.verbose(Parse.applicationId + 'afterSave is triggered'); let originalParseObject = null; @@ -233,7 +237,7 @@ class ParseLiveQueryServer { if (typeof client === 'undefined') { continue; } - for (const requestId of requestIds) { + requestIds.forEach(async requestId => { // Set orignal ParseObject ACL checking promise, if the object does not match // subscription, we do not need to check ACL let originalACLCheckingPromise; @@ -256,86 +260,99 @@ class ParseLiveQueryServer { const currentACL = message.currentParseObject.getACL(); currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId); } - const op = this._getCLPOperation(subscription.query); - this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op) - .then(() => { - return Promise.all([originalACLCheckingPromise, currentACLCheckingPromise]); - }) - .then(([isOriginalMatched, isCurrentMatched]) => { - logger.verbose( - 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', - originalParseObject, - currentParseObject, - isOriginalSubscriptionMatched, - isCurrentSubscriptionMatched, - isOriginalMatched, - isCurrentMatched, - subscription.hash - ); - // Decide event type - let type; - if (isOriginalMatched && isCurrentMatched) { - type = 'update'; - } else if (isOriginalMatched && !isCurrentMatched) { - type = 'leave'; - } else if (!isOriginalMatched && isCurrentMatched) { - if (originalParseObject) { - type = 'enter'; - } else { - type = 'create'; - } + try { + const op = this._getCLPOperation(subscription.query); + await this._matchesCLP( + classLevelPermissions, + message.currentParseObject, + client, + requestId, + op + ); + const [isOriginalMatched, isCurrentMatched] = await Promise.all([ + originalACLCheckingPromise, + currentACLCheckingPromise, + ]); + logger.verbose( + 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', + originalParseObject, + currentParseObject, + isOriginalSubscriptionMatched, + isCurrentSubscriptionMatched, + isOriginalMatched, + isCurrentMatched, + subscription.hash + ); + // Decide event type + let type; + if (isOriginalMatched && isCurrentMatched) { + type = 'update'; + } else if (isOriginalMatched && !isCurrentMatched) { + type = 'leave'; + } else if (!isOriginalMatched && isCurrentMatched) { + if (originalParseObject) { + type = 'enter'; } else { - return null; + type = 'create'; } - message.event = type; - res = { - event: type, - sessionToken: client.sessionToken, - object: currentParseObject, - original: originalParseObject, - clients: this.clients.size, - subscriptions: this.subscriptions.size, - useMasterKey: client.hasMasterKey, - installationId: client.installationId, - sendEvent: true, - }; - return maybeRunAfterEventTrigger('afterEvent', className, res); - }) - .then( - () => { - if (!res.sendEvent) { - return; - } - if (res.object && typeof res.object.toJSON === 'function') { - currentParseObject = res.object.toJSON(); - currentParseObject.className = res.object.className || className; - } - - if (res.original && typeof res.original.toJSON === 'function') { - originalParseObject = res.original.toJSON(); - originalParseObject.className = res.original.className || className; - } - const functionName = - 'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1); - if (client[functionName]) { - client[functionName](requestId, currentParseObject, originalParseObject); - } - }, - error => { - Client.pushError( - client.parseWebSocket, - error.code || 141, - error.message || error, - false, - requestId - ); - logger.error( - `Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` + - JSON.stringify(error) - ); + } else { + return null; + } + message.event = type; + res = { + event: type, + sessionToken: client.sessionToken, + object: currentParseObject, + original: originalParseObject, + clients: this.clients.size, + subscriptions: this.subscriptions.size, + useMasterKey: client.hasMasterKey, + installationId: client.installationId, + sendEvent: true, + }; + const trigger = getTrigger(className, 'afterEvent', Parse.applicationId); + if (trigger) { + if (res.object) { + res.object = Parse.Object.fromJSON(res.object); + } + if (res.original) { + res.original = Parse.Object.fromJSON(res.original); } + const auth = await this.getAuthForSessionToken(res.sessionToken); + res.user = auth.user; + await runTrigger(trigger, `afterEvent.${className}`, res, auth); + } + if (!res.sendEvent) { + return; + } + if (res.object && typeof res.object.toJSON === 'function') { + currentParseObject = res.object.toJSON(); + currentParseObject.className = res.object.className || className; + } + + if (res.original && typeof res.original.toJSON === 'function') { + originalParseObject = res.original.toJSON(); + originalParseObject.className = res.original.className || className; + } + const functionName = + 'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1); + if (client[functionName]) { + client[functionName](requestId, currentParseObject, originalParseObject); + } + } catch (error) { + Client.pushError( + client.parseWebSocket, + error.code || 141, + error.message || error, + false, + requestId ); - } + logger.error( + `Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` + + JSON.stringify(error) + ); + } + }); } } } @@ -614,7 +631,12 @@ class ParseLiveQueryServer { useMasterKey: client.hasMasterKey, installationId: request.installationId, }; - await maybeRunConnectTrigger('beforeConnect', req); + const trigger = getTrigger('@Connect', 'beforeConnect', Parse.applicationId); + if (trigger) { + const auth = await this.getAuthForSessionToken(req.sessionToken); + req.user = auth.user; + await runTrigger(trigger, `beforeConnect.@Connect`, req, auth); + } parseWebsocket.clientId = clientId; this.clients.set(parseWebsocket.clientId, client); logger.info(`Create new client: ${parseWebsocket.clientId}`); @@ -668,7 +690,22 @@ class ParseLiveQueryServer { const client = this.clients.get(parseWebsocket.clientId); const className = request.query.className; try { - await maybeRunSubscribeTrigger('beforeSubscribe', className, request); + const trigger = getTrigger(className, 'beforeSubscribe', Parse.applicationId); + if (trigger) { + const auth = await this.getAuthForSessionToken(request.sessionToken); + request.user = auth.user; + + const parseQuery = new Parse.Query(className); + parseQuery.withJSON(request.query); + request.query = parseQuery; + await runTrigger(trigger, `beforeSubscribe.${className}`, request, auth); + + const query = request.query.toJSON(); + if (query.keys) { + query.fields = query.keys.split(','); + } + request.query = query; + } // Get subscription from subscriptions, create one if necessary const subscriptionHash = queryHash(request.query); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 1b891bf26c..d239908103 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -173,7 +173,7 @@ export class FunctionsRouter extends PromiseRouter { ); return Promise.resolve() .then(() => { - return triggers.maybeRunValidator(request, functionName); + return triggers.maybeRunValidator(request, functionName, req.auth); }) .then(() => { return theFunction(request); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 80eead1f31..5ed6aa728f 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -751,6 +751,9 @@ module.exports = ParseCloud; * @property {Array|function|Any} requireUserKeys.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid. * @property {String} requireUserKeys.field.error custom error message if field is invalid. * + * @property {Array|function}requireAnyUserRoles If set, request.user has to be part of at least one roles name to make the request. If set to a function, function must return role names. + * @property {Array|function}requireAllUserRoles If set, request.user has to be part all roles name to make the request. If set to a function, function must return role names. + * * @property {Object|Array} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, fields to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`. * @property {String} fields.field name of field to validate. * @property {String} fields.field.type expected type of data for field. diff --git a/src/triggers.js b/src/triggers.js index fcaee9ee23..a9f08052c3 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -168,6 +168,17 @@ export function getTrigger(className, triggerType, applicationId) { return get(Category.Triggers, `${triggerType}.${className}`, applicationId); } +export async function runTrigger(trigger, name, request, auth) { + if (!trigger) { + return; + } + await maybeRunValidator(request, name, auth); + if (request.skipWithMasterKey) { + return; + } + return await trigger(request); +} + export function getFileTrigger(type, applicationId) { return getTrigger(FileClassName, type, applicationId); } @@ -327,6 +338,7 @@ export function getResponseObject(request, resolve, reject) { response = {}; if (request.triggerName === Types.beforeSave) { response['object'] = request.object._getSaveJSON(); + response['object']['objectId'] = request.object.id; } return resolve(response); }, @@ -423,7 +435,7 @@ export function maybeRunAfterFindTrigger( }); return Promise.resolve() .then(() => { - return maybeRunValidator(request, `${triggerType}.${className}`); + return maybeRunValidator(request, `${triggerType}.${className}`, auth); }) .then(() => { if (request.skipWithMasterKey) { @@ -488,7 +500,7 @@ export function maybeRunQueryTrigger( ); return Promise.resolve() .then(() => { - return maybeRunValidator(requestObject, `${triggerType}.${className}`); + return maybeRunValidator(requestObject, `${triggerType}.${className}`, auth); }) .then(() => { if (requestObject.skipWithMasterKey) { @@ -590,7 +602,7 @@ export function resolveError(message, defaultOpts) { } return error; } -export function maybeRunValidator(request, functionName) { +export function maybeRunValidator(request, functionName, auth) { const theValidator = getValidator(functionName, Parse.applicationId); if (!theValidator) { return; @@ -602,7 +614,7 @@ export function maybeRunValidator(request, functionName) { return Promise.resolve() .then(() => { return typeof theValidator === 'object' - ? builtInTriggerValidator(theValidator, request) + ? builtInTriggerValidator(theValidator, request, auth) : theValidator(request); }) .then(() => { @@ -617,7 +629,7 @@ export function maybeRunValidator(request, functionName) { }); }); } -function builtInTriggerValidator(options, request) { +async function builtInTriggerValidator(options, request, auth) { if (request.master && !options.validateMasterKey) { return; } @@ -630,7 +642,10 @@ function builtInTriggerValidator(options, request) { ) { reqUser = request.object; } - if (options.requireUser && !reqUser) { + if ( + (options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) && + !reqUser + ) { throw 'Validation failed. Please login to continue.'; } if (options.requireMaster && !request.master) { @@ -721,6 +736,38 @@ function builtInTriggerValidator(options, request) { } } } + let userRoles = options.requireAnyUserRoles; + let requireAllRoles = options.requireAllUserRoles; + const promises = [Promise.resolve(), Promise.resolve(), Promise.resolve()]; + if (userRoles || requireAllRoles) { + promises[0] = auth.getUserRoles(); + } + if (typeof userRoles === 'function') { + promises[1] = userRoles(); + } + if (typeof requireAllRoles === 'function') { + promises[2] = requireAllRoles(); + } + const [roles, resolvedUserRoles, resolvedRequireAll] = await Promise.all(promises); + if (resolvedUserRoles && Array.isArray(resolvedUserRoles)) { + userRoles = resolvedUserRoles; + } + if (resolvedRequireAll && Array.isArray(resolvedRequireAll)) { + requireAllRoles = resolvedRequireAll; + } + if (userRoles) { + const hasRole = userRoles.some(requiredRole => roles.includes(`role:${requiredRole}`)); + if (!hasRole) { + throw `Validation failed. User does not match the required roles.`; + } + } + if (requireAllRoles) { + for (const requiredRole of requireAllRoles) { + if (!roles.includes(`role:${requiredRole}`)) { + throw `Validation failed. User does not match all the required roles.`; + } + } + } const userKeys = options.requireUserKeys || []; if (Array.isArray(userKeys)) { for (const key of userKeys) { @@ -808,7 +855,7 @@ export function maybeRunTrigger( // to the RestWrite.execute() call. return Promise.resolve() .then(() => { - return maybeRunValidator(request, `${triggerType}.${parseObject.className}`); + return maybeRunValidator(request, `${triggerType}.${parseObject.className}`, auth); }) .then(() => { if (request.skipWithMasterKey) { @@ -889,7 +936,7 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) if (typeof fileTrigger === 'function') { try { const request = getRequestFileObject(triggerType, auth, fileObject, config); - await maybeRunValidator(request, `${triggerType}.${FileClassName}`); + await maybeRunValidator(request, `${triggerType}.${FileClassName}`, auth); if (request.skipWithMasterKey) { return fileObject; } @@ -915,70 +962,3 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) } return fileObject; } - -export async function maybeRunConnectTrigger(triggerType, request) { - const trigger = getTrigger(ConnectClassName, triggerType, Parse.applicationId); - if (!trigger) { - return; - } - request.user = await userForSessionToken(request.sessionToken); - await maybeRunValidator(request, `${triggerType}.${ConnectClassName}`); - if (request.skipWithMasterKey) { - return; - } - return trigger(request); -} - -export async function maybeRunSubscribeTrigger(triggerType, className, request) { - const trigger = getTrigger(className, triggerType, Parse.applicationId); - if (!trigger) { - return; - } - const parseQuery = new Parse.Query(className); - parseQuery.withJSON(request.query); - request.query = parseQuery; - request.user = await userForSessionToken(request.sessionToken); - await maybeRunValidator(request, `${triggerType}.${className}`); - if (request.skipWithMasterKey) { - return; - } - await trigger(request); - const query = request.query.toJSON(); - if (query.keys) { - query.fields = query.keys.split(','); - } - request.query = query; -} - -export async function maybeRunAfterEventTrigger(triggerType, className, request) { - const trigger = getTrigger(className, triggerType, Parse.applicationId); - if (!trigger) { - return; - } - if (request.object) { - request.object = Parse.Object.fromJSON(request.object); - } - if (request.original) { - request.original = Parse.Object.fromJSON(request.original); - } - request.user = await userForSessionToken(request.sessionToken); - await maybeRunValidator(request, `${triggerType}.${className}`); - if (request.skipWithMasterKey) { - return; - } - return trigger(request); -} - -async function userForSessionToken(sessionToken) { - if (!sessionToken) { - return; - } - const q = new Parse.Query('_Session'); - q.equalTo('sessionToken', sessionToken); - q.include('user'); - const session = await q.first({ useMasterKey: true }); - if (!session) { - return; - } - return session.get('user'); -}