Skip to content

Commit 392102e

Browse files
committed
Cache users by objectID, and clear cache when updated via master key (fixes #1836) (#1844)
* Cache users by objectID, and clear cache when updated via master key * Go back to caching by session token. Clear out cache by querying _Session when user is modified with Master Key (ew, hopefully that can be improved later) * Fix issue with user updates from different sessions causing stale reads * Tests aren't transpiled... * Still not transpiled
1 parent eefa2cc commit 392102e

File tree

6 files changed

+115
-20
lines changed

6 files changed

+115
-20
lines changed

spec/CloudCode.spec.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use strict"
22
const Parse = require("parse/node");
3+
const request = require('request');
4+
const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter;
35

46
describe('Cloud Code', () => {
57
it('can load absolute cloud code file', done => {
@@ -467,4 +469,92 @@ describe('Cloud Code', () => {
467469
done();
468470
});
469471
});
472+
473+
it('doesnt receive stale user in cloud code functions after user has been updated with master key (regression test for #1836)', done => {
474+
Parse.Cloud.define('testQuery', function(request, response) {
475+
response.success(request.user.get('data'));
476+
});
477+
478+
Parse.User.signUp('user', 'pass')
479+
.then(user => {
480+
user.set('data', 'AAA');
481+
return user.save();
482+
})
483+
.then(() => Parse.Cloud.run('testQuery'))
484+
.then(result => {
485+
expect(result).toEqual('AAA');
486+
Parse.User.current().set('data', 'BBB');
487+
return Parse.User.current().save(null, {useMasterKey: true});
488+
})
489+
.then(() => Parse.Cloud.run('testQuery'))
490+
.then(result => {
491+
expect(result).toEqual('BBB');
492+
done();
493+
});
494+
});
495+
496+
it('clears out the user cache for all sessions when the user is changed', done => {
497+
const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 });
498+
setServerConfiguration(Object.assign({}, defaultConfiguration, { cacheAdapter: cacheAdapter }));
499+
Parse.Cloud.define('checkStaleUser', (request, response) => {
500+
response.success(request.user.get('data'));
501+
});
502+
503+
let user = new Parse.User();
504+
user.set('username', 'test');
505+
user.set('password', 'moon-y');
506+
user.set('data', 'first data');
507+
user.signUp()
508+
.then(user => {
509+
let session1 = user.getSessionToken();
510+
request.get({
511+
url: 'http://localhost:8378/1/login?username=test&password=moon-y',
512+
json: true,
513+
headers: {
514+
'X-Parse-Application-Id': 'test',
515+
'X-Parse-REST-API-Key': 'rest',
516+
},
517+
}, (error, response, body) => {
518+
let session2 = body.sessionToken;
519+
520+
//Ensure both session tokens are in the cache
521+
Parse.Cloud.run('checkStaleUser')
522+
.then(() => {
523+
request.post({
524+
url: 'http://localhost:8378/1/functions/checkStaleUser',
525+
json: true,
526+
headers: {
527+
'X-Parse-Application-Id': 'test',
528+
'X-Parse-REST-API-Key': 'rest',
529+
'X-Parse-Session-Token': session2,
530+
}
531+
}, (error, response, body) => {
532+
Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)])
533+
.then(cachedVals => {
534+
expect(cachedVals[0].objectId).toEqual(user.id);
535+
expect(cachedVals[1].objectId).toEqual(user.id);
536+
537+
//Change with session 1 and then read with session 2.
538+
user.set('data', 'second data');
539+
user.save()
540+
.then(() => {
541+
request.post({
542+
url: 'http://localhost:8378/1/functions/checkStaleUser',
543+
json: true,
544+
headers: {
545+
'X-Parse-Application-Id': 'test',
546+
'X-Parse-REST-API-Key': 'rest',
547+
'X-Parse-Session-Token': session2,
548+
}
549+
}, (error, response, body) => {
550+
expect(body.result).toEqual('second data');
551+
done();
552+
})
553+
});
554+
});
555+
});
556+
});
557+
});
558+
});
559+
});
470560
});

src/Adapters/Cache/InMemoryCache.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export class InMemoryCache {
5353
if (record.timeout) {
5454
clearTimeout(record.timeout);
5555
}
56-
5756
delete this.cache[key];
5857
}
5958

src/Auth.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
7272
obj['className'] = '_User';
7373
obj['sessionToken'] = sessionToken;
7474
config.cacheController.user.put(sessionToken, obj);
75-
7675
let userObject = Parse.Object.fromJSON(obj);
7776
return new Auth({config, isMaster: false, installationId, user: userObject});
7877
});

src/RestWrite.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var cryptoUtils = require('./cryptoUtils');
1111
var passwordCrypto = require('./password');
1212
var Parse = require('parse/node');
1313
var triggers = require('./triggers');
14+
import RestQuery from './RestQuery';
1415

1516
// query and data are both provided in REST API format. So data
1617
// types are encoded by plain old objects.
@@ -318,10 +319,17 @@ RestWrite.prototype.transformUser = function() {
318319

319320
var promise = Promise.resolve();
320321

321-
// If we're updating a _User object, clear the user cache for the session
322-
if (this.query && this.auth.user && this.auth.user.getSessionToken()) {
323-
let cacheAdapter = this.config.cacheController;
324-
cacheAdapter.user.del(this.auth.user.getSessionToken());
322+
if (this.query) {
323+
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
324+
// session tokens, and remove them from the cache.
325+
promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: {
326+
__type: "Pointer",
327+
className: "_User",
328+
objectId: this.objectId(),
329+
}}).execute()
330+
.then(results => {
331+
results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken));
332+
});
325333
}
326334

327335
return promise.then(() => {
@@ -414,8 +422,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() {
414422
if (this.response && this.response.response) {
415423
this.response.response.sessionToken = token;
416424
}
417-
var create = new RestWrite(this.config, Auth.master(this.config),
418-
'_Session', null, sessionData);
425+
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
419426
return create.execute();
420427
}
421428

@@ -482,8 +489,7 @@ RestWrite.prototype.handleSession = function() {
482489
}
483490
sessionData[key] = this.data[key];
484491
}
485-
var create = new RestWrite(this.config, Auth.master(this.config),
486-
'_Session', null, sessionData);
492+
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
487493
return create.execute().then((results) => {
488494
if (!results.response) {
489495
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,

src/Routers/UsersRouter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export class UsersRouter extends ClassesRouter {
8585
user = results[0];
8686
return passwordCrypto.compare(req.body.password, user.password);
8787
}).then((correct) => {
88+
8889
if (!correct) {
8990
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
9091
}

src/middlewares.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ function handleParseHeaders(req, res, next) {
2727
dotNetKey: req.get('X-Parse-Windows-Key'),
2828
restAPIKey: req.get('X-Parse-REST-API-Key')
2929
};
30-
30+
3131
var basicAuth = httpAuth(req);
32-
32+
3333
if (basicAuth) {
3434
info.appId = basicAuth.appId
3535
info.masterKey = basicAuth.masterKey || info.masterKey;
@@ -156,24 +156,24 @@ function httpAuth(req) {
156156
if (!(req.req || req).headers.authorization)
157157
return ;
158158

159-
var header = (req.req || req).headers.authorization;
160-
var appId, masterKey, javascriptKey;
159+
var header = (req.req || req).headers.authorization;
160+
var appId, masterKey, javascriptKey;
161161

162162
// parse header
163163
var authPrefix = 'basic ';
164-
164+
165165
var match = header.toLowerCase().indexOf(authPrefix);
166-
166+
167167
if (match == 0) {
168168
var encodedAuth = header.substring(authPrefix.length, header.length);
169169
var credentials = decodeBase64(encodedAuth).split(':');
170-
170+
171171
if (credentials.length == 2) {
172172
appId = credentials[0];
173173
var key = credentials[1];
174-
174+
175175
var jsKeyPrefix = 'javascript-key=';
176-
176+
177177
var matchKey = key.indexOf(jsKeyPrefix)
178178
if (matchKey == 0) {
179179
javascriptKey = key.substring(jsKeyPrefix.length, key.length);
@@ -183,7 +183,7 @@ function httpAuth(req) {
183183
}
184184
}
185185
}
186-
186+
187187
return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey};
188188
}
189189

0 commit comments

Comments
 (0)