Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Parse Pointer allows to access internal Parse Server classes and circumvent beforeFind query trigger #8732

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2342,6 +2342,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 @@ -5269,7 +5269,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 @@ -398,15 +398,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 @@ -437,34 +438,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
48 changes: 39 additions & 9 deletions src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ const getAuthForSessionToken = async function ({
include: 'user',
};

const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
const query = await RestQuery({
method: RestQuery.Method.get,
runBeforeFind: false,
config,
auth: master(config),
className: '_Session',
restWhere: { sessionToken },
restOptions,
});
results = (await query.execute()).results;
} else {
results = (
Expand Down Expand Up @@ -121,11 +129,19 @@ const getAuthForSessionToken = async function ({
});
};

var getAuthForLegacySessionToken = function ({ config, sessionToken, installationId }) {
var getAuthForLegacySessionToken = async function ({ config, sessionToken, installationId }) {
var restOptions = {
limit: 1,
};
var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
var query = await RestQuery({
method: RestQuery.Method.get,
runBeforeFind: false,
config,
auth: master(config),
className: '_User',
restWhere: { sessionToken },
restOptions,
});
return query.execute().then(response => {
var results = response.results;
if (results.length !== 1) {
Expand Down Expand Up @@ -169,9 +185,16 @@ Auth.prototype.getRolesForUser = async function () {
objectId: this.user.id,
},
};
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,
auth: master(this.config),
runBeforeFind: false,
className: '_Role',
restWhere,
restOptions: {},
});
await query.each(result => results.push(result));
} else {
await new Parse.Query(Parse.Role)
.equalTo('users', this.user)
Expand Down Expand Up @@ -262,9 +285,16 @@ Auth.prototype.getRolesByIds = async function (ins) {
};
});
const restWhere = { roles: { $in: roles } };
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,
restOptions: {},
});
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
27 changes: 18 additions & 9 deletions src/Controllers/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class UserController extends AdaptableController {
}
}

verifyEmail(username, token) {
async verifyEmail(username, token) {
if (!this.shouldVerifyEmails) {
// Trying to verify email when not enabled
// TODO: Better error here.
Expand All @@ -70,12 +70,14 @@ export class UserController extends AdaptableController {
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
}
const masterAuth = Auth.master(this.config);
var findUserForEmailVerification = new RestQuery(
this.config,
Auth.master(this.config),
'_User',
{ username: username }
);
var findUserForEmailVerification = await RestQuery({
method: RestQuery.Method.get,
config: this.config,
runBeforeFind: false,
auth: Auth.master(this.config),
className: '_User',
restWhere: { username },
});
return findUserForEmailVerification.execute().then(result => {
if (result.results.length && result.results[0].emailVerified) {
return Promise.resolve(result.results.length[0]);
Expand Down Expand Up @@ -112,7 +114,7 @@ export class UserController extends AdaptableController {
});
}

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