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: 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).
|
- 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)
|
- 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)
|
- 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
|
### 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) {
|
it('basic beforeSave requireMaster', function (done) {
|
||||||
Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, {
|
Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, {
|
||||||
requireMaster: true,
|
requireMaster: true,
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ import { ParsePubSub } from './ParsePubSub';
|
|||||||
import SchemaController from '../Controllers/SchemaController';
|
import SchemaController from '../Controllers/SchemaController';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import {
|
import { runLiveQueryEventHandlers, getTrigger, runTrigger } from '../triggers';
|
||||||
runLiveQueryEventHandlers,
|
|
||||||
maybeRunConnectTrigger,
|
|
||||||
maybeRunSubscribeTrigger,
|
|
||||||
maybeRunAfterEventTrigger,
|
|
||||||
} from '../triggers';
|
|
||||||
import { getAuthForSessionToken, Auth } from '../Auth';
|
import { getAuthForSessionToken, Auth } from '../Auth';
|
||||||
import { getCacheController } from '../Controllers';
|
import { getCacheController } from '../Controllers';
|
||||||
import LRU from 'lru-cache';
|
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 is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
|
||||||
// Message.originalParseObject is the original ParseObject.
|
// Message.originalParseObject is the original ParseObject.
|
||||||
_onAfterDelete(message: any): void {
|
async _onAfterDelete(message: any): void {
|
||||||
logger.verbose(Parse.applicationId + 'afterDelete is triggered');
|
logger.verbose(Parse.applicationId + 'afterDelete is triggered');
|
||||||
|
|
||||||
let deletedParseObject = message.currentParseObject.toJSON();
|
let deletedParseObject = message.currentParseObject.toJSON();
|
||||||
@@ -135,6 +130,7 @@ class ParseLiveQueryServer {
|
|||||||
logger.debug('Can not find subscriptions under this class ' + className);
|
logger.debug('Can not find subscriptions under this class ' + className);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const subscription of classSubscriptions.values()) {
|
for (const subscription of classSubscriptions.values()) {
|
||||||
const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
||||||
if (!isSubscriptionMatched) {
|
if (!isSubscriptionMatched) {
|
||||||
@@ -145,63 +141,71 @@ class ParseLiveQueryServer {
|
|||||||
if (typeof client === 'undefined') {
|
if (typeof client === 'undefined') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const requestId of requestIds) {
|
requestIds.forEach(async requestId => {
|
||||||
const acl = message.currentParseObject.getACL();
|
const acl = message.currentParseObject.getACL();
|
||||||
// Check CLP
|
// Check CLP
|
||||||
const op = this._getCLPOperation(subscription.query);
|
const op = this._getCLPOperation(subscription.query);
|
||||||
let res = {};
|
let res = {};
|
||||||
this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op)
|
try {
|
||||||
.then(() => {
|
await this._matchesCLP(
|
||||||
// Check ACL
|
classLevelPermissions,
|
||||||
return this._matchesACL(acl, client, requestId);
|
message.currentParseObject,
|
||||||
})
|
client,
|
||||||
.then(isMatched => {
|
requestId,
|
||||||
if (!isMatched) {
|
op
|
||||||
return null;
|
);
|
||||||
|
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 = {
|
await runTrigger(trigger, `afterEvent.${className}`, res, auth);
|
||||||
event: 'delete',
|
}
|
||||||
sessionToken: client.sessionToken,
|
if (!res.sendEvent) {
|
||||||
object: deletedParseObject,
|
return;
|
||||||
clients: this.clients.size,
|
}
|
||||||
subscriptions: this.subscriptions.size,
|
if (res.object && typeof res.object.toJSON === 'function') {
|
||||||
useMasterKey: client.hasMasterKey,
|
deletedParseObject = res.object.toJSON();
|
||||||
installationId: client.installationId,
|
deletedParseObject.className = className;
|
||||||
sendEvent: true,
|
}
|
||||||
};
|
client.pushDelete(requestId, deletedParseObject);
|
||||||
return maybeRunAfterEventTrigger('afterEvent', className, res);
|
} catch (error) {
|
||||||
})
|
Client.pushError(
|
||||||
.then(() => {
|
client.parseWebSocket,
|
||||||
if (!res.sendEvent) {
|
error.code || 141,
|
||||||
return;
|
error.message || error,
|
||||||
}
|
false,
|
||||||
if (res.object && typeof res.object.toJSON === 'function') {
|
requestId
|
||||||
deletedParseObject = res.object.toJSON();
|
);
|
||||||
deletedParseObject.className = className;
|
logger.error(
|
||||||
}
|
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
|
||||||
client.pushDelete(requestId, deletedParseObject);
|
JSON.stringify(error)
|
||||||
})
|
);
|
||||||
.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 is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
|
||||||
// Message.originalParseObject is the original ParseObject.
|
// Message.originalParseObject is the original ParseObject.
|
||||||
_onAfterSave(message: any): void {
|
async _onAfterSave(message: any): void {
|
||||||
logger.verbose(Parse.applicationId + 'afterSave is triggered');
|
logger.verbose(Parse.applicationId + 'afterSave is triggered');
|
||||||
|
|
||||||
let originalParseObject = null;
|
let originalParseObject = null;
|
||||||
@@ -233,7 +237,7 @@ class ParseLiveQueryServer {
|
|||||||
if (typeof client === 'undefined') {
|
if (typeof client === 'undefined') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const requestId of requestIds) {
|
requestIds.forEach(async requestId => {
|
||||||
// Set orignal ParseObject ACL checking promise, if the object does not match
|
// Set orignal ParseObject ACL checking promise, if the object does not match
|
||||||
// subscription, we do not need to check ACL
|
// subscription, we do not need to check ACL
|
||||||
let originalACLCheckingPromise;
|
let originalACLCheckingPromise;
|
||||||
@@ -256,86 +260,99 @@ class ParseLiveQueryServer {
|
|||||||
const currentACL = message.currentParseObject.getACL();
|
const currentACL = message.currentParseObject.getACL();
|
||||||
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
|
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
|
||||||
}
|
}
|
||||||
const op = this._getCLPOperation(subscription.query);
|
try {
|
||||||
this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op)
|
const op = this._getCLPOperation(subscription.query);
|
||||||
.then(() => {
|
await this._matchesCLP(
|
||||||
return Promise.all([originalACLCheckingPromise, currentACLCheckingPromise]);
|
classLevelPermissions,
|
||||||
})
|
message.currentParseObject,
|
||||||
.then(([isOriginalMatched, isCurrentMatched]) => {
|
client,
|
||||||
logger.verbose(
|
requestId,
|
||||||
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
op
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
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,
|
useMasterKey: client.hasMasterKey,
|
||||||
installationId: request.installationId,
|
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;
|
parseWebsocket.clientId = clientId;
|
||||||
this.clients.set(parseWebsocket.clientId, client);
|
this.clients.set(parseWebsocket.clientId, client);
|
||||||
logger.info(`Create new client: ${parseWebsocket.clientId}`);
|
logger.info(`Create new client: ${parseWebsocket.clientId}`);
|
||||||
@@ -668,7 +690,22 @@ class ParseLiveQueryServer {
|
|||||||
const client = this.clients.get(parseWebsocket.clientId);
|
const client = this.clients.get(parseWebsocket.clientId);
|
||||||
const className = request.query.className;
|
const className = request.query.className;
|
||||||
try {
|
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
|
// Get subscription from subscriptions, create one if necessary
|
||||||
const subscriptionHash = queryHash(request.query);
|
const subscriptionHash = queryHash(request.query);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export class FunctionsRouter extends PromiseRouter {
|
|||||||
);
|
);
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return triggers.maybeRunValidator(request, functionName);
|
return triggers.maybeRunValidator(request, functionName, req.auth);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return theFunction(request);
|
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 {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 {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 {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 name of field to validate.
|
||||||
* @property {String} fields.field.type expected type of data for field.
|
* @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);
|
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) {
|
export function getFileTrigger(type, applicationId) {
|
||||||
return getTrigger(FileClassName, type, applicationId);
|
return getTrigger(FileClassName, type, applicationId);
|
||||||
}
|
}
|
||||||
@@ -424,7 +435,7 @@ export function maybeRunAfterFindTrigger(
|
|||||||
});
|
});
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return maybeRunValidator(request, `${triggerType}.${className}`);
|
return maybeRunValidator(request, `${triggerType}.${className}`, auth);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (request.skipWithMasterKey) {
|
if (request.skipWithMasterKey) {
|
||||||
@@ -489,7 +500,7 @@ export function maybeRunQueryTrigger(
|
|||||||
);
|
);
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return maybeRunValidator(requestObject, `${triggerType}.${className}`);
|
return maybeRunValidator(requestObject, `${triggerType}.${className}`, auth);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (requestObject.skipWithMasterKey) {
|
if (requestObject.skipWithMasterKey) {
|
||||||
@@ -591,7 +602,7 @@ export function resolveError(message, defaultOpts) {
|
|||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
export function maybeRunValidator(request, functionName) {
|
export function maybeRunValidator(request, functionName, auth) {
|
||||||
const theValidator = getValidator(functionName, Parse.applicationId);
|
const theValidator = getValidator(functionName, Parse.applicationId);
|
||||||
if (!theValidator) {
|
if (!theValidator) {
|
||||||
return;
|
return;
|
||||||
@@ -603,7 +614,7 @@ export function maybeRunValidator(request, functionName) {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return typeof theValidator === 'object'
|
return typeof theValidator === 'object'
|
||||||
? builtInTriggerValidator(theValidator, request)
|
? builtInTriggerValidator(theValidator, request, auth)
|
||||||
: theValidator(request);
|
: theValidator(request);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.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) {
|
if (request.master && !options.validateMasterKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -631,7 +642,10 @@ function builtInTriggerValidator(options, request) {
|
|||||||
) {
|
) {
|
||||||
reqUser = request.object;
|
reqUser = request.object;
|
||||||
}
|
}
|
||||||
if (options.requireUser && !reqUser) {
|
if (
|
||||||
|
(options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) &&
|
||||||
|
!reqUser
|
||||||
|
) {
|
||||||
throw 'Validation failed. Please login to continue.';
|
throw 'Validation failed. Please login to continue.';
|
||||||
}
|
}
|
||||||
if (options.requireMaster && !request.master) {
|
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 || [];
|
const userKeys = options.requireUserKeys || [];
|
||||||
if (Array.isArray(userKeys)) {
|
if (Array.isArray(userKeys)) {
|
||||||
for (const key of userKeys) {
|
for (const key of userKeys) {
|
||||||
@@ -809,7 +855,7 @@ export function maybeRunTrigger(
|
|||||||
// to the RestWrite.execute() call.
|
// to the RestWrite.execute() call.
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return maybeRunValidator(request, `${triggerType}.${parseObject.className}`);
|
return maybeRunValidator(request, `${triggerType}.${parseObject.className}`, auth);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (request.skipWithMasterKey) {
|
if (request.skipWithMasterKey) {
|
||||||
@@ -890,7 +936,7 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
|
|||||||
if (typeof fileTrigger === 'function') {
|
if (typeof fileTrigger === 'function') {
|
||||||
try {
|
try {
|
||||||
const request = getRequestFileObject(triggerType, auth, fileObject, config);
|
const request = getRequestFileObject(triggerType, auth, fileObject, config);
|
||||||
await maybeRunValidator(request, `${triggerType}.${FileClassName}`);
|
await maybeRunValidator(request, `${triggerType}.${FileClassName}`, auth);
|
||||||
if (request.skipWithMasterKey) {
|
if (request.skipWithMasterKey) {
|
||||||
return fileObject;
|
return fileObject;
|
||||||
}
|
}
|
||||||
@@ -916,70 +962,3 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
|
|||||||
}
|
}
|
||||||
return fileObject;
|
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