Skip to content

Cache users by objectID, and clear cache when updated via master key (fixes #1836) #1844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
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