Skip to content

New: requireAnyUserRoles and requireAllUserRoles for Parse Cloud Validator #7097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 12, 2021
293 changes: 165 additions & 128 deletions src/LiveQuery/ParseLiveQueryServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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) {
Expand All @@ -150,58 +146,66 @@ class ParseLiveQueryServer {
// 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;
Expand Down Expand Up @@ -256,85 +260,98 @@ 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)
);
}
}
}
}
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Routers/FunctionsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading