Fix intense CPU usage when sessionToken is invalid in liveQuery (#5126)

* Ensure we bail out early when auth or userId are not provided (sessionToken fetch is invalid)

* Adds changelog

* better handling of session token errors and client tokens
This commit is contained in:
Florent Vilmart
2018-10-18 07:21:31 -04:00
committed by GitHub
parent 318a784e20
commit 4b7037ac9a
3 changed files with 74 additions and 37 deletions

View File

@@ -9,6 +9,7 @@
* Expire password reset tokens on email change. See #5104 * Expire password reset tokens on email change. See #5104
#### Bug fixes: #### Bug fixes:
* Fixes issue with vkontatke authentication * Fixes issue with vkontatke authentication
* Improves performance for roles and ACL's in live query server
### 3.0.0 ### 3.0.0

View File

@@ -98,6 +98,14 @@ describe('ParseLiveQueryServer', function() {
if (sessionToken === 'pleaseThrow') { if (sessionToken === 'pleaseThrow') {
return Promise.reject(); return Promise.reject();
} }
if (sessionToken === 'invalid') {
return Promise.reject(
new Parse.Error(
Parse.Error.INVALID_SESSION_TOKEN,
'invalid session token'
)
);
}
return Promise.resolve( return Promise.resolve(
new auth.Auth({ cacheController, user: { id: testUserId } }) new auth.Auth({ cacheController, user: { id: testUserId } })
); );
@@ -1629,6 +1637,17 @@ describe('ParseLiveQueryServer', function() {
expect(parseLiveQueryServer.authCache.get('pleaseThrow')).toBe(undefined); 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() { afterEach(function() {
jasmine.restoreLibrary( jasmine.restoreLibrary(
'../lib/LiveQuery/ParseWebSocketServer', '../lib/LiveQuery/ParseWebSocketServer',

View File

@@ -420,11 +420,21 @@ class ParseLiveQueryServer {
.then(auth => { .then(auth => {
return { auth, userId: auth && auth.user && auth.user.id }; return { auth, userId: auth && auth.user && auth.user.id };
}) })
.catch(() => { .catch(error => {
// If you can't continue, let's just wrap it up and delete it. // There was an error with the session token
// Next time, one will try again const result = {};
this.authCache.del(sessionToken); if (error && error.code === Parse.Error.INVALID_SESSION_TOKEN) {
return {}; // 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); this.authCache.set(sessionToken, authPromise);
return authPromise; return authPromise;
@@ -482,25 +492,19 @@ class ParseLiveQueryServer {
: 'find'; : 'find';
} }
async _matchesACL( async _verifyACL(acl: any, token: string) {
acl: any, if (!token) {
client: any,
requestId: number
): Promise<boolean> {
// 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; return false;
} }
// TODO: get auth there and de-duplicate code below to work with the same Auth obj. const { auth, userId } = await this.getAuthForSessionToken(token);
const { auth, userId } = await this.getAuthForSessionToken(
subscriptionInfo.sessionToken // 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); const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId);
if (isSubscriptionSessionTokenMatched) { if (isSubscriptionSessionTokenMatched) {
return true; return true;
@@ -527,27 +531,40 @@ class ParseLiveQueryServer {
} }
return false; 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(() => { .catch(() => {
return false; return false;
}); });
} }
async _matchesACL(
acl: any,
client: any,
requestId: number
): Promise<boolean> {
// 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 { _handleConnect(parseWebsocket: any, request: any): any {
if (!this._validateKeys(request, this.keyPairs)) { if (!this._validateKeys(request, this.keyPairs)) {
Client.pushError(parseWebsocket, 4, 'Key in request is not valid'); Client.pushError(parseWebsocket, 4, 'Key in request is not valid');