Skip to content

Commit

Permalink
Cache users by objectID, and clear cache when updated via master key (f…
Browse files Browse the repository at this point in the history
…ixes parse-community#1836) (parse-community#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
  • Loading branch information
drew-gross authored and JeremyPlease committed May 24, 2016
1 parent ca1c9cc commit 6a04ef3
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 20 deletions.
90 changes: 90 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use strict"
const Parse = require("parse/node");
const request = require('request');
const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter;

describe('Cloud Code', () => {
it('can load absolute cloud code file', done => {
Expand Down Expand Up @@ -467,4 +469,92 @@ describe('Cloud Code', () => {
done();
});
});

it('doesnt receive stale user in cloud code functions after user has been updated with master key (regression test for #1836)', done => {
Parse.Cloud.define('testQuery', function(request, response) {
response.success(request.user.get('data'));
});

Parse.User.signUp('user', 'pass')
.then(user => {
user.set('data', 'AAA');
return user.save();
})
.then(() => Parse.Cloud.run('testQuery'))
.then(result => {
expect(result).toEqual('AAA');
Parse.User.current().set('data', 'BBB');
return Parse.User.current().save(null, {useMasterKey: true});
})
.then(() => Parse.Cloud.run('testQuery'))
.then(result => {
expect(result).toEqual('BBB');
done();
});
});

it('clears out the user cache for all sessions when the user is changed', done => {
const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 });
setServerConfiguration(Object.assign({}, defaultConfiguration, { cacheAdapter: cacheAdapter }));
Parse.Cloud.define('checkStaleUser', (request, response) => {
response.success(request.user.get('data'));
});

let user = new Parse.User();
user.set('username', 'test');
user.set('password', 'moon-y');
user.set('data', 'first data');
user.signUp()
.then(user => {
let session1 = user.getSessionToken();
request.get({
url: 'http://localhost:8378/1/login?username=test&password=moon-y',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
}, (error, response, body) => {
let session2 = body.sessionToken;

//Ensure both session tokens are in the cache
Parse.Cloud.run('checkStaleUser')
.then(() => {
request.post({
url: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}, (error, response, body) => {
Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)])
.then(cachedVals => {
expect(cachedVals[0].objectId).toEqual(user.id);
expect(cachedVals[1].objectId).toEqual(user.id);

//Change with session 1 and then read with session 2.
user.set('data', 'second data');
user.save()
.then(() => {
request.post({
url: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}, (error, response, body) => {
expect(body.result).toEqual('second data');
done();
})
});
});
});
});
});
});
});
});
1 change: 0 additions & 1 deletion src/Adapters/Cache/InMemoryCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ export class InMemoryCache {
if (record.timeout) {
clearTimeout(record.timeout);
}

delete this.cache[key];
}

Expand Down
1 change: 0 additions & 1 deletion src/Auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
obj['className'] = '_User';
obj['sessionToken'] = sessionToken;
config.cacheController.user.put(sessionToken, obj);

let userObject = Parse.Object.fromJSON(obj);
return new Auth({config, isMaster: false, installationId, user: userObject});
});
Expand Down
22 changes: 14 additions & 8 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password');
var Parse = require('parse/node');
var triggers = require('./triggers');
import RestQuery from './RestQuery';

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

var promise = Promise.resolve();

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

return promise.then(() => {
Expand Down Expand Up @@ -414,8 +422,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() {
if (this.response && this.response.response) {
this.response.response.sessionToken = token;
}
var create = new RestWrite(this.config, Auth.master(this.config),
'_Session', null, sessionData);
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
return create.execute();
}

Expand Down Expand Up @@ -482,8 +489,7 @@ RestWrite.prototype.handleSession = function() {
}
sessionData[key] = this.data[key];
}
var create = new RestWrite(this.config, Auth.master(this.config),
'_Session', null, sessionData);
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
return create.execute().then((results) => {
if (!results.response) {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
Expand Down
1 change: 1 addition & 0 deletions src/Routers/UsersRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export class UsersRouter extends ClassesRouter {
user = results[0];
return passwordCrypto.compare(req.body.password, user.password);
}).then((correct) => {

if (!correct) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}
Expand Down
20 changes: 10 additions & 10 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ function handleParseHeaders(req, res, next) {
dotNetKey: req.get('X-Parse-Windows-Key'),
restAPIKey: req.get('X-Parse-REST-API-Key')
};

var basicAuth = httpAuth(req);

if (basicAuth) {
info.appId = basicAuth.appId
info.masterKey = basicAuth.masterKey || info.masterKey;
Expand Down Expand Up @@ -156,24 +156,24 @@ function httpAuth(req) {
if (!(req.req || req).headers.authorization)
return ;

var header = (req.req || req).headers.authorization;
var appId, masterKey, javascriptKey;
var header = (req.req || req).headers.authorization;
var appId, masterKey, javascriptKey;

// parse header
var authPrefix = 'basic ';

var match = header.toLowerCase().indexOf(authPrefix);

if (match == 0) {
var encodedAuth = header.substring(authPrefix.length, header.length);
var credentials = decodeBase64(encodedAuth).split(':');

if (credentials.length == 2) {
appId = credentials[0];
var key = credentials[1];

var jsKeyPrefix = 'javascript-key=';

var matchKey = key.indexOf(jsKeyPrefix)
if (matchKey == 0) {
javascriptKey = key.substring(jsKeyPrefix.length, key.length);
Expand All @@ -183,7 +183,7 @@ function httpAuth(req) {
}
}
}

return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey};
}

Expand Down

0 comments on commit 6a04ef3

Please sign in to comment.