From caee281bc58901fea7cead21d6a9a0bb2022d91b Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 9 Oct 2021 02:24:33 +1100 Subject: [PATCH] fix: allow LiveQuery on Parse.Session (#7554) --- CHANGELOG.md | 1 + spec/ParseLiveQuery.spec.js | 54 +++++++++++++++++++++++++- src/Controllers/LiveQueryController.js | 6 ++- src/LiveQuery/ParseLiveQueryServer.js | 26 +++++++++++++ src/RestWrite.js | 19 ++++++++- src/cloud-code/Parse.Cloud.js | 29 ++++++-------- src/triggers.js | 7 ++++ 7 files changed, 120 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5f6f8dfe..42858f691a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -157,6 +157,7 @@ ___ - Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496) - Allow cloud string for ES modules (Daniel Blyth) [#7560](https://github.com/parse-community/parse-server/pull/7560) - docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562) +- Allow liveQuery on Session class (Daniel Blyth) [#7554](https://github.com/parse-community/parse-server/pull/7554) ## 4.10.4 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.3...4.10.4) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index dda6d7740b..2393467544 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -708,6 +708,58 @@ describe('ParseLiveQuery', function () { } }); + it('liveQuery on Session class', async done => { + await reconfigureServer({ + liveQuery: { classNames: [Parse.Session] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const user = new Parse.User(); + user.setUsername('username'); + user.setPassword('password'); + await user.signUp(); + + const query = new Parse.Query(Parse.Session); + const subscription = await query.subscribe(); + + subscription.on('create', async obj => { + await new Promise(resolve => setTimeout(resolve, 200)); + expect(obj.get('user').id).toBe(user.id); + expect(obj.get('createdWith')).toEqual({ action: 'login', authProvider: 'password' }); + expect(obj.get('expiresAt')).toBeInstanceOf(Date); + expect(obj.get('installationId')).toBeDefined(); + expect(obj.get('createdAt')).toBeInstanceOf(Date); + expect(obj.get('updatedAt')).toBeInstanceOf(Date); + done(); + }); + + await Parse.User.logIn('username', 'password'); + }); + + it('prevent liveQuery on Session class when not logged in', async done => { + await reconfigureServer({ + liveQuery: { + classNames: [Parse.Session], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + Parse.LiveQuery.on('error', error => { + expect(error).toBe('Invalid session token'); + }); + const query = new Parse.Query(Parse.Session); + const subscription = await query.subscribe(); + subscription.on('error', error => { + Parse.LiveQuery.removeAllListeners('error'); + expect(error).toBe('Invalid session token'); + done(); + }); + }); + it('handle invalid websocket payload length', async done => { await reconfigureServer({ liveQuery: { @@ -754,7 +806,7 @@ describe('ParseLiveQuery', function () { await reconfigureServer({ liveQuery: { - classNames: ['_User'], + classNames: [Parse.User], }, startLiveQueryServer: true, verbose: false, diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 34cf28893f..064084caa4 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -1,5 +1,6 @@ import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher'; import { LiveQueryOptions } from '../Options'; +import { getClassName } from './../triggers'; export class LiveQueryController { classNames: any; liveQueryPublisher: any; @@ -9,7 +10,10 @@ export class LiveQueryController { if (!config || !config.classNames) { this.classNames = new Set(); } else if (config.classNames instanceof Array) { - const classNames = config.classNames.map(name => new RegExp('^' + name + '$')); + const classNames = config.classNames.map(name => { + const _name = getClassName(name); + return new RegExp(`^${_name}$`); + }); this.classNames = new Set(classNames); } else { throw 'liveQuery.classes should be an array of string'; diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index f7a065b7f6..0091c459c0 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -729,10 +729,12 @@ class ParseLiveQueryServer { } const client = this.clients.get(parseWebsocket.clientId); const className = request.query.className; + let authCalled = false; try { const trigger = getTrigger(className, 'beforeSubscribe', Parse.applicationId); if (trigger) { const auth = await this.getAuthFromClient(client, request.requestId, request.sessionToken); + authCalled = true; if (auth && auth.user) { request.user = auth.user; } @@ -749,6 +751,30 @@ class ParseLiveQueryServer { request.query = query; } + if (className === '_Session') { + if (!authCalled) { + const auth = await this.getAuthFromClient( + client, + request.requestId, + request.sessionToken + ); + if (auth && auth.user) { + request.user = auth.user; + } + } + if (request.user) { + request.query.where.user = request.user.toPointer(); + } else if (!request.master) { + Client.pushError( + parseWebsocket, + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token', + false, + request.requestId + ); + return; + } + } // Get subscription from subscriptions, create one if necessary const subscriptionHash = queryHash(request.query); // Add className to subscriptions if necessary diff --git a/src/RestWrite.js b/src/RestWrite.js index c1f7ca2a26..a651cf9c6c 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1591,11 +1591,22 @@ RestWrite.prototype.sanitizedData = function () { // Returns an updated copy of the object RestWrite.prototype.buildUpdatedObject = function (extraData) { + const className = Parse.Object.fromJSON(extraData); + const readOnlyAttributes = className.constructor.readOnlyAttributes + ? className.constructor.readOnlyAttributes() + : []; + if (!this.originalData) { + for (const attribute of readOnlyAttributes) { + extraData[attribute] = this.data[attribute]; + } + } const updatedObject = triggers.inflate(extraData, this.originalData); Object.keys(this.data).reduce(function (data, key) { if (key.indexOf('.') > 0) { if (typeof data[key].__op === 'string') { - updatedObject.set(key, data[key]); + if (!readOnlyAttributes.includes(key)) { + updatedObject.set(key, data[key]); + } } else { // subdocument key with dot notation { 'x.y': v } => { 'x': { 'y' : v } }) const splittedKey = key.split('.'); @@ -1612,7 +1623,11 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) { return data; }, deepcopy(this.data)); - updatedObject.set(this.sanitizedData()); + const sanitized = this.sanitizedData(); + for (const attribute of readOnlyAttributes) { + delete sanitized[attribute]; + } + updatedObject.set(sanitized); return updatedObject; }; diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index d16fe28ad4..5329a3eda2 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -6,13 +6,6 @@ function isParseObjectConstructor(object) { return typeof object === 'function' && Object.prototype.hasOwnProperty.call(object, 'className'); } -function getClassName(parseClass) { - if (parseClass && parseClass.className) { - return parseClass.className; - } - return parseClass; -} - function validateValidator(validator) { if (!validator || typeof validator === 'function') { return; @@ -161,7 +154,7 @@ ParseCloud.job = function (functionName, handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeSave, @@ -197,7 +190,7 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeDelete, @@ -236,7 +229,7 @@ ParseCloud.beforeLogin = function (handler) { if (typeof handler === 'string' || isParseObjectConstructor(handler)) { // validation will occur downstream, this is to maintain internal // code consistency with the other hook types. - className = getClassName(handler); + className = triggers.getClassName(handler); handler = arguments[1]; } triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId); @@ -266,7 +259,7 @@ ParseCloud.afterLogin = function (handler) { if (typeof handler === 'string' || isParseObjectConstructor(handler)) { // validation will occur downstream, this is to maintain internal // code consistency with the other hook types. - className = getClassName(handler); + className = triggers.getClassName(handler); handler = arguments[1]; } triggers.addTrigger(triggers.Types.afterLogin, className, handler, Parse.applicationId); @@ -295,7 +288,7 @@ ParseCloud.afterLogout = function (handler) { if (typeof handler === 'string' || isParseObjectConstructor(handler)) { // validation will occur downstream, this is to maintain internal // code consistency with the other hook types. - className = getClassName(handler); + className = triggers.getClassName(handler); handler = arguments[1]; } triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId); @@ -327,7 +320,7 @@ ParseCloud.afterLogout = function (handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterSave, @@ -363,7 +356,7 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterDelete, @@ -399,7 +392,7 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.BeforeFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeFind, @@ -435,7 +428,7 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.AfterFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { - const className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterFind, @@ -663,7 +656,7 @@ ParseCloud.sendEmail = function (data) { */ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { validateValidator(validationHandler); - var className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeSubscribe, className, @@ -701,7 +694,7 @@ ParseCloud.onLiveQueryEvent = function (handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.LiveQueryEventTrigger}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterLiveQueryEvent = function (parseClass, handler, validationHandler) { - const className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterEvent, diff --git a/src/triggers.js b/src/triggers.js index cdd4353128..4d1cb5fba9 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -46,6 +46,13 @@ const baseStore = function () { }); }; +export function getClassName(parseClass) { + if (parseClass && parseClass.className) { + return parseClass.className; + } + return parseClass; +} + function validateClassNameForTriggers(className, type) { if (type == Types.beforeSave && className === '_PushStatus') { // _PushStatus uses undocumented nested key increment ops