Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions spec/ParseLiveQueryServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } })
);
Expand Down Expand Up @@ -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',
Expand Down
91 changes: 54 additions & 37 deletions src/LiveQuery/ParseLiveQueryServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -482,25 +492,19 @@ class ParseLiveQueryServer {
: 'find';
}

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') {
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail fast

// 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;
Expand All @@ -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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was initally overlooked, the new implementation takes properly care of both tokens

clientSessionToken
);
return acl.getReadAccess(userId);
} else {
return isRoleMatched;
}
})
.catch(() => {
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 {
if (!this._validateKeys(request, this.keyPairs)) {
Client.pushError(parseWebsocket, 4, 'Key in request is not valid');
Expand Down