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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String>|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<String>|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<String>} 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.
|
||||
|
||||
129
src/triggers.js
129
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user