refactor: Parse Pointer allows to access internal Parse Server classes and circumvent beforeFind query trigger (#8735)

This commit is contained in:
Manuel
2023-09-04 16:01:02 +02:00
committed by GitHub
parent 877eede075
commit 5954f0ffa0
12 changed files with 423 additions and 230 deletions

View File

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