Skip to content

Commit

Permalink
Validates permission before calling beforeSave trigger (#5546)
Browse files Browse the repository at this point in the history
* Test to reproduce the problem

* Validating update before calling beforeSave trigger

* Fixing lint

* Commenting code

* Improving the code
  • Loading branch information
davimacedo authored and acinader committed May 11, 2019
1 parent 2cc21bf commit 90c81c1
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 2 deletions.
210 changes: 210 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
const Config = require('../lib/Config');
const Parse = require('parse/node');
const request = require('../lib/request');
const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter')
Expand Down Expand Up @@ -239,6 +240,215 @@ describe('Cloud Code', () => {
});
});

it('beforeSave should be called only if user fulfills permissions', async () => {
const triggeruser = new Parse.User();
triggeruser.setUsername('triggeruser');
triggeruser.setPassword('triggeruser');
await triggeruser.signUp();

const triggeruser2 = new Parse.User();
triggeruser2.setUsername('triggeruser2');
triggeruser2.setPassword('triggeruser2');
await triggeruser2.signUp();

const triggeruser3 = new Parse.User();
triggeruser3.setUsername('triggeruser3');
triggeruser3.setPassword('triggeruser3');
await triggeruser3.signUp();

const triggeruser4 = new Parse.User();
triggeruser4.setUsername('triggeruser4');
triggeruser4.setPassword('triggeruser4');
await triggeruser4.signUp();

const triggeruser5 = new Parse.User();
triggeruser5.setUsername('triggeruser5');
triggeruser5.setPassword('triggeruser5');
await triggeruser5.signUp();

const triggerroleacl = new Parse.ACL();
triggerroleacl.setPublicReadAccess(true);

const triggerrole = new Parse.Role();
triggerrole.setName('triggerrole');
triggerrole.setACL(triggerroleacl);
triggerrole.getUsers().add(triggeruser);
triggerrole.getUsers().add(triggeruser3);
await triggerrole.save();

const config = Config.get('test');
const schema = await config.database.loadSchema();
await schema.addClassIfNotExists(
'triggerclass',
{
someField: { type: 'String' },
pointerToUser: { type: 'Pointer', targetClass: '_User' },
},
{
find: {
'role:triggerrole': true,
[triggeruser.id]: true,
[triggeruser2.id]: true,
},
create: {
'role:triggerrole': true,
[triggeruser.id]: true,
[triggeruser2.id]: true,
},
get: {
'role:triggerrole': true,
[triggeruser.id]: true,
[triggeruser2.id]: true,
},
update: {
'role:triggerrole': true,
[triggeruser.id]: true,
[triggeruser2.id]: true,
},
addField: {
'role:triggerrole': true,
[triggeruser.id]: true,
[triggeruser2.id]: true,
},
delete: {
'role:triggerrole': true,
[triggeruser.id]: true,
[triggeruser2.id]: true,
},
readUserFields: ['pointerToUser'],
writeUserFields: ['pointerToUser'],
},
{}
);

let called = 0;
Parse.Cloud.beforeSave('triggerclass', () => {
called++;
});

const triggerobject = new Parse.Object('triggerclass');
triggerobject.set('someField', 'someValue');
triggerobject.set('someField2', 'someValue');
const triggerobjectacl = new Parse.ACL();
triggerobjectacl.setPublicReadAccess(false);
triggerobjectacl.setPublicWriteAccess(false);
triggerobjectacl.setRoleReadAccess(triggerrole, true);
triggerobjectacl.setRoleWriteAccess(triggerrole, true);
triggerobjectacl.setReadAccess(triggeruser.id, true);
triggerobjectacl.setWriteAccess(triggeruser.id, true);
triggerobjectacl.setReadAccess(triggeruser2.id, true);
triggerobjectacl.setWriteAccess(triggeruser2.id, true);
triggerobject.setACL(triggerobjectacl);

await triggerobject.save(undefined, {
sessionToken: triggeruser.getSessionToken(),
});
expect(called).toBe(1);
await triggerobject.save(undefined, {
sessionToken: triggeruser.getSessionToken(),
});
expect(called).toBe(2);
await triggerobject.save(undefined, {
sessionToken: triggeruser2.getSessionToken(),
});
expect(called).toBe(3);
await triggerobject.save(undefined, {
sessionToken: triggeruser3.getSessionToken(),
});
expect(called).toBe(4);

const triggerobject2 = new Parse.Object('triggerclass');
triggerobject2.set('someField', 'someValue');
triggerobject2.set('someField22', 'someValue');
const triggerobjectacl2 = new Parse.ACL();
triggerobjectacl2.setPublicReadAccess(false);
triggerobjectacl2.setPublicWriteAccess(false);
triggerobjectacl2.setReadAccess(triggeruser.id, true);
triggerobjectacl2.setWriteAccess(triggeruser.id, true);
triggerobjectacl2.setReadAccess(triggeruser2.id, true);
triggerobjectacl2.setWriteAccess(triggeruser2.id, true);
triggerobjectacl2.setReadAccess(triggeruser5.id, true);
triggerobjectacl2.setWriteAccess(triggeruser5.id, true);
triggerobject2.setACL(triggerobjectacl2);

await triggerobject2.save(undefined, {
sessionToken: triggeruser2.getSessionToken(),
});
expect(called).toBe(5);
await triggerobject2.save(undefined, {
sessionToken: triggeruser2.getSessionToken(),
});
expect(called).toBe(6);
await triggerobject2.save(undefined, {
sessionToken: triggeruser.getSessionToken(),
});
expect(called).toBe(7);

let catched = false;
try {
await triggerobject2.save(undefined, {
sessionToken: triggeruser3.getSessionToken(),
});
} catch (e) {
catched = true;
expect(e.code).toBe(101);
}
expect(catched).toBe(true);
expect(called).toBe(7);

catched = false;
try {
await triggerobject2.save(undefined, {
sessionToken: triggeruser4.getSessionToken(),
});
} catch (e) {
catched = true;
expect(e.code).toBe(101);
}
expect(catched).toBe(true);
expect(called).toBe(7);

catched = false;
try {
await triggerobject2.save(undefined, {
sessionToken: triggeruser5.getSessionToken(),
});
} catch (e) {
catched = true;
expect(e.code).toBe(101);
}
expect(catched).toBe(true);
expect(called).toBe(7);

const triggerobject3 = new Parse.Object('triggerclass');
triggerobject3.set('someField', 'someValue');
triggerobject3.set('someField33', 'someValue');

catched = false;
try {
await triggerobject3.save(undefined, {
sessionToken: triggeruser4.getSessionToken(),
});
} catch (e) {
catched = true;
expect(e.code).toBe(119);
}
expect(catched).toBe(true);
expect(called).toBe(7);

catched = false;
try {
await triggerobject3.save(undefined, {
sessionToken: triggeruser5.getSessionToken(),
});
} catch (e) {
catched = true;
expect(e.code).toBe(119);
}
expect(catched).toBe(true);
expect(called).toBe(7);
});

it('test afterSave ran and created an object', function(done) {
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
const obj = new Parse.Object('AfterSaveProof');
Expand Down
28 changes: 26 additions & 2 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@ class DatabaseController {
query: any,
update: any,
{ acl, many, upsert }: FullQueryOptions = {},
skipSanitization: boolean = false
skipSanitization: boolean = false,
validateOnly: boolean = false
): Promise<any> {
const originalQuery = query;
const originalUpdate = update;
Expand Down Expand Up @@ -557,6 +558,19 @@ class DatabaseController {
}
update = transformObjectACL(update);
transformAuthData(className, update, schema);
if (validateOnly) {
return this.adapter
.find(className, schema, query, {})
.then(result => {
if (!result || !result.length) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'
);
}
return {};
});
}
if (many) {
return this.adapter.updateObjectsByQuery(
className,
Expand Down Expand Up @@ -588,6 +602,9 @@ class DatabaseController {
'Object not found.'
);
}
if (validateOnly) {
return result;
}
return this.handleRelationUpdates(
className,
originalQuery.objectId,
Expand Down Expand Up @@ -802,7 +819,8 @@ class DatabaseController {
create(
className: string,
object: any,
{ acl }: QueryOptions = {}
{ acl }: QueryOptions = {},
validateOnly: boolean = false
): Promise<any> {
// Make a copy of the object, so we don't mutate the incoming data.
const originalObject = object;
Expand Down Expand Up @@ -831,13 +849,19 @@ class DatabaseController {
.then(schema => {
transformAuthData(className, object, schema);
flattenUpdateOperatorsForCreate(object);
if (validateOnly) {
return {};
}
return this.adapter.createObject(
className,
SchemaController.convertSchemaToAdapterSchema(schema),
object
);
})
.then(result => {
if (validateOnly) {
return originalObject;
}
return this.handleRelationUpdates(
className,
object.objectId,
Expand Down
32 changes: 32 additions & 0 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,38 @@ RestWrite.prototype.runBeforeSaveTrigger = function() {
}

return Promise.resolve()
.then(() => {
// Before calling the trigger, validate the permissions for the save operation
let databasePromise = null;
if (this.query) {
// Validate for updating
databasePromise = this.config.database.update(
this.className,
this.query,
this.data,
this.runOptions,
false,
true
);
} else {
// Validate for creating
databasePromise = this.config.database.create(
this.className,
this.data,
this.runOptions,
true
);
}
// In the case that there is no permission for the operation, it throws an error
return databasePromise.then(result => {
if (!result || result.length <= 0) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'
);
}
});
})
.then(() => {
return triggers.maybeRunTrigger(
triggers.Types.beforeSave,
Expand Down

0 comments on commit 90c81c1

Please sign in to comment.