From 5954f0ffa0ebedcb2955b9cffd571a0190e32a29 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:01:02 +0200 Subject: [PATCH] refactor: Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger (#8735) --- spec/CloudCode.spec.js | 29 +++++ spec/ParseGraphQLServer.spec.js | 1 - spec/ParseRole.spec.js | 2 +- spec/RestQuery.spec.js | 44 ++++--- spec/rest.spec.js | 32 +++++ src/Auth.js | 61 ++++++--- src/Controllers/PushController.js | 11 +- src/Controllers/UserController.js | 23 +++- src/RestQuery.js | 197 ++++++++++++++++++++++-------- src/RestWrite.js | 32 +++-- src/SharedRest.js | 37 ++++++ src/rest.js | 184 ++++++++++------------------ 12 files changed, 423 insertions(+), 230 deletions(-) create mode 100644 src/SharedRest.js diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 90ab313826..9a39664614 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2431,6 +2431,35 @@ describe('beforeFind hooks', () => { }) .then(() => done()); }); + + it('should run beforeFind on pointers and array of pointers from an object', async () => { + const obj1 = new Parse.Object('TestObject'); + const obj2 = new Parse.Object('TestObject2'); + const obj3 = new Parse.Object('TestObject'); + obj2.set('aField', 'aFieldValue'); + await obj2.save(); + obj1.set('pointerField', obj2); + obj3.set('pointerFieldArray', [obj2]); + await obj1.save(); + await obj3.save(); + const spy = jasmine.createSpy('beforeFindSpy'); + Parse.Cloud.beforeFind('TestObject2', spy); + const query = new Parse.Query('TestObject'); + await query.get(obj1.id); + // Pointer not included in query so we don't expect beforeFind to be called + expect(spy).not.toHaveBeenCalled(); + const query2 = new Parse.Query('TestObject'); + query2.include('pointerField'); + const res = await query2.get(obj1.id); + expect(res.get('pointerField').get('aField')).toBe('aFieldValue'); + // Pointer included in query so we expect beforeFind to be called + expect(spy).toHaveBeenCalledTimes(1); + const query3 = new Parse.Query('TestObject'); + query3.include('pointerFieldArray'); + const res2 = await query3.get(obj3.id); + expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue'); + expect(spy).toHaveBeenCalledTimes(2); + }); }); describe('afterFind hooks', () => { diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 87718da13a..022fb99fd2 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => { it('should only count', async () => { await prepareData(); - await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear(); const where = { diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 47fed865fb..31de5b661e 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -142,7 +142,7 @@ describe('Parse Role testing', () => { return Promise.all(promises); }; - const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough(); + const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); let user, auth, getAllRolesSpy; createTestUser() diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 24e22ac4f5..023d3b4790 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -399,15 +399,16 @@ describe('RestQuery.each', () => { } const config = Config.get('test'); await Parse.Object.saveAll(objects); - const query = new RestQuery( + const query = await RestQuery({ + method: RestQuery.Method.find, config, - auth.master(config), - 'Object', - { value: { $gt: 2 } }, - { limit: 2 } - ); + auth: auth.master(config), + className: 'Object', + restWhere: { value: { $gt: 2 } }, + restOptions: { limit: 2 }, + }); const spy = spyOn(query, 'execute').and.callThrough(); - const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough(); + const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); const results = []; await query.each(result => { expect(result.value).toBeGreaterThan(2); @@ -438,34 +439,37 @@ describe('RestQuery.each', () => { * Two queries needed since objectId are sorted and we can't know which one * going to be the first and then skip by the $gt added by each */ - const queryOne = new RestQuery( + const queryOne = await RestQuery({ + method: RestQuery.Method.get, config, - auth.master(config), - 'Letter', - { + auth: auth.master(config), + className: 'Letter', + restWhere: { numbers: { __type: 'Pointer', className: 'Number', objectId: object1.id, }, }, - { limit: 1 } - ); - const queryTwo = new RestQuery( + restOptions: { limit: 1 }, + }); + + const queryTwo = await RestQuery({ + method: RestQuery.Method.get, config, - auth.master(config), - 'Letter', - { + auth: auth.master(config), + className: 'Letter', + restWhere: { numbers: { __type: 'Pointer', className: 'Number', objectId: object2.id, }, }, - { limit: 1 } - ); + restOptions: { limit: 1 }, + }); - const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough(); + const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough(); const resultsOne = []; const resultsTwo = []; await queryOne.each(result => { diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 02d2f5960b..61a5c728e4 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -660,6 +660,38 @@ describe('rest create', () => { }); }); + it('cannot get object in volatileClasses if not masterKey through pointer', async () => { + const masterKeyOnlyClassObject = new Parse.Object('_PushStatus'); + await masterKeyOnlyClassObject.save(null, { useMasterKey: true }); + const obj2 = new Parse.Object('TestObject'); + // Anyone is can basically create a pointer to any object + // or some developers can use master key in some hook to link + // private objects to standard objects + obj2.set('pointer', masterKeyOnlyClassObject); + await obj2.save(); + const query = new Parse.Query('TestObject'); + query.include('pointer'); + await expectAsync(query.get(obj2.id)).toBeRejectedWithError( + "Clients aren't allowed to perform the get operation on the _PushStatus collection." + ); + }); + + it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => { + await Parse.Config.save({ privateData: 'secret' }, { privateData: true }); + const obj2 = new Parse.Object('TestObject'); + obj2.set('globalConfigPointer', { + __type: 'Pointer', + className: '_GlobalConfig', + objectId: 1, + }); + await obj2.save(); + const query = new Parse.Query('TestObject'); + query.include('globalConfigPointer'); + await expectAsync(query.get(obj2.id)).toBeRejectedWithError( + "Clients aren't allowed to perform the get operation on the _GlobalConfig collection." + ); + }); + it('locks down session', done => { let currentUser; Parse.User.signUp('foo', 'bar') diff --git a/src/Auth.js b/src/Auth.js index 0fe3b54460..6488b8427e 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -77,13 +77,16 @@ const renewSessionIfNeeded = async ({ config, session, sessionToken }) => { throttle[sessionToken] = setTimeout(async () => { try { if (!session) { - const { results } = await new RestQuery( + const query = await RestQuery({ + method: RestQuery.Method.get, config, - master(config), - '_Session', - { sessionToken }, - { limit: 1 } - ).execute(); + auth: master(config), + runBeforeFind: false, + className: '_Session', + restWhere: { sessionToken }, + restOptions: { limit: 1 }, + }); + const { results } = await query.execute(); session = results[0]; } const lastUpdated = new Date(session?.updatedAt); @@ -140,7 +143,15 @@ const getAuthForSessionToken = async function ({ include: 'user', }; const RestQuery = require('./RestQuery'); - const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions); + const query = await RestQuery({ + method: RestQuery.Method.get, + config, + runBeforeFind: false, + auth: master(config), + className: '_Session', + restWhere: { sessionToken }, + restOptions, + }); results = (await query.execute()).results; } else { results = ( @@ -179,12 +190,20 @@ const getAuthForSessionToken = async function ({ }); }; -var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) { +var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) { var restOptions = { limit: 1, }; const RestQuery = require('./RestQuery'); - var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions); + var query = await RestQuery({ + method: RestQuery.Method.get, + config, + runBeforeFind: false, + auth: master(config), + className: '_User', + restWhere: { _session_token: sessionToken }, + restOptions, + }); return query.execute().then(response => { var results = response.results; if (results.length !== 1) { @@ -229,9 +248,15 @@ Auth.prototype.getRolesForUser = async function () { }, }; const RestQuery = require('./RestQuery'); - await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result => - results.push(result) - ); + const query = await RestQuery({ + method: RestQuery.Method.find, + runBeforeFind: false, + config: this.config, + auth: master(this.config), + className: '_Role', + restWhere, + }); + await query.each(result => results.push(result)); } else { await new Parse.Query(Parse.Role) .equalTo('users', this.user) @@ -323,9 +348,15 @@ Auth.prototype.getRolesByIds = async function (ins) { }); const restWhere = { roles: { $in: roles } }; const RestQuery = require('./RestQuery'); - await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result => - results.push(result) - ); + const query = await RestQuery({ + method: RestQuery.Method.find, + config: this.config, + runBeforeFind: false, + auth: master(this.config), + className: '_Role', + restWhere, + }); + await query.each(result => results.push(result)); } return results; }; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 1a5b9bf491..04fb5c4fd0 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -58,9 +58,16 @@ export class PushController { // Force filtering on only valid device tokens const updateWhere = applyDeviceTokenExists(where); - badgeUpdate = () => { + badgeUpdate = async () => { // Build a real RestQuery so we can use it in RestWrite - const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere); + const restQuery = await RestQuery({ + method: RestQuery.Method.find, + config, + runBeforeFind: false, + auth: master(config), + className: '_Installation', + restWhere: updateWhere, + }); return restQuery.buildRestWhere().then(() => { const write = new RestWrite( config, diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 7618f500bf..726dc279fa 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -61,7 +61,7 @@ export class UserController extends AdaptableController { return true; } - verifyEmail(username, token) { + async verifyEmail(username, token) { if (!this.shouldVerifyEmails) { // Trying to verify email when not enabled // TODO: Better error here. @@ -83,8 +83,14 @@ export class UserController extends AdaptableController { updateFields._email_verify_token_expires_at = { __op: 'Delete' }; } const maintenanceAuth = Auth.maintenance(this.config); - var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', { - username, + var findUserForEmailVerification = await RestQuery({ + method: RestQuery.Method.get, + config: this.config, + auth: maintenanceAuth, + className: '_User', + restWhere: { + username, + }, }); return findUserForEmailVerification.execute().then(result => { if (result.results.length && result.results[0].emailVerified) { @@ -123,7 +129,7 @@ export class UserController extends AdaptableController { }); } - getUserIfNeeded(user) { + async getUserIfNeeded(user) { if (user.username && user.email) { return Promise.resolve(user); } @@ -135,7 +141,14 @@ export class UserController extends AdaptableController { where.email = user.email; } - var query = new RestQuery(this.config, Auth.master(this.config), '_User', where); + var query = await RestQuery({ + method: RestQuery.Method.get, + config: this.config, + runBeforeFind: false, + auth: Auth.master(this.config), + className: '_User', + restWhere: where, + }); return query.execute().then(function (result) { if (result.results.length != 1) { throw undefined; diff --git a/src/RestQuery.js b/src/RestQuery.js index fe3617eb1b..538d87d4c1 100644 --- a/src/RestQuery.js +++ b/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; diff --git a/src/RestWrite.js b/src/RestWrite.js index d35e52d6b5..a469936fd8 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -621,7 +621,7 @@ RestWrite.prototype.checkRestrictedFields = async function () { }; // The non-third-party parts of User transformation -RestWrite.prototype.transformUser = function () { +RestWrite.prototype.transformUser = async function () { var promise = Promise.resolve(); if (this.className !== '_User') { return promise; @@ -631,19 +631,25 @@ RestWrite.prototype.transformUser = function () { if (this.query && this.objectId()) { // If we're updating a _User object, we need to clear out the cache for that user. Find all their // session tokens, and remove them from the cache. - promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { - user: { - __type: 'Pointer', - className: '_User', - objectId: this.objectId(), + const query = await RestQuery({ + method: RestQuery.Method.find, + config: this.config, + auth: Auth.master(this.config), + className: '_Session', + runBeforeFind: false, + restWhere: { + user: { + __type: 'Pointer', + className: '_User', + objectId: this.objectId(), + }, }, - }) - .execute() - .then(results => { - results.results.forEach(session => - this.config.cacheController.user.del(session.sessionToken) - ); - }); + }); + promise = query.execute().then(results => { + results.results.forEach(session => + this.config.cacheController.user.del(session.sessionToken) + ); + }); } return promise diff --git a/src/SharedRest.js b/src/SharedRest.js new file mode 100644 index 0000000000..0b4a07c320 --- /dev/null +++ b/src/SharedRest.js @@ -0,0 +1,37 @@ +const classesWithMasterOnlyAccess = [ + '_JobStatus', + '_PushStatus', + '_Hooks', + '_GlobalConfig', + '_JobSchedule', + '_Idempotency', +]; +// Disallowing access to the _Role collection except by master key +function enforceRoleSecurity(method, className, auth) { + if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { + if (method === 'delete' || method === 'find') { + const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + } + } + + //all volatileClasses are masterKey only + if ( + classesWithMasterOnlyAccess.indexOf(className) >= 0 && + !auth.isMaster && + !auth.isMaintenance + ) { + const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + } + + // readOnly masterKey is not allowed + if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { + const error = `read-only masterKey isn't allowed to perform the ${method} operation.`; + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + } +} + +module.exports = { + enforceRoleSecurity, +}; diff --git a/src/rest.js b/src/rest.js index e1e53668a6..1f9dbacb73 100644 --- a/src/rest.js +++ b/src/rest.js @@ -12,6 +12,7 @@ var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); +const { enforceRoleSecurity } = require('./SharedRest'); function checkTriggers(className, config, types) { return types.some(triggerType => { @@ -24,65 +25,34 @@ function checkLiveQuery(className, config) { } // Returns a promise for an object with optional keys 'results' and 'count'. -function find(config, auth, className, restWhere, restOptions, clientSDK, context) { - enforceRoleSecurity('find', className, auth); - return triggers - .maybeRunQueryTrigger( - triggers.Types.beforeFind, - className, - restWhere, - restOptions, - config, - auth, - context - ) - .then(result => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - const query = new RestQuery( - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - true, - context - ); - return query.execute(); - }); -} +const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { + const query = await RestQuery({ + method: RestQuery.Method.find, + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + }); + return query.execute(); +}; // get is just like find but only queries an objectId. -const get = (config, auth, className, objectId, restOptions, clientSDK, context) => { +const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { var restWhere = { objectId }; - enforceRoleSecurity('get', className, auth); - return triggers - .maybeRunQueryTrigger( - triggers.Types.beforeFind, - className, - restWhere, - restOptions, - config, - auth, - context, - true - ) - .then(result => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - const query = new RestQuery( - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - true, - context - ); - return query.execute(); - }); + const query = await RestQuery({ + method: RestQuery.Method.get, + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + }); + return query.execute(); }; // Returns a promise that doesn't resolve to any useful value. @@ -101,35 +71,40 @@ function del(config, auth, className, objectId, context) { let schemaController; return Promise.resolve() - .then(() => { + .then(async () => { const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']); const hasLiveQuery = checkLiveQuery(className, config); if (hasTriggers || hasLiveQuery || className == '_Session') { - return new RestQuery(config, auth, className, { objectId }) - .execute({ op: 'delete' }) - .then(response => { - if (response && response.results && response.results.length) { - const firstResult = response.results[0]; - firstResult.className = className; - if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { - if (!auth.user || firstResult.user.objectId !== auth.user.id) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } + const query = await RestQuery({ + method: RestQuery.Method.get, + config, + auth, + className, + restWhere: { objectId }, + }); + return query.execute({ op: 'delete' }).then(response => { + if (response && response.results && response.results.length) { + const firstResult = response.results[0]; + firstResult.className = className; + if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { + if (!auth.user || firstResult.user.objectId !== auth.user.id) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } - var cacheAdapter = config.cacheController; - cacheAdapter.user.del(firstResult.sessionToken); - inflatedObject = Parse.Object.fromJSON(firstResult); - return triggers.maybeRunTrigger( - triggers.Types.beforeDelete, - auth, - inflatedObject, - null, - config, - context - ); } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.'); - }); + var cacheAdapter = config.cacheController; + cacheAdapter.user.del(firstResult.sessionToken); + inflatedObject = Parse.Object.fromJSON(firstResult); + return triggers.maybeRunTrigger( + triggers.Types.beforeDelete, + auth, + inflatedObject, + null, + config, + context + ); + } + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for delete.'); + }); } return Promise.resolve({}); }) @@ -193,21 +168,22 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte enforceRoleSecurity('update', className, auth); return Promise.resolve() - .then(() => { + .then(async () => { const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']); const hasLiveQuery = checkLiveQuery(className, config); if (hasTriggers || hasLiveQuery) { // Do not use find, as it runs the before finds - return new RestQuery( + const query = await RestQuery({ + method: RestQuery.Method.get, config, auth, className, restWhere, - undefined, - undefined, - false, - context - ).execute({ + runAfterFind: false, + runBeforeFind: false, + context, + }); + return query.execute({ op: 'update', }); } @@ -248,40 +224,6 @@ function handleSessionMissingError(error, className, auth) { throw error; } -const classesWithMasterOnlyAccess = [ - '_JobStatus', - '_PushStatus', - '_Hooks', - '_GlobalConfig', - '_JobSchedule', - '_Idempotency', -]; -// Disallowing access to the _Role collection except by master key -function enforceRoleSecurity(method, className, auth) { - if (className === '_Installation' && !auth.isMaster && !auth.isMaintenance) { - if (method === 'delete' || method === 'find') { - const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); - } - } - - //all volatileClasses are masterKey only - if ( - classesWithMasterOnlyAccess.indexOf(className) >= 0 && - !auth.isMaster && - !auth.isMaintenance - ) { - const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); - } - - // readOnly masterKey is not allowed - if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { - const error = `read-only masterKey isn't allowed to perform the ${method} operation.`; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); - } -} - module.exports = { create, del,