From 11b67c99b021122c0254e12a71203cb98129bc56 Mon Sep 17 00:00:00 2001 From: Addison Elliott Date: Thu, 23 Nov 2017 14:40:51 -0600 Subject: [PATCH 1/5] Allow Session triggers --- spec/CloudCode.spec.js | 6 ------ src/triggers.js | 4 ---- 2 files changed, 10 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7ee53ca27c..90b35e9995 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1780,12 +1780,6 @@ describe('afterFind hooks', () => { }); it('should validate triggers correctly', () => { - expect(() => { - Parse.Cloud.beforeSave('_Session', () => {}); - }).toThrow('Triggers are not supported for _Session class.'); - expect(() => { - Parse.Cloud.afterSave('_Session', () => {}); - }).toThrow('Triggers are not supported for _Session class.'); expect(() => { Parse.Cloud.beforeSave('_PushStatus', () => {}); }).toThrow('Only afterSave is allowed on _PushStatus'); diff --git a/src/triggers.js b/src/triggers.js index a11a71f7f4..74f3d158e8 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -31,10 +31,6 @@ const baseStore = function() { }; function validateClassNameForTriggers(className, type) { - const restrictedClassNames = [ '_Session' ]; - if (restrictedClassNames.indexOf(className) != -1) { - throw `Triggers are not supported for ${className} class.`; - } if (type == Types.beforeSave && className === '_PushStatus') { // _PushStatus uses undocumented nested key increment ops // allowing beforeSave would mess up the objects big time From ec94e33b54e449bff462e87509d63d51ff20dbf4 Mon Sep 17 00:00:00 2001 From: Addison Elliott Date: Fri, 24 Nov 2017 22:43:37 -0600 Subject: [PATCH 2/5] Restore tests for validating _Session triggers and take not of this Instead of removing the tests for validating _Session triggers, I restored them and made sure that having a _Session trigger does NOT throw an exception --- spec/CloudCode.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 90b35e9995..0b6cf6f6a8 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1780,6 +1780,12 @@ describe('afterFind hooks', () => { }); it('should validate triggers correctly', () => { + expect(() => { + Parse.Cloud.beforeSave('_Session', () => {}); + }).not.toThrow('Triggers are not supported for _Session class.'); + expect(() => { + Parse.Cloud.afterSave('_Session', () => {}); + }).not.toThrow('Triggers are not supported for _Session class.'); expect(() => { Parse.Cloud.beforeSave('_PushStatus', () => {}); }).toThrow('Only afterSave is allowed on _PushStatus'); From 83c7602b37230e2baeef7e09e4d86a491dbf190d Mon Sep 17 00:00:00 2001 From: Addison Elliott Date: Tue, 28 Nov 2017 22:37:54 -0600 Subject: [PATCH 3/5] [WIP] Adding support for _Session class triggers --- spec/CloudCode.spec.js | 16 ++++++++++++++++ src/RestWrite.js | 2 +- src/Routers/PublicAPIRouter.js | 2 +- src/triggers.js | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 0b6cf6f6a8..7e77554020 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1778,7 +1778,9 @@ describe('afterFind hooks', () => { }) .then(() => done()); }); +}); +describe('Trigger Testing', () => { it('should validate triggers correctly', () => { expect(() => { Parse.Cloud.beforeSave('_Session', () => {}); @@ -1793,4 +1795,18 @@ describe('afterFind hooks', () => { Parse.Cloud.afterSave('_PushStatus', () => {}); }).not.toThrow(); }); + + it('beforeSave _Session should not modify class', (done) => { + Parse.Cloud.beforeSave('_Session', (req, res) => { + req.object.set("") + res.success(); + + expect(req.headers).toBeDefined(); + res.success(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + myObject.save().then(() => done()); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 424284d5ba..51fa3cdcc4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -167,7 +167,7 @@ RestWrite.prototype.runBeforeTrigger = function() { return Promise.resolve().then(() => { return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config); }).then((response) => { - if (response && response.object) { + if (this.className !== '_Session' && response && response.object) { this.storage.fieldsChangedByTrigger = _.reduce(response.object, (result, value, key) => { if (!_.isEqual(this.data[key], value)) { result.push(key); diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index a126423cb0..b43ceb16ff 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -15,7 +15,7 @@ export class PublicAPIRouter extends PromiseRouter { const appId = req.params.appId; const config = Config.get(appId); - if(!config){ + if(!config) { this.invalidRequest(); } diff --git a/src/triggers.js b/src/triggers.js index 74f3d158e8..74725e59ec 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -199,7 +199,7 @@ export function getResponseObject(request, resolve, reject) { return { success: function(response) { if (request.triggerName === Types.afterFind) { - if(!response){ + if(!response) { response = request.objects; } response = response.map(object => { From 2af1cfba3ce4ab8916d454e09d01a895b9aa0614 Mon Sep 17 00:00:00 2001 From: Addison Elliott Date: Thu, 30 Nov 2017 16:14:55 -0600 Subject: [PATCH 4/5] Altered buildUpdatedObjects to combine the extra data, original object and new object into one REST array and then convert to ParseObject. Previously, it would convert the extraData and original object into a ParseObject and copy data from new array over. This has the side effect of failing for _Session class because the attributes are read only. Rename sanitizedData to sanitizeData because sanitizedData implies the data will be returned. Instead, sanitizeData mutates the data field to remove any sensitive information. Create expandData function which mutates the data field and expands any keys that use dot notation ('x.y') --- spec/CloudCode.spec.js | 51 +++++++++++++++++++++++++++++++++++++----- src/RestWrite.js | 24 +++++++++++--------- src/triggers.js | 5 ++++- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7e77554020..76e8386d9b 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1782,6 +1782,12 @@ describe('afterFind hooks', () => { describe('Trigger Testing', () => { it('should validate triggers correctly', () => { + expect(() => { + Parse.Cloud.beforeFind('_Session', () => {}); + }).toThrow('beforeFind and afterFind triggers are not allowed for _Session class.'); + expect(() => { + Parse.Cloud.afterFind('_Session', () => {}); + }).toThrow('beforeFind and afterFind triggers are not allowed for _Session class.'); expect(() => { Parse.Cloud.beforeSave('_Session', () => {}); }).not.toThrow('Triggers are not supported for _Session class.'); @@ -1797,16 +1803,49 @@ describe('Trigger Testing', () => { }); it('beforeSave _Session should not modify class', (done) => { + var hasCalled = false; Parse.Cloud.beforeSave('_Session', (req, res) => { - req.object.set("") - res.success(); + req.object.set('foo', 'bing'); + expect(() => { + req.object.set('createdWith', 'test') + }).toThrow(); + expect(() => { + req.object.set('expiresAt', new Date()) + }).toThrow(); + expect(() => { + req.object.set('installationId', 'test') + }).toThrow(); + expect(() => { + req.object.set('restricted', 'test') + }).toThrow(); + expect(() => { + req.object.set('sessionToken', 'test') + }).toThrow(); + expect(() => { + req.object.set('user', null) + }).toThrow(); - expect(req.headers).toBeDefined(); + hasCalled = true; res.success(); }); - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - myObject.save().then(() => done()); + var user = new Parse.User(); + user.set("username", "zxcv"); + user.set("email", "asdf@example.com"); + user.set("password", "asdf"); + user.signUp(null, { + success: function() { + Parse.Session.current().then((result) => { + expect(hasCalled).toBe(true); + expect(result).toBeDefined(); + expect(result.get('foo')).toBeUndefined(); + done(); + }); + }, + error: function() { + fail('Failed to save user'); + done(); + } + }); }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 51fa3cdcc4..a347c427d5 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1161,38 +1161,42 @@ RestWrite.prototype.objectId = function() { }; // Returns a copy of the data and delete bad keys (_auth_data, _hashed_password...) -RestWrite.prototype.sanitizedData = function() { - const data = Object.keys(this.data).reduce((data, key) => { +RestWrite.prototype.sanitizeData = function() { + Object.keys(this.data).reduce((data, key) => { // Regexp comes from Parse.Object.prototype.validate if (!(/^[A-Za-z][0-9A-Za-z_]*$/).test(key)) { delete data[key]; } return data; }, deepcopy(this.data)); - return Parse._decode(undefined, data); } -// Returns an updated copy of the object -RestWrite.prototype.buildUpdatedObject = function (extraData) { - const updatedObject = triggers.inflate(extraData, this.originalData); +// Expand dot notation keys to their full JSON equivalent +// e.g. 'x.y':v => 'x':{'y':v} +RestWrite.prototype.expandData = function() { Object.keys(this.data).reduce(function (data, key) { if (key.indexOf(".") > 0) { // subdocument key with dot notation ('x.y':v => 'x':{'y':v}) const splittedKey = key.split("."); const parentProp = splittedKey[0]; - let parentVal = updatedObject.get(parentProp); + let parentVal = this.data[parentProp]; if(typeof parentVal !== 'object') { parentVal = {}; } parentVal[splittedKey[1]] = data[key]; - updatedObject.set(parentProp, parentVal); + data[parentProp] = parentVal; delete data[key]; } return data; }, deepcopy(this.data)); +} - updatedObject.set(this.sanitizedData()); - return updatedObject; +// Returns an updated copy of the object +RestWrite.prototype.buildUpdatedObject = function (extraData) { + this.expandData(); + this.sanitizeData(); + var updatedObject = Object.assign(extraData, this.originalData, this.data); + return Parse.Object.fromJSON(updatedObject); }; RestWrite.prototype.cleanUserAuthData = function() { diff --git a/src/triggers.js b/src/triggers.js index 74725e59ec..fb6674effe 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -31,7 +31,10 @@ const baseStore = function() { }; function validateClassNameForTriggers(className, type) { - if (type == Types.beforeSave && className === '_PushStatus') { + if ((type == Types.beforeFind || type == Types.afterFind) && className === '_Session') { + throw 'beforeFind and afterFind triggers are not allowed for _Session class.'; + } + else if (type == Types.beforeSave && className === '_PushStatus') { // _PushStatus uses undocumented nested key increment ops // allowing beforeSave would mess up the objects big time // TODO: Allow proper documented way of using nested increment ops From b00c05c13dc8002e514680c60738f02705ceacec Mon Sep 17 00:00:00 2001 From: Addison Elliott Date: Thu, 30 Nov 2017 17:37:08 -0600 Subject: [PATCH 5/5] Fix issue with buildUpdatedObject function. Had to revert many of my changes since they wouldn't work --- spec/CloudCode.spec.js | 8 ++++---- src/RestWrite.js | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 76e8386d9b..3854b9241e 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -181,12 +181,12 @@ describe('Cloud Code', () => { it('test afterSave ran on created object and returned a promise', function(done) { Parse.Cloud.afterSave('AfterSaveTest2', function(req) { const obj = req.object; - if(!obj.existed()) + if (!obj.existed()) { const promise = new Parse.Promise(); - setTimeout(function(){ + setTimeout(function() { obj.set('proof', obj.id); - obj.save().then(function(){ + obj.save().then(function() { promise.resolve(); }); }, 1000); @@ -196,7 +196,7 @@ describe('Cloud Code', () => { }); const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then(function(){ + obj.save().then(function() { const query = new Parse.Query('AfterSaveTest2'); query.equalTo('proof', obj.id); query.find().then(function(results) { diff --git a/src/RestWrite.js b/src/RestWrite.js index a347c427d5..5a904c95bf 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1169,34 +1169,35 @@ RestWrite.prototype.sanitizeData = function() { } return data; }, deepcopy(this.data)); + + return this.data; } -// Expand dot notation keys to their full JSON equivalent -// e.g. 'x.y':v => 'x':{'y':v} -RestWrite.prototype.expandData = function() { +// Returns an updated copy of the object +RestWrite.prototype.buildUpdatedObject = function (extraData) { + if (extraData.className == '_Session') { + return triggers.inflate(extraData, this.sanitizeData()); + } + + const updatedObject = triggers.inflate(extraData, this.originalData); Object.keys(this.data).reduce(function (data, key) { if (key.indexOf(".") > 0) { // subdocument key with dot notation ('x.y':v => 'x':{'y':v}) const splittedKey = key.split("."); const parentProp = splittedKey[0]; - let parentVal = this.data[parentProp]; + let parentVal = updatedObject.get(parentProp); if(typeof parentVal !== 'object') { parentVal = {}; } parentVal[splittedKey[1]] = data[key]; - data[parentProp] = parentVal; + updatedObject.set(parentProp, parentVal); delete data[key]; } return data; }, deepcopy(this.data)); -} -// Returns an updated copy of the object -RestWrite.prototype.buildUpdatedObject = function (extraData) { - this.expandData(); - this.sanitizeData(); - var updatedObject = Object.assign(extraData, this.originalData, this.data); - return Parse.Object.fromJSON(updatedObject); + updatedObject.set(Parse._decode(undefined, this.sanitizeData())); + return updatedObject; }; RestWrite.prototype.cleanUserAuthData = function() {