diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index af9de4ef..54bdbc49 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -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') @@ -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'); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index cbbce0a0..0b4bb2ab 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -476,7 +476,8 @@ class DatabaseController { query: any, update: any, { acl, many, upsert }: FullQueryOptions = {}, - skipSanitization: boolean = false + skipSanitization: boolean = false, + validateOnly: boolean = false ): Promise { const originalQuery = query; const originalUpdate = update; @@ -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, @@ -588,6 +602,9 @@ class DatabaseController { 'Object not found.' ); } + if (validateOnly) { + return result; + } return this.handleRelationUpdates( className, originalQuery.objectId, @@ -802,7 +819,8 @@ class DatabaseController { create( className: string, object: any, - { acl }: QueryOptions = {} + { acl }: QueryOptions = {}, + validateOnly: boolean = false ): Promise { // Make a copy of the object, so we don't mutate the incoming data. const originalObject = object; @@ -831,6 +849,9 @@ class DatabaseController { .then(schema => { transformAuthData(className, object, schema); flattenUpdateOperatorsForCreate(object); + if (validateOnly) { + return {}; + } return this.adapter.createObject( className, SchemaController.convertSchemaToAdapterSchema(schema), @@ -838,6 +859,9 @@ class DatabaseController { ); }) .then(result => { + if (validateOnly) { + return originalObject; + } return this.handleRelationUpdates( className, object.objectId, diff --git a/src/RestWrite.js b/src/RestWrite.js index 09fa497b..22df2a97 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -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,