diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e754f07cf..fc2dc9ef11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Expire password reset tokens on email change. See #5104 #### Bug fixes: * Fixes issue with vkontatke authentication +* Improves performance for roles and ACL's in live query server ### 3.0.0 diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index bb839d6d6d..6def159240 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -98,6 +98,14 @@ describe('ParseLiveQueryServer', function() { if (sessionToken === 'pleaseThrow') { return Promise.reject(); } + if (sessionToken === 'invalid') { + return Promise.reject( + new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'invalid session token' + ) + ); + } return Promise.resolve( new auth.Auth({ cacheController, user: { id: testUserId } }) ); @@ -1629,6 +1637,17 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.authCache.get('pleaseThrow')).toBe(undefined); }); + it('should keep a cache of invalid sessions', async () => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const promise = parseLiveQueryServer.getAuthForSessionToken('invalid'); + expect(parseLiveQueryServer.authCache.get('invalid')).toBe(promise); + // after the promise finishes, it should have removed it from the cache + await promise; + const finalResult = await parseLiveQueryServer.authCache.get('invalid'); + expect(finalResult.error).not.toBeUndefined(); + expect(parseLiveQueryServer.authCache.get('invalid')).not.toBe(undefined); + }); + afterEach(function() { jasmine.restoreLibrary( '../lib/LiveQuery/ParseWebSocketServer', diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 27157de5be..b175ebe36a 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -420,11 +420,21 @@ class ParseLiveQueryServer { .then(auth => { return { auth, userId: auth && auth.user && auth.user.id }; }) - .catch(() => { - // If you can't continue, let's just wrap it up and delete it. - // Next time, one will try again - this.authCache.del(sessionToken); - return {}; + .catch(error => { + // There was an error with the session token + const result = {}; + if (error && error.code === Parse.Error.INVALID_SESSION_TOKEN) { + // Store a resolved promise with the error for 10 minutes + result.error = error; + this.authCache.set( + sessionToken, + Promise.resolve(result), + 60 * 10 * 1000 + ); + } else { + this.authCache.del(sessionToken); + } + return result; }); this.authCache.set(sessionToken, authPromise); return authPromise; @@ -482,25 +492,19 @@ class ParseLiveQueryServer { : 'find'; } - async _matchesACL( - acl: any, - client: any, - requestId: number - ): Promise { - // Return true directly if ACL isn't present, ACL is public read, or client has master key - if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { - return true; - } - // Check subscription sessionToken matches ACL first - const subscriptionInfo = client.getSubscriptionInfo(requestId); - if (typeof subscriptionInfo === 'undefined') { + async _verifyACL(acl: any, token: string) { + if (!token) { return false; } - // TODO: get auth there and de-duplicate code below to work with the same Auth obj. - const { auth, userId } = await this.getAuthForSessionToken( - subscriptionInfo.sessionToken - ); + const { auth, userId } = await this.getAuthForSessionToken(token); + + // Getting the session token failed + // This means that no additional auth is available + // At this point, just bail out as no additional visibility can be inferred. + if (!auth || !userId) { + return false; + } const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId); if (isSubscriptionSessionTokenMatched) { return true; @@ -527,27 +531,40 @@ class ParseLiveQueryServer { } return false; }) - .then(async isRoleMatched => { - if (isRoleMatched) { - return Promise.resolve(true); - } - - // Check client sessionToken matches ACL - const clientSessionToken = client.sessionToken; - if (clientSessionToken) { - const { userId } = await this.getAuthForSessionToken( - clientSessionToken - ); - return acl.getReadAccess(userId); - } else { - return isRoleMatched; - } - }) .catch(() => { return false; }); } + async _matchesACL( + acl: any, + client: any, + requestId: number + ): Promise { + // Return true directly if ACL isn't present, ACL is public read, or client has master key + if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { + return true; + } + // Check subscription sessionToken matches ACL first + const subscriptionInfo = client.getSubscriptionInfo(requestId); + if (typeof subscriptionInfo === 'undefined') { + return false; + } + + const subscriptionToken = subscriptionInfo.sessionToken; + const clientSessionToken = client.sessionToken; + + if (await this._verifyACL(acl, subscriptionToken)) { + return true; + } + + if (await this._verifyACL(acl, clientSessionToken)) { + return true; + } + + return false; + } + _handleConnect(parseWebsocket: any, request: any): any { if (!this._validateKeys(request, this.keyPairs)) { Client.pushError(parseWebsocket, 4, 'Key in request is not valid');