From 3d76643286d41b954b6c8e2d6614657c891a31e7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 13 Feb 2021 09:01:38 +1100 Subject: [PATCH] New: requireAnyUserRoles and requireAllUserRoles for Parse Cloud Validator (#7097) * new: requireUserRole for Parse Cloud Validator * change to requireUserRoles * Update CHANGELOG.md * revoke triggers * Update triggers.js * Update ParseLiveQueryServer.js * Update ParseLiveQueryServer.js * create requireUserRoles * rename to requireAny and requireAll * allow for a function --- CHANGELOG.md | 2 + spec/CloudCode.Validator.spec.js | 144 ++++++++++++ src/LiveQuery/ParseLiveQueryServer.js | 307 +++++++++++++++----------- src/Routers/FunctionsRouter.js | 2 +- src/cloud-code/Parse.Cloud.js | 3 + src/triggers.js | 129 +++++------ 6 files changed, 376 insertions(+), 211 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1608343a..8bf67af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ ___ - 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: 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 62749714..36a7fc96 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 0913f0a2..a4a6e6e7 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; + 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); } - 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; - } - 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'; - } - } 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, - }; - 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) - ); - } + 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 { + type = 'create'; + } + } 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 1b891bf2..d2399081 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 80eead1f..5ed6aa72 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 218cd033..a9f08052 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); } @@ -424,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) { @@ -489,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) { @@ -591,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; @@ -603,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(() => { @@ -618,7 +629,7 @@ export function maybeRunValidator(request, functionName) { }); }); } -function builtInTriggerValidator(options, request) { +async function builtInTriggerValidator(options, request, auth) { if (request.master && !options.validateMasterKey) { return; } @@ -631,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) { @@ -722,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) { @@ -809,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) { @@ -890,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; } @@ -916,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'); -}