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

This commit is contained in:
Manuel
2023-09-04 16:01:22 +02:00
committed by GitHub
parent d6b17baa32
commit 739ffbed86
12 changed files with 423 additions and 230 deletions

View File

@@ -2423,6 +2423,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', () => {

View File

@@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => {
it('should only count', async () => {
await prepareData();
await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();
const where = {

View File

@@ -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()

View File

@@ -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 => {

View File

@@ -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')

View File

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

View File

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

View File

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

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;

View File

@@ -620,7 +620,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;
@@ -630,19 +630,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

37
src/SharedRest.js Normal file
View File

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

View File

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