Skip to content

Commit

Permalink
refactor: Parse Pointer allows to access internal Parse Server classe…
Browse files Browse the repository at this point in the history
…s and circumvent `beforeFind` query trigger (parse-community#8734)
  • Loading branch information
mtrezza authored Sep 4, 2023
1 parent d6b17ba commit 739ffbe
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 230 deletions.
29 changes: 29 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 0 additions & 1 deletion spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => {

it('should only count', async () => {
await prepareData();

await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();

const where = {
Expand Down
2 changes: 1 addition & 1 deletion spec/ParseRole.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
44 changes: 24 additions & 20 deletions spec/RestQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 => {
Expand Down
32 changes: 32 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
61 changes: 46 additions & 15 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
};
Expand Down
11 changes: 9 additions & 2 deletions src/Controllers/PushController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 18 additions & 5 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -123,7 +129,7 @@ export class UserController extends AdaptableController {
});
}

getUserIfNeeded(user) {
async getUserIfNeeded(user) {
if (user.username && user.email) {
return Promise.resolve(user);
}
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 739ffbe

Please sign in to comment.