Skip to content

Commit d2badbf

Browse files
davimacedoacinader
authored andcommitted
Validates permission before calling beforeSave trigger (parse-community#5546)
* Test to reproduce the problem * Validating update before calling beforeSave trigger * Fixing lint * Commenting code * Improving the code
1 parent 78b0665 commit d2badbf

File tree

3 files changed

+268
-2
lines changed

3 files changed

+268
-2
lines changed

spec/CloudCode.spec.js

+210
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const Config = require('../lib/Config');
23
const Parse = require('parse/node');
34
const request = require('../lib/request');
45
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
@@ -239,6 +240,215 @@ describe('Cloud Code', () => {
239240
});
240241
});
241242

243+
it('beforeSave should be called only if user fulfills permissions', async () => {
244+
const triggeruser = new Parse.User();
245+
triggeruser.setUsername('triggeruser');
246+
triggeruser.setPassword('triggeruser');
247+
await triggeruser.signUp();
248+
249+
const triggeruser2 = new Parse.User();
250+
triggeruser2.setUsername('triggeruser2');
251+
triggeruser2.setPassword('triggeruser2');
252+
await triggeruser2.signUp();
253+
254+
const triggeruser3 = new Parse.User();
255+
triggeruser3.setUsername('triggeruser3');
256+
triggeruser3.setPassword('triggeruser3');
257+
await triggeruser3.signUp();
258+
259+
const triggeruser4 = new Parse.User();
260+
triggeruser4.setUsername('triggeruser4');
261+
triggeruser4.setPassword('triggeruser4');
262+
await triggeruser4.signUp();
263+
264+
const triggeruser5 = new Parse.User();
265+
triggeruser5.setUsername('triggeruser5');
266+
triggeruser5.setPassword('triggeruser5');
267+
await triggeruser5.signUp();
268+
269+
const triggerroleacl = new Parse.ACL();
270+
triggerroleacl.setPublicReadAccess(true);
271+
272+
const triggerrole = new Parse.Role();
273+
triggerrole.setName('triggerrole');
274+
triggerrole.setACL(triggerroleacl);
275+
triggerrole.getUsers().add(triggeruser);
276+
triggerrole.getUsers().add(triggeruser3);
277+
await triggerrole.save();
278+
279+
const config = Config.get('test');
280+
const schema = await config.database.loadSchema();
281+
await schema.addClassIfNotExists(
282+
'triggerclass',
283+
{
284+
someField: { type: 'String' },
285+
pointerToUser: { type: 'Pointer', targetClass: '_User' },
286+
},
287+
{
288+
find: {
289+
'role:triggerrole': true,
290+
[triggeruser.id]: true,
291+
[triggeruser2.id]: true,
292+
},
293+
create: {
294+
'role:triggerrole': true,
295+
[triggeruser.id]: true,
296+
[triggeruser2.id]: true,
297+
},
298+
get: {
299+
'role:triggerrole': true,
300+
[triggeruser.id]: true,
301+
[triggeruser2.id]: true,
302+
},
303+
update: {
304+
'role:triggerrole': true,
305+
[triggeruser.id]: true,
306+
[triggeruser2.id]: true,
307+
},
308+
addField: {
309+
'role:triggerrole': true,
310+
[triggeruser.id]: true,
311+
[triggeruser2.id]: true,
312+
},
313+
delete: {
314+
'role:triggerrole': true,
315+
[triggeruser.id]: true,
316+
[triggeruser2.id]: true,
317+
},
318+
readUserFields: ['pointerToUser'],
319+
writeUserFields: ['pointerToUser'],
320+
},
321+
{}
322+
);
323+
324+
let called = 0;
325+
Parse.Cloud.beforeSave('triggerclass', () => {
326+
called++;
327+
});
328+
329+
const triggerobject = new Parse.Object('triggerclass');
330+
triggerobject.set('someField', 'someValue');
331+
triggerobject.set('someField2', 'someValue');
332+
const triggerobjectacl = new Parse.ACL();
333+
triggerobjectacl.setPublicReadAccess(false);
334+
triggerobjectacl.setPublicWriteAccess(false);
335+
triggerobjectacl.setRoleReadAccess(triggerrole, true);
336+
triggerobjectacl.setRoleWriteAccess(triggerrole, true);
337+
triggerobjectacl.setReadAccess(triggeruser.id, true);
338+
triggerobjectacl.setWriteAccess(triggeruser.id, true);
339+
triggerobjectacl.setReadAccess(triggeruser2.id, true);
340+
triggerobjectacl.setWriteAccess(triggeruser2.id, true);
341+
triggerobject.setACL(triggerobjectacl);
342+
343+
await triggerobject.save(undefined, {
344+
sessionToken: triggeruser.getSessionToken(),
345+
});
346+
expect(called).toBe(1);
347+
await triggerobject.save(undefined, {
348+
sessionToken: triggeruser.getSessionToken(),
349+
});
350+
expect(called).toBe(2);
351+
await triggerobject.save(undefined, {
352+
sessionToken: triggeruser2.getSessionToken(),
353+
});
354+
expect(called).toBe(3);
355+
await triggerobject.save(undefined, {
356+
sessionToken: triggeruser3.getSessionToken(),
357+
});
358+
expect(called).toBe(4);
359+
360+
const triggerobject2 = new Parse.Object('triggerclass');
361+
triggerobject2.set('someField', 'someValue');
362+
triggerobject2.set('someField22', 'someValue');
363+
const triggerobjectacl2 = new Parse.ACL();
364+
triggerobjectacl2.setPublicReadAccess(false);
365+
triggerobjectacl2.setPublicWriteAccess(false);
366+
triggerobjectacl2.setReadAccess(triggeruser.id, true);
367+
triggerobjectacl2.setWriteAccess(triggeruser.id, true);
368+
triggerobjectacl2.setReadAccess(triggeruser2.id, true);
369+
triggerobjectacl2.setWriteAccess(triggeruser2.id, true);
370+
triggerobjectacl2.setReadAccess(triggeruser5.id, true);
371+
triggerobjectacl2.setWriteAccess(triggeruser5.id, true);
372+
triggerobject2.setACL(triggerobjectacl2);
373+
374+
await triggerobject2.save(undefined, {
375+
sessionToken: triggeruser2.getSessionToken(),
376+
});
377+
expect(called).toBe(5);
378+
await triggerobject2.save(undefined, {
379+
sessionToken: triggeruser2.getSessionToken(),
380+
});
381+
expect(called).toBe(6);
382+
await triggerobject2.save(undefined, {
383+
sessionToken: triggeruser.getSessionToken(),
384+
});
385+
expect(called).toBe(7);
386+
387+
let catched = false;
388+
try {
389+
await triggerobject2.save(undefined, {
390+
sessionToken: triggeruser3.getSessionToken(),
391+
});
392+
} catch (e) {
393+
catched = true;
394+
expect(e.code).toBe(101);
395+
}
396+
expect(catched).toBe(true);
397+
expect(called).toBe(7);
398+
399+
catched = false;
400+
try {
401+
await triggerobject2.save(undefined, {
402+
sessionToken: triggeruser4.getSessionToken(),
403+
});
404+
} catch (e) {
405+
catched = true;
406+
expect(e.code).toBe(101);
407+
}
408+
expect(catched).toBe(true);
409+
expect(called).toBe(7);
410+
411+
catched = false;
412+
try {
413+
await triggerobject2.save(undefined, {
414+
sessionToken: triggeruser5.getSessionToken(),
415+
});
416+
} catch (e) {
417+
catched = true;
418+
expect(e.code).toBe(101);
419+
}
420+
expect(catched).toBe(true);
421+
expect(called).toBe(7);
422+
423+
const triggerobject3 = new Parse.Object('triggerclass');
424+
triggerobject3.set('someField', 'someValue');
425+
triggerobject3.set('someField33', 'someValue');
426+
427+
catched = false;
428+
try {
429+
await triggerobject3.save(undefined, {
430+
sessionToken: triggeruser4.getSessionToken(),
431+
});
432+
} catch (e) {
433+
catched = true;
434+
expect(e.code).toBe(119);
435+
}
436+
expect(catched).toBe(true);
437+
expect(called).toBe(7);
438+
439+
catched = false;
440+
try {
441+
await triggerobject3.save(undefined, {
442+
sessionToken: triggeruser5.getSessionToken(),
443+
});
444+
} catch (e) {
445+
catched = true;
446+
expect(e.code).toBe(119);
447+
}
448+
expect(catched).toBe(true);
449+
expect(called).toBe(7);
450+
});
451+
242452
it('test afterSave ran and created an object', function(done) {
243453
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
244454
const obj = new Parse.Object('AfterSaveProof');

src/Controllers/DatabaseController.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,8 @@ class DatabaseController {
476476
query: any,
477477
update: any,
478478
{ acl, many, upsert }: FullQueryOptions = {},
479-
skipSanitization: boolean = false
479+
skipSanitization: boolean = false,
480+
validateOnly: boolean = false
480481
): Promise<any> {
481482
const originalQuery = query;
482483
const originalUpdate = update;
@@ -557,6 +558,19 @@ class DatabaseController {
557558
}
558559
update = transformObjectACL(update);
559560
transformAuthData(className, update, schema);
561+
if (validateOnly) {
562+
return this.adapter
563+
.find(className, schema, query, {})
564+
.then(result => {
565+
if (!result || !result.length) {
566+
throw new Parse.Error(
567+
Parse.Error.OBJECT_NOT_FOUND,
568+
'Object not found.'
569+
);
570+
}
571+
return {};
572+
});
573+
}
560574
if (many) {
561575
return this.adapter.updateObjectsByQuery(
562576
className,
@@ -588,6 +602,9 @@ class DatabaseController {
588602
'Object not found.'
589603
);
590604
}
605+
if (validateOnly) {
606+
return result;
607+
}
591608
return this.handleRelationUpdates(
592609
className,
593610
originalQuery.objectId,
@@ -802,7 +819,8 @@ class DatabaseController {
802819
create(
803820
className: string,
804821
object: any,
805-
{ acl }: QueryOptions = {}
822+
{ acl }: QueryOptions = {},
823+
validateOnly: boolean = false
806824
): Promise<any> {
807825
// Make a copy of the object, so we don't mutate the incoming data.
808826
const originalObject = object;
@@ -831,13 +849,19 @@ class DatabaseController {
831849
.then(schema => {
832850
transformAuthData(className, object, schema);
833851
flattenUpdateOperatorsForCreate(object);
852+
if (validateOnly) {
853+
return {};
854+
}
834855
return this.adapter.createObject(
835856
className,
836857
SchemaController.convertSchemaToAdapterSchema(schema),
837858
object
838859
);
839860
})
840861
.then(result => {
862+
if (validateOnly) {
863+
return originalObject;
864+
}
841865
return this.handleRelationUpdates(
842866
className,
843867
object.objectId,

src/RestWrite.js

+32
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,38 @@ RestWrite.prototype.runBeforeSaveTrigger = function() {
220220
}
221221

222222
return Promise.resolve()
223+
.then(() => {
224+
// Before calling the trigger, validate the permissions for the save operation
225+
let databasePromise = null;
226+
if (this.query) {
227+
// Validate for updating
228+
databasePromise = this.config.database.update(
229+
this.className,
230+
this.query,
231+
this.data,
232+
this.runOptions,
233+
false,
234+
true
235+
);
236+
} else {
237+
// Validate for creating
238+
databasePromise = this.config.database.create(
239+
this.className,
240+
this.data,
241+
this.runOptions,
242+
true
243+
);
244+
}
245+
// In the case that there is no permission for the operation, it throws an error
246+
return databasePromise.then(result => {
247+
if (!result || result.length <= 0) {
248+
throw new Parse.Error(
249+
Parse.Error.OBJECT_NOT_FOUND,
250+
'Object not found.'
251+
);
252+
}
253+
});
254+
})
223255
.then(() => {
224256
return triggers.maybeRunTrigger(
225257
triggers.Types.beforeSave,

0 commit comments

Comments
 (0)