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:
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user