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:
dblythy
2021-02-13 09:01:38 +11:00
committed by GitHub
parent 7224cde023
commit 3d76643286
6 changed files with 376 additions and 211 deletions

View File

@@ -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);