diff --git a/CHANGELOG.md b/CHANGELOG.md index 2974978f93..314d74bc8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ ___ - IMPROVE: Parse Server will from now on be continuously tested against all relevant MongoDB versions (minor versions). 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: 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: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). 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) - FIX: Winston Logger interpolating stdout to console [#7114](https://github.com/parse-community/parse-server/pull/7114). Thanks to [dplewis](https://github.com/dplewis) ### 4.5.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/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..f0f65a5aed 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); } @@ -423,7 +434,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 +499,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 +601,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 +613,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 +628,7 @@ export function maybeRunValidator(request, functionName) { }); }); } -function builtInTriggerValidator(options, request) { +async function builtInTriggerValidator(options, request, auth) { if (request.master && !options.validateMasterKey) { return; } @@ -630,7 +641,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 +735,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 +854,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 +935,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 +961,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'); -}