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

@@ -2431,6 +2431,35 @@ describe('beforeFind hooks', () => {
}) })
.then(() => done()); .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', () => { describe('afterFind hooks', () => {

View File

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

View File

@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
return Promise.all(promises); 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; let user, auth, getAllRolesSpy;
createTestUser() createTestUser()

View File

@@ -399,15 +399,16 @@ describe('RestQuery.each', () => {
} }
const config = Config.get('test'); const config = Config.get('test');
await Parse.Object.saveAll(objects); await Parse.Object.saveAll(objects);
const query = new RestQuery( const query = await RestQuery({
method: RestQuery.Method.find,
config, config,
auth.master(config), auth: auth.master(config),
'Object', className: 'Object',
{ value: { $gt: 2 } }, restWhere: { value: { $gt: 2 } },
{ limit: 2 } restOptions: { limit: 2 },
); });
const spy = spyOn(query, 'execute').and.callThrough(); 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 = []; const results = [];
await query.each(result => { await query.each(result => {
expect(result.value).toBeGreaterThan(2); 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 * 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 * 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, config,
auth.master(config), auth: auth.master(config),
'Letter', className: 'Letter',
{ restWhere: {
numbers: { numbers: {
__type: 'Pointer', __type: 'Pointer',
className: 'Number', className: 'Number',
objectId: object1.id, objectId: object1.id,
}, },
}, },
{ limit: 1 } restOptions: { limit: 1 },
); });
const queryTwo = new RestQuery(
const queryTwo = await RestQuery({
method: RestQuery.Method.get,
config, config,
auth.master(config), auth: auth.master(config),
'Letter', className: 'Letter',
{ restWhere: {
numbers: { numbers: {
__type: 'Pointer', __type: 'Pointer',
className: 'Number', className: 'Number',
objectId: object2.id, 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 resultsOne = [];
const resultsTwo = []; const resultsTwo = [];
await queryOne.each(result => { 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 => { it('locks down session', done => {
let currentUser; let currentUser;
Parse.User.signUp('foo', 'bar') Parse.User.signUp('foo', 'bar')

View File

@@ -77,13 +77,16 @@ const renewSessionIfNeeded = async ({ config, session, sessionToken }) => {
throttle[sessionToken] = setTimeout(async () => { throttle[sessionToken] = setTimeout(async () => {
try { try {
if (!session) { if (!session) {
const { results } = await new RestQuery( const query = await RestQuery({
method: RestQuery.Method.get,
config, config,
master(config), auth: master(config),
'_Session', runBeforeFind: false,
{ sessionToken }, className: '_Session',
{ limit: 1 } restWhere: { sessionToken },
).execute(); restOptions: { limit: 1 },
});
const { results } = await query.execute();
session = results[0]; session = results[0];
} }
const lastUpdated = new Date(session?.updatedAt); const lastUpdated = new Date(session?.updatedAt);
@@ -140,7 +143,15 @@ const getAuthForSessionToken = async function ({
include: 'user', include: 'user',
}; };
const RestQuery = require('./RestQuery'); 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; results = (await query.execute()).results;
} else { } else {
results = ( results = (
@@ -179,12 +190,20 @@ const getAuthForSessionToken = async function ({
}); });
}; };
var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) { var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
var restOptions = { var restOptions = {
limit: 1, limit: 1,
}; };
const RestQuery = require('./RestQuery'); 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 => { return query.execute().then(response => {
var results = response.results; var results = response.results;
if (results.length !== 1) { if (results.length !== 1) {
@@ -229,9 +248,15 @@ Auth.prototype.getRolesForUser = async function () {
}, },
}; };
const RestQuery = require('./RestQuery'); const RestQuery = require('./RestQuery');
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result => const query = await RestQuery({
results.push(result) method: RestQuery.Method.find,
); runBeforeFind: false,
config: this.config,
auth: master(this.config),
className: '_Role',
restWhere,
});
await query.each(result => results.push(result));
} else { } else {
await new Parse.Query(Parse.Role) await new Parse.Query(Parse.Role)
.equalTo('users', this.user) .equalTo('users', this.user)
@@ -323,9 +348,15 @@ Auth.prototype.getRolesByIds = async function (ins) {
}); });
const restWhere = { roles: { $in: roles } }; const restWhere = { roles: { $in: roles } };
const RestQuery = require('./RestQuery'); const RestQuery = require('./RestQuery');
await new RestQuery(this.config, master(this.config), '_Role', restWhere, {}).each(result => const query = await RestQuery({
results.push(result) 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; return results;
}; };

View File

@@ -58,9 +58,16 @@ export class PushController {
// Force filtering on only valid device tokens // Force filtering on only valid device tokens
const updateWhere = applyDeviceTokenExists(where); const updateWhere = applyDeviceTokenExists(where);
badgeUpdate = () => { badgeUpdate = async () => {
// Build a real RestQuery so we can use it in RestWrite // 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(() => { return restQuery.buildRestWhere().then(() => {
const write = new RestWrite( const write = new RestWrite(
config, config,

View File

@@ -61,7 +61,7 @@ export class UserController extends AdaptableController {
return true; return true;
} }
verifyEmail(username, token) { async verifyEmail(username, token) {
if (!this.shouldVerifyEmails) { if (!this.shouldVerifyEmails) {
// Trying to verify email when not enabled // Trying to verify email when not enabled
// TODO: Better error here. // TODO: Better error here.
@@ -83,8 +83,14 @@ export class UserController extends AdaptableController {
updateFields._email_verify_token_expires_at = { __op: 'Delete' }; updateFields._email_verify_token_expires_at = { __op: 'Delete' };
} }
const maintenanceAuth = Auth.maintenance(this.config); const maintenanceAuth = Auth.maintenance(this.config);
var findUserForEmailVerification = new RestQuery(this.config, maintenanceAuth, '_User', { var findUserForEmailVerification = await RestQuery({
username, method: RestQuery.Method.get,
config: this.config,
auth: maintenanceAuth,
className: '_User',
restWhere: {
username,
},
}); });
return findUserForEmailVerification.execute().then(result => { return findUserForEmailVerification.execute().then(result => {
if (result.results.length && result.results[0].emailVerified) { 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) { if (user.username && user.email) {
return Promise.resolve(user); return Promise.resolve(user);
} }
@@ -135,7 +141,14 @@ export class UserController extends AdaptableController {
where.email = user.email; 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) { return query.execute().then(function (result) {
if (result.results.length != 1) { if (result.results.length != 1) {
throw undefined; throw undefined;

View File

@@ -6,6 +6,8 @@ var Parse = require('parse/node').Parse;
const triggers = require('./triggers'); const triggers = require('./triggers');
const { continueWhile } = require('parse/lib/node/promiseUtils'); const { continueWhile } = require('parse/lib/node/promiseUtils');
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
const { enforceRoleSecurity } = require('./SharedRest');
// restOptions can include: // restOptions can include:
// skip // skip
// limit // limit
@@ -18,7 +20,80 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
// readPreference // readPreference
// includeReadPreference // includeReadPreference
// subqueryReadPreference // 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, config,
auth, auth,
className, className,
@@ -197,7 +272,7 @@ function RestQuery(
// Returns a promise for the response - an object with optional keys // Returns a promise for the response - an object with optional keys
// 'results' and 'count'. // 'results' and 'count'.
// TODO: consolidate the replaceX functions // TODO: consolidate the replaceX functions
RestQuery.prototype.execute = function (executeOptions) { _UnsafeRestQuery.prototype.execute = function (executeOptions) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
return this.buildRestWhere(); 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; const { config, auth, className, restWhere, restOptions, clientSDK } = this;
// if the limit is set, use it // if the limit is set, use it
restOptions.limit = restOptions.limit || 100; restOptions.limit = restOptions.limit || 100;
@@ -243,7 +318,9 @@ RestQuery.prototype.each = function (callback) {
return !finished; return !finished;
}, },
async () => { async () => {
const query = new RestQuery( // Safe here to use _UnsafeRestQuery because the security was already
// checked during "await RestQuery()"
const query = new _UnsafeRestQuery(
config, config,
auth, auth,
className, className,
@@ -265,7 +342,7 @@ RestQuery.prototype.each = function (callback) {
); );
}; };
RestQuery.prototype.buildRestWhere = function () { _UnsafeRestQuery.prototype.buildRestWhere = function () {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
return this.getUserAndRoleACL(); 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 // 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) { if (this.auth.isMaster) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -313,7 +390,7 @@ RestQuery.prototype.getUserAndRoleACL = function () {
// Changes the className if redirectClassNameForKey is set. // Changes the className if redirectClassNameForKey is set.
// Returns a promise. // Returns a promise.
RestQuery.prototype.redirectClassNameForKey = function () { _UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
if (!this.redirectKey) { if (!this.redirectKey) {
return Promise.resolve(); return Promise.resolve();
} }
@@ -328,7 +405,7 @@ RestQuery.prototype.redirectClassNameForKey = function () {
}; };
// Validates this operation against the allowClientClassCreation config. // Validates this operation against the allowClientClassCreation config.
RestQuery.prototype.validateClientClassCreation = function () { _UnsafeRestQuery.prototype.validateClientClassCreation = function () {
if ( if (
this.config.allowClientClassCreation === false && this.config.allowClientClassCreation === false &&
!this.auth.isMaster && !this.auth.isMaster &&
@@ -371,7 +448,7 @@ function transformInQuery(inQueryObject, className, results) {
// $inQuery clause. // $inQuery clause.
// The $inQuery clause turns into an $in with values that are just // The $inQuery clause turns into an $in with values that are just
// pointers to the objects returned in the subquery. // pointers to the objects returned in the subquery.
RestQuery.prototype.replaceInQuery = function () { _UnsafeRestQuery.prototype.replaceInQuery = async function () {
var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery'); var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery');
if (!inQueryObject) { if (!inQueryObject) {
return; return;
@@ -394,13 +471,14 @@ RestQuery.prototype.replaceInQuery = function () {
additionalOptions.readPreference = this.restOptions.readPreference; additionalOptions.readPreference = this.restOptions.readPreference;
} }
var subquery = new RestQuery( const subquery = await RestQuery({
this.config, method: RestQuery.Method.find,
this.auth, config: this.config,
inQueryValue.className, auth: this.auth,
inQueryValue.where, className: inQueryValue.className,
additionalOptions restWhere: inQueryValue.where,
); restOptions: additionalOptions,
});
return subquery.execute().then(response => { return subquery.execute().then(response => {
transformInQuery(inQueryObject, subquery.className, response.results); transformInQuery(inQueryObject, subquery.className, response.results);
// Recurse to repeat // Recurse to repeat
@@ -429,7 +507,7 @@ function transformNotInQuery(notInQueryObject, className, results) {
// $notInQuery clause. // $notInQuery clause.
// The $notInQuery clause turns into a $nin with values that are just // The $notInQuery clause turns into a $nin with values that are just
// pointers to the objects returned in the subquery. // pointers to the objects returned in the subquery.
RestQuery.prototype.replaceNotInQuery = function () { _UnsafeRestQuery.prototype.replaceNotInQuery = async function () {
var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery'); var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery');
if (!notInQueryObject) { if (!notInQueryObject) {
return; return;
@@ -452,13 +530,15 @@ RestQuery.prototype.replaceNotInQuery = function () {
additionalOptions.readPreference = this.restOptions.readPreference; additionalOptions.readPreference = this.restOptions.readPreference;
} }
var subquery = new RestQuery( const subquery = await RestQuery({
this.config, method: RestQuery.Method.find,
this.auth, config: this.config,
notInQueryValue.className, auth: this.auth,
notInQueryValue.where, className: notInQueryValue.className,
additionalOptions restWhere: notInQueryValue.where,
); restOptions: additionalOptions,
});
return subquery.execute().then(response => { return subquery.execute().then(response => {
transformNotInQuery(notInQueryObject, subquery.className, response.results); transformNotInQuery(notInQueryObject, subquery.className, response.results);
// Recurse to repeat // 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 $select clause turns into an $in with values selected out of
// the subquery. // the subquery.
// Returns a possible-promise. // Returns a possible-promise.
RestQuery.prototype.replaceSelect = function () { _UnsafeRestQuery.prototype.replaceSelect = async function () {
var selectObject = findObjectWithKey(this.restWhere, '$select'); var selectObject = findObjectWithKey(this.restWhere, '$select');
if (!selectObject) { if (!selectObject) {
return; return;
@@ -522,13 +602,15 @@ RestQuery.prototype.replaceSelect = function () {
additionalOptions.readPreference = this.restOptions.readPreference; additionalOptions.readPreference = this.restOptions.readPreference;
} }
var subquery = new RestQuery( const subquery = await RestQuery({
this.config, method: RestQuery.Method.find,
this.auth, config: this.config,
selectValue.query.className, auth: this.auth,
selectValue.query.where, className: selectValue.query.className,
additionalOptions restWhere: selectValue.query.where,
); restOptions: additionalOptions,
});
return subquery.execute().then(response => { return subquery.execute().then(response => {
transformSelect(selectObject, selectValue.key, response.results); transformSelect(selectObject, selectValue.key, response.results);
// Keep replacing $select clauses // 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 $dontSelect clause turns into an $nin with values selected out of
// the subquery. // the subquery.
// Returns a possible-promise. // Returns a possible-promise.
RestQuery.prototype.replaceDontSelect = function () { _UnsafeRestQuery.prototype.replaceDontSelect = async function () {
var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect'); var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect');
if (!dontSelectObject) { if (!dontSelectObject) {
return; return;
@@ -582,13 +664,15 @@ RestQuery.prototype.replaceDontSelect = function () {
additionalOptions.readPreference = this.restOptions.readPreference; additionalOptions.readPreference = this.restOptions.readPreference;
} }
var subquery = new RestQuery( const subquery = await RestQuery({
this.config, method: RestQuery.Method.find,
this.auth, config: this.config,
dontSelectValue.query.className, auth: this.auth,
dontSelectValue.query.where, className: dontSelectValue.query.className,
additionalOptions restWhere: dontSelectValue.query.where,
); restOptions: additionalOptions,
});
return subquery.execute().then(response => { return subquery.execute().then(response => {
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
// Keep replacing $dontSelect clauses // 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; delete result.password;
if (result.authData) { if (result.authData) {
Object.keys(result.authData).forEach(provider => { Object.keys(result.authData).forEach(provider => {
@@ -635,7 +719,7 @@ const replaceEqualityConstraint = constraint => {
return constraint; return constraint;
}; };
RestQuery.prototype.replaceEquality = function () { _UnsafeRestQuery.prototype.replaceEquality = function () {
if (typeof this.restWhere !== 'object') { if (typeof this.restWhere !== 'object') {
return; return;
} }
@@ -646,7 +730,7 @@ RestQuery.prototype.replaceEquality = function () {
// Returns a promise for whether it was successful. // Returns a promise for whether it was successful.
// Populates this.response with an object that only has 'results'. // 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) { if (this.findOptions.limit === 0) {
this.response = { results: [] }; this.response = { results: [] };
return Promise.resolve(); return Promise.resolve();
@@ -682,7 +766,7 @@ RestQuery.prototype.runFind = function (options = {}) {
// Returns a promise for whether it was successful. // Returns a promise for whether it was successful.
// Populates this.response.count with the count // Populates this.response.count with the count
RestQuery.prototype.runCount = function () { _UnsafeRestQuery.prototype.runCount = function () {
if (!this.doCount) { if (!this.doCount) {
return; return;
} }
@@ -694,7 +778,7 @@ RestQuery.prototype.runCount = function () {
}); });
}; };
RestQuery.prototype.denyProtectedFields = async function () { _UnsafeRestQuery.prototype.denyProtectedFields = async function () {
if (this.auth.isMaster) { if (this.auth.isMaster) {
return; return;
} }
@@ -719,7 +803,7 @@ RestQuery.prototype.denyProtectedFields = async function () {
}; };
// Augments this.response with all pointers on an object // Augments this.response with all pointers on an object
RestQuery.prototype.handleIncludeAll = function () { _UnsafeRestQuery.prototype.handleIncludeAll = function () {
if (!this.includeAll) { if (!this.includeAll) {
return; return;
} }
@@ -748,7 +832,7 @@ RestQuery.prototype.handleIncludeAll = function () {
}; };
// Updates property `this.keys` to contain all keys but the ones unselected. // Updates property `this.keys` to contain all keys but the ones unselected.
RestQuery.prototype.handleExcludeKeys = function () { _UnsafeRestQuery.prototype.handleExcludeKeys = function () {
if (!this.excludeKeys) { if (!this.excludeKeys) {
return; return;
} }
@@ -766,7 +850,7 @@ RestQuery.prototype.handleExcludeKeys = function () {
}; };
// Augments this.response with data at the paths provided in this.include. // 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) { if (this.include.length == 0) {
return; return;
} }
@@ -793,7 +877,7 @@ RestQuery.prototype.handleInclude = function () {
}; };
//Returns a promise of a processed set of results //Returns a promise of a processed set of results
RestQuery.prototype.runAfterFindTrigger = function () { _UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
if (!this.response) { if (!this.response) {
return; 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) { if (this.className !== '_User' || this.findOptions.explain) {
return; return;
} }
@@ -927,7 +1011,7 @@ function includePath(config, auth, response, path, restOptions = {}) {
includeRestOptions.readPreference = restOptions.readPreference; 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]); const objectIds = Array.from(pointersHash[className]);
let where; let where;
if (objectIds.length === 1) { if (objectIds.length === 1) {
@@ -935,7 +1019,14 @@ function includePath(config, auth, response, path, restOptions = {}) {
} else { } else {
where = { objectId: { $in: objectIds } }; 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 => { return query.execute({ op: 'get' }).then(results => {
results.className = className; results.className = className;
return Promise.resolve(results); return Promise.resolve(results);
@@ -1066,3 +1157,5 @@ function findObjectWithKey(root, key) {
} }
module.exports = RestQuery; module.exports = RestQuery;
// For tests
module.exports._UnsafeRestQuery = _UnsafeRestQuery;

View File

@@ -621,7 +621,7 @@ RestWrite.prototype.checkRestrictedFields = async function () {
}; };
// The non-third-party parts of User transformation // The non-third-party parts of User transformation
RestWrite.prototype.transformUser = function () { RestWrite.prototype.transformUser = async function () {
var promise = Promise.resolve(); var promise = Promise.resolve();
if (this.className !== '_User') { if (this.className !== '_User') {
return promise; return promise;
@@ -631,19 +631,25 @@ RestWrite.prototype.transformUser = function () {
if (this.query && this.objectId()) { 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 // 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. // session tokens, and remove them from the cache.
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { const query = await RestQuery({
user: { method: RestQuery.Method.find,
__type: 'Pointer', config: this.config,
className: '_User', auth: Auth.master(this.config),
objectId: this.objectId(), className: '_Session',
runBeforeFind: false,
restWhere: {
user: {
__type: 'Pointer',
className: '_User',
objectId: this.objectId(),
},
}, },
}) });
.execute() promise = query.execute().then(results => {
.then(results => { results.results.forEach(session =>
results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken)
this.config.cacheController.user.del(session.sessionToken) );
); });
});
} }
return promise 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 RestQuery = require('./RestQuery');
var RestWrite = require('./RestWrite'); var RestWrite = require('./RestWrite');
var triggers = require('./triggers'); var triggers = require('./triggers');
const { enforceRoleSecurity } = require('./SharedRest');
function checkTriggers(className, config, types) { function checkTriggers(className, config, types) {
return types.some(triggerType => { return types.some(triggerType => {
@@ -24,65 +25,34 @@ function checkLiveQuery(className, config) {
} }
// Returns a promise for an object with optional keys 'results' and 'count'. // Returns a promise for an object with optional keys 'results' and 'count'.
function find(config, auth, className, restWhere, restOptions, clientSDK, context) { const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
enforceRoleSecurity('find', className, auth); const query = await RestQuery({
return triggers method: RestQuery.Method.find,
.maybeRunQueryTrigger( config,
triggers.Types.beforeFind, auth,
className, className,
restWhere, restWhere,
restOptions, restOptions,
config, clientSDK,
auth, context,
context });
) return query.execute();
.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();
});
}
// get is just like find but only queries an objectId. // 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 }; var restWhere = { objectId };
enforceRoleSecurity('get', className, auth); const query = await RestQuery({
return triggers method: RestQuery.Method.get,
.maybeRunQueryTrigger( config,
triggers.Types.beforeFind, auth,
className, className,
restWhere, restWhere,
restOptions, restOptions,
config, clientSDK,
auth, context,
context, });
true return query.execute();
)
.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();
});
}; };
// Returns a promise that doesn't resolve to any useful value. // Returns a promise that doesn't resolve to any useful value.
@@ -101,35 +71,40 @@ function del(config, auth, className, objectId, context) {
let schemaController; let schemaController;
return Promise.resolve() return Promise.resolve()
.then(() => { .then(async () => {
const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']); const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']);
const hasLiveQuery = checkLiveQuery(className, config); const hasLiveQuery = checkLiveQuery(className, config);
if (hasTriggers || hasLiveQuery || className == '_Session') { if (hasTriggers || hasLiveQuery || className == '_Session') {
return new RestQuery(config, auth, className, { objectId }) const query = await RestQuery({
.execute({ op: 'delete' }) method: RestQuery.Method.get,
.then(response => { config,
if (response && response.results && response.results.length) { auth,
const firstResult = response.results[0]; className,
firstResult.className = className; restWhere: { objectId },
if (className === '_Session' && !auth.isMaster && !auth.isMaintenance) { });
if (!auth.user || firstResult.user.objectId !== auth.user.id) { return query.execute({ op: 'delete' }).then(response => {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); 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({}); return Promise.resolve({});
}) })
@@ -193,21 +168,22 @@ function update(config, auth, className, restWhere, restObject, clientSDK, conte
enforceRoleSecurity('update', className, auth); enforceRoleSecurity('update', className, auth);
return Promise.resolve() return Promise.resolve()
.then(() => { .then(async () => {
const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']); const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']);
const hasLiveQuery = checkLiveQuery(className, config); const hasLiveQuery = checkLiveQuery(className, config);
if (hasTriggers || hasLiveQuery) { if (hasTriggers || hasLiveQuery) {
// Do not use find, as it runs the before finds // Do not use find, as it runs the before finds
return new RestQuery( const query = await RestQuery({
method: RestQuery.Method.get,
config, config,
auth, auth,
className, className,
restWhere, restWhere,
undefined, runAfterFind: false,
undefined, runBeforeFind: false,
false, context,
context });
).execute({ return query.execute({
op: 'update', op: 'update',
}); });
} }
@@ -248,40 +224,6 @@ function handleSessionMissingError(error, className, auth) {
throw error; 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 = { module.exports = {
create, create,
del, del,