refactor: Parse Pointer allows to access internal Parse Server classes and circumvent beforeFind query trigger (#8735)
This commit is contained in:
197
src/RestQuery.js
197
src/RestQuery.js
@@ -6,6 +6,8 @@ var Parse = require('parse/node').Parse;
|
||||
const triggers = require('./triggers');
|
||||
const { continueWhile } = require('parse/lib/node/promiseUtils');
|
||||
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
const { enforceRoleSecurity } = require('./SharedRest');
|
||||
|
||||
// restOptions can include:
|
||||
// skip
|
||||
// limit
|
||||
@@ -18,7 +20,80 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
// readPreference
|
||||
// includeReadPreference
|
||||
// subqueryReadPreference
|
||||
function RestQuery(
|
||||
/**
|
||||
* Use to perform a query on a class. It will run security checks and triggers.
|
||||
* @param options
|
||||
* @param options.method {RestQuery.Method} The type of query to perform
|
||||
* @param options.config {ParseServerConfiguration} The server configuration
|
||||
* @param options.auth {Auth} The auth object for the request
|
||||
* @param options.className {string} The name of the class to query
|
||||
* @param options.restWhere {object} The where object for the query
|
||||
* @param options.restOptions {object} The options object for the query
|
||||
* @param options.clientSDK {string} The client SDK that is performing the query
|
||||
* @param options.runAfterFind {boolean} Whether to run the afterFind trigger
|
||||
* @param options.runBeforeFind {boolean} Whether to run the beforeFind trigger
|
||||
* @param options.context {object} The context object for the query
|
||||
* @returns {Promise<_UnsafeRestQuery>} A promise that is resolved with the _UnsafeRestQuery object
|
||||
*/
|
||||
async function RestQuery({
|
||||
method,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere = {},
|
||||
restOptions = {},
|
||||
clientSDK,
|
||||
runAfterFind = true,
|
||||
runBeforeFind = true,
|
||||
context,
|
||||
}) {
|
||||
if (![RestQuery.Method.find, RestQuery.Method.get].includes(method)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
|
||||
}
|
||||
enforceRoleSecurity(method, className, auth);
|
||||
const result = runBeforeFind
|
||||
? await triggers.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
method === RestQuery.Method.get
|
||||
)
|
||||
: Promise.resolve({ restWhere, restOptions });
|
||||
|
||||
return new _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
result.restWhere || restWhere,
|
||||
result.restOptions || restOptions,
|
||||
clientSDK,
|
||||
runAfterFind,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
RestQuery.Method = Object.freeze({
|
||||
get: 'get',
|
||||
find: 'find',
|
||||
});
|
||||
|
||||
/**
|
||||
* _UnsafeRestQuery is meant for specific internal usage only. When you need to skip security checks or some triggers.
|
||||
* Don't use it if you don't know what you are doing.
|
||||
* @param config
|
||||
* @param auth
|
||||
* @param className
|
||||
* @param restWhere
|
||||
* @param restOptions
|
||||
* @param clientSDK
|
||||
* @param runAfterFind
|
||||
* @param context
|
||||
*/
|
||||
function _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -197,7 +272,7 @@ function RestQuery(
|
||||
// Returns a promise for the response - an object with optional keys
|
||||
// 'results' and 'count'.
|
||||
// TODO: consolidate the replaceX functions
|
||||
RestQuery.prototype.execute = function (executeOptions) {
|
||||
_UnsafeRestQuery.prototype.execute = function (executeOptions) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
@@ -231,7 +306,7 @@ RestQuery.prototype.execute = function (executeOptions) {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.each = function (callback) {
|
||||
_UnsafeRestQuery.prototype.each = function (callback) {
|
||||
const { config, auth, className, restWhere, restOptions, clientSDK } = this;
|
||||
// if the limit is set, use it
|
||||
restOptions.limit = restOptions.limit || 100;
|
||||
@@ -243,7 +318,9 @@ RestQuery.prototype.each = function (callback) {
|
||||
return !finished;
|
||||
},
|
||||
async () => {
|
||||
const query = new RestQuery(
|
||||
// Safe here to use _UnsafeRestQuery because the security was already
|
||||
// checked during "await RestQuery()"
|
||||
const query = new _UnsafeRestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -265,7 +342,7 @@ RestQuery.prototype.each = function (callback) {
|
||||
);
|
||||
};
|
||||
|
||||
RestQuery.prototype.buildRestWhere = function () {
|
||||
_UnsafeRestQuery.prototype.buildRestWhere = function () {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.getUserAndRoleACL();
|
||||
@@ -294,7 +371,7 @@ RestQuery.prototype.buildRestWhere = function () {
|
||||
};
|
||||
|
||||
// Uses the Auth object to get the list of roles, adds the user id
|
||||
RestQuery.prototype.getUserAndRoleACL = function () {
|
||||
_UnsafeRestQuery.prototype.getUserAndRoleACL = function () {
|
||||
if (this.auth.isMaster) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -313,7 +390,7 @@ RestQuery.prototype.getUserAndRoleACL = function () {
|
||||
|
||||
// Changes the className if redirectClassNameForKey is set.
|
||||
// Returns a promise.
|
||||
RestQuery.prototype.redirectClassNameForKey = function () {
|
||||
_UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
|
||||
if (!this.redirectKey) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -328,7 +405,7 @@ RestQuery.prototype.redirectClassNameForKey = function () {
|
||||
};
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function () {
|
||||
_UnsafeRestQuery.prototype.validateClientClassCreation = function () {
|
||||
if (
|
||||
this.config.allowClientClassCreation === false &&
|
||||
!this.auth.isMaster &&
|
||||
@@ -371,7 +448,7 @@ function transformInQuery(inQueryObject, className, results) {
|
||||
// $inQuery clause.
|
||||
// The $inQuery clause turns into an $in with values that are just
|
||||
// pointers to the objects returned in the subquery.
|
||||
RestQuery.prototype.replaceInQuery = function () {
|
||||
_UnsafeRestQuery.prototype.replaceInQuery = async function () {
|
||||
var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery');
|
||||
if (!inQueryObject) {
|
||||
return;
|
||||
@@ -394,13 +471,14 @@ RestQuery.prototype.replaceInQuery = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
inQueryValue.className,
|
||||
inQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: inQueryValue.className,
|
||||
restWhere: inQueryValue.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
return subquery.execute().then(response => {
|
||||
transformInQuery(inQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
@@ -429,7 +507,7 @@ function transformNotInQuery(notInQueryObject, className, results) {
|
||||
// $notInQuery clause.
|
||||
// The $notInQuery clause turns into a $nin with values that are just
|
||||
// pointers to the objects returned in the subquery.
|
||||
RestQuery.prototype.replaceNotInQuery = function () {
|
||||
_UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
|
||||
var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery');
|
||||
if (!notInQueryObject) {
|
||||
return;
|
||||
@@ -452,13 +530,15 @@ RestQuery.prototype.replaceNotInQuery = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
notInQueryValue.className,
|
||||
notInQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: notInQueryValue.className,
|
||||
restWhere: notInQueryValue.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformNotInQuery(notInQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
@@ -492,7 +572,7 @@ const transformSelect = (selectObject, key, objects) => {
|
||||
// The $select clause turns into an $in with values selected out of
|
||||
// the subquery.
|
||||
// Returns a possible-promise.
|
||||
RestQuery.prototype.replaceSelect = function () {
|
||||
_UnsafeRestQuery.prototype.replaceSelect = async function () {
|
||||
var selectObject = findObjectWithKey(this.restWhere, '$select');
|
||||
if (!selectObject) {
|
||||
return;
|
||||
@@ -522,13 +602,15 @@ RestQuery.prototype.replaceSelect = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
selectValue.query.className,
|
||||
selectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: selectValue.query.className,
|
||||
restWhere: selectValue.query.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformSelect(selectObject, selectValue.key, response.results);
|
||||
// Keep replacing $select clauses
|
||||
@@ -554,7 +636,7 @@ const transformDontSelect = (dontSelectObject, key, objects) => {
|
||||
// The $dontSelect clause turns into an $nin with values selected out of
|
||||
// the subquery.
|
||||
// Returns a possible-promise.
|
||||
RestQuery.prototype.replaceDontSelect = function () {
|
||||
_UnsafeRestQuery.prototype.replaceDontSelect = async function () {
|
||||
var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect');
|
||||
if (!dontSelectObject) {
|
||||
return;
|
||||
@@ -582,13 +664,15 @@ RestQuery.prototype.replaceDontSelect = function () {
|
||||
additionalOptions.readPreference = this.restOptions.readPreference;
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config,
|
||||
this.auth,
|
||||
dontSelectValue.query.className,
|
||||
dontSelectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
const subquery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config: this.config,
|
||||
auth: this.auth,
|
||||
className: dontSelectValue.query.className,
|
||||
restWhere: dontSelectValue.query.where,
|
||||
restOptions: additionalOptions,
|
||||
});
|
||||
|
||||
return subquery.execute().then(response => {
|
||||
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
|
||||
// Keep replacing $dontSelect clauses
|
||||
@@ -596,7 +680,7 @@ RestQuery.prototype.replaceDontSelect = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.cleanResultAuthData = function (result) {
|
||||
_UnsafeRestQuery.prototype.cleanResultAuthData = function (result) {
|
||||
delete result.password;
|
||||
if (result.authData) {
|
||||
Object.keys(result.authData).forEach(provider => {
|
||||
@@ -635,7 +719,7 @@ const replaceEqualityConstraint = constraint => {
|
||||
return constraint;
|
||||
};
|
||||
|
||||
RestQuery.prototype.replaceEquality = function () {
|
||||
_UnsafeRestQuery.prototype.replaceEquality = function () {
|
||||
if (typeof this.restWhere !== 'object') {
|
||||
return;
|
||||
}
|
||||
@@ -646,7 +730,7 @@ RestQuery.prototype.replaceEquality = function () {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response with an object that only has 'results'.
|
||||
RestQuery.prototype.runFind = function (options = {}) {
|
||||
_UnsafeRestQuery.prototype.runFind = function (options = {}) {
|
||||
if (this.findOptions.limit === 0) {
|
||||
this.response = { results: [] };
|
||||
return Promise.resolve();
|
||||
@@ -682,7 +766,7 @@ RestQuery.prototype.runFind = function (options = {}) {
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response.count with the count
|
||||
RestQuery.prototype.runCount = function () {
|
||||
_UnsafeRestQuery.prototype.runCount = function () {
|
||||
if (!this.doCount) {
|
||||
return;
|
||||
}
|
||||
@@ -694,7 +778,7 @@ RestQuery.prototype.runCount = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.denyProtectedFields = async function () {
|
||||
_UnsafeRestQuery.prototype.denyProtectedFields = async function () {
|
||||
if (this.auth.isMaster) {
|
||||
return;
|
||||
}
|
||||
@@ -719,7 +803,7 @@ RestQuery.prototype.denyProtectedFields = async function () {
|
||||
};
|
||||
|
||||
// Augments this.response with all pointers on an object
|
||||
RestQuery.prototype.handleIncludeAll = function () {
|
||||
_UnsafeRestQuery.prototype.handleIncludeAll = function () {
|
||||
if (!this.includeAll) {
|
||||
return;
|
||||
}
|
||||
@@ -748,7 +832,7 @@ RestQuery.prototype.handleIncludeAll = function () {
|
||||
};
|
||||
|
||||
// Updates property `this.keys` to contain all keys but the ones unselected.
|
||||
RestQuery.prototype.handleExcludeKeys = function () {
|
||||
_UnsafeRestQuery.prototype.handleExcludeKeys = function () {
|
||||
if (!this.excludeKeys) {
|
||||
return;
|
||||
}
|
||||
@@ -766,7 +850,7 @@ RestQuery.prototype.handleExcludeKeys = function () {
|
||||
};
|
||||
|
||||
// Augments this.response with data at the paths provided in this.include.
|
||||
RestQuery.prototype.handleInclude = function () {
|
||||
_UnsafeRestQuery.prototype.handleInclude = function () {
|
||||
if (this.include.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -793,7 +877,7 @@ RestQuery.prototype.handleInclude = function () {
|
||||
};
|
||||
|
||||
//Returns a promise of a processed set of results
|
||||
RestQuery.prototype.runAfterFindTrigger = function () {
|
||||
_UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
|
||||
if (!this.response) {
|
||||
return;
|
||||
}
|
||||
@@ -845,7 +929,7 @@ RestQuery.prototype.runAfterFindTrigger = function () {
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.handleAuthAdapters = async function () {
|
||||
_UnsafeRestQuery.prototype.handleAuthAdapters = async function () {
|
||||
if (this.className !== '_User' || this.findOptions.explain) {
|
||||
return;
|
||||
}
|
||||
@@ -927,7 +1011,7 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
includeRestOptions.readPreference = restOptions.readPreference;
|
||||
}
|
||||
|
||||
const queryPromises = Object.keys(pointersHash).map(className => {
|
||||
const queryPromises = Object.keys(pointersHash).map(async className => {
|
||||
const objectIds = Array.from(pointersHash[className]);
|
||||
let where;
|
||||
if (objectIds.length === 1) {
|
||||
@@ -935,7 +1019,14 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
} else {
|
||||
where = { objectId: { $in: objectIds } };
|
||||
}
|
||||
var query = new RestQuery(config, auth, className, where, includeRestOptions);
|
||||
const query = await RestQuery({
|
||||
method: objectIds.length === 1 ? RestQuery.Method.get : RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere: where,
|
||||
restOptions: includeRestOptions,
|
||||
});
|
||||
return query.execute({ op: 'get' }).then(results => {
|
||||
results.className = className;
|
||||
return Promise.resolve(results);
|
||||
@@ -1066,3 +1157,5 @@ function findObjectWithKey(root, key) {
|
||||
}
|
||||
|
||||
module.exports = RestQuery;
|
||||
// For tests
|
||||
module.exports._UnsafeRestQuery = _UnsafeRestQuery;
|
||||
|
||||
Reference in New Issue
Block a user