* 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
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
"use strict"
|
"use strict"
|
||||||
const Parse = require("parse/node");
|
const Parse = require("parse/node");
|
||||||
|
const request = require('request');
|
||||||
|
const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter;
|
||||||
|
|
||||||
describe('Cloud Code', () => {
|
describe('Cloud Code', () => {
|
||||||
it('can load absolute cloud code file', done => {
|
it('can load absolute cloud code file', done => {
|
||||||
@@ -467,4 +469,92 @@ describe('Cloud Code', () => {
|
|||||||
done();
|
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();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export class InMemoryCache {
|
|||||||
if (record.timeout) {
|
if (record.timeout) {
|
||||||
clearTimeout(record.timeout);
|
clearTimeout(record.timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete this.cache[key];
|
delete this.cache[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } =
|
|||||||
obj['className'] = '_User';
|
obj['className'] = '_User';
|
||||||
obj['sessionToken'] = sessionToken;
|
obj['sessionToken'] = sessionToken;
|
||||||
config.cacheController.user.put(sessionToken, obj);
|
config.cacheController.user.put(sessionToken, obj);
|
||||||
|
|
||||||
let userObject = Parse.Object.fromJSON(obj);
|
let userObject = Parse.Object.fromJSON(obj);
|
||||||
return new Auth({config, isMaster: false, installationId, user: userObject});
|
return new Auth({config, isMaster: false, installationId, user: userObject});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ var cryptoUtils = require('./cryptoUtils');
|
|||||||
var passwordCrypto = require('./password');
|
var passwordCrypto = require('./password');
|
||||||
var Parse = require('parse/node');
|
var Parse = require('parse/node');
|
||||||
var triggers = require('./triggers');
|
var triggers = require('./triggers');
|
||||||
|
import RestQuery from './RestQuery';
|
||||||
|
|
||||||
// query and data are both provided in REST API format. So data
|
// query and data are both provided in REST API format. So data
|
||||||
// types are encoded by plain old objects.
|
// types are encoded by plain old objects.
|
||||||
@@ -318,10 +319,17 @@ RestWrite.prototype.transformUser = function() {
|
|||||||
|
|
||||||
var promise = Promise.resolve();
|
var promise = Promise.resolve();
|
||||||
|
|
||||||
// If we're updating a _User object, clear the user cache for the session
|
if (this.query) {
|
||||||
if (this.query && this.auth.user && this.auth.user.getSessionToken()) {
|
// If we're updating a _User object, we need to clear out the cache for that user. Find all their
|
||||||
let cacheAdapter = this.config.cacheController;
|
// session tokens, and remove them from the cache.
|
||||||
cacheAdapter.user.del(this.auth.user.getSessionToken());
|
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(() => {
|
return promise.then(() => {
|
||||||
@@ -414,8 +422,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() {
|
|||||||
if (this.response && this.response.response) {
|
if (this.response && this.response.response) {
|
||||||
this.response.response.sessionToken = token;
|
this.response.response.sessionToken = token;
|
||||||
}
|
}
|
||||||
var create = new RestWrite(this.config, Auth.master(this.config),
|
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
|
||||||
'_Session', null, sessionData);
|
|
||||||
return create.execute();
|
return create.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,8 +489,7 @@ RestWrite.prototype.handleSession = function() {
|
|||||||
}
|
}
|
||||||
sessionData[key] = this.data[key];
|
sessionData[key] = this.data[key];
|
||||||
}
|
}
|
||||||
var create = new RestWrite(this.config, Auth.master(this.config),
|
var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData);
|
||||||
'_Session', null, sessionData);
|
|
||||||
return create.execute().then((results) => {
|
return create.execute().then((results) => {
|
||||||
if (!results.response) {
|
if (!results.response) {
|
||||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
user = results[0];
|
user = results[0];
|
||||||
return passwordCrypto.compare(req.body.password, user.password);
|
return passwordCrypto.compare(req.body.password, user.password);
|
||||||
}).then((correct) => {
|
}).then((correct) => {
|
||||||
|
|
||||||
if (!correct) {
|
if (!correct) {
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ function handleParseHeaders(req, res, next) {
|
|||||||
dotNetKey: req.get('X-Parse-Windows-Key'),
|
dotNetKey: req.get('X-Parse-Windows-Key'),
|
||||||
restAPIKey: req.get('X-Parse-REST-API-Key')
|
restAPIKey: req.get('X-Parse-REST-API-Key')
|
||||||
};
|
};
|
||||||
|
|
||||||
var basicAuth = httpAuth(req);
|
var basicAuth = httpAuth(req);
|
||||||
|
|
||||||
if (basicAuth) {
|
if (basicAuth) {
|
||||||
info.appId = basicAuth.appId
|
info.appId = basicAuth.appId
|
||||||
info.masterKey = basicAuth.masterKey || info.masterKey;
|
info.masterKey = basicAuth.masterKey || info.masterKey;
|
||||||
@@ -156,24 +156,24 @@ function httpAuth(req) {
|
|||||||
if (!(req.req || req).headers.authorization)
|
if (!(req.req || req).headers.authorization)
|
||||||
return ;
|
return ;
|
||||||
|
|
||||||
var header = (req.req || req).headers.authorization;
|
var header = (req.req || req).headers.authorization;
|
||||||
var appId, masterKey, javascriptKey;
|
var appId, masterKey, javascriptKey;
|
||||||
|
|
||||||
// parse header
|
// parse header
|
||||||
var authPrefix = 'basic ';
|
var authPrefix = 'basic ';
|
||||||
|
|
||||||
var match = header.toLowerCase().indexOf(authPrefix);
|
var match = header.toLowerCase().indexOf(authPrefix);
|
||||||
|
|
||||||
if (match == 0) {
|
if (match == 0) {
|
||||||
var encodedAuth = header.substring(authPrefix.length, header.length);
|
var encodedAuth = header.substring(authPrefix.length, header.length);
|
||||||
var credentials = decodeBase64(encodedAuth).split(':');
|
var credentials = decodeBase64(encodedAuth).split(':');
|
||||||
|
|
||||||
if (credentials.length == 2) {
|
if (credentials.length == 2) {
|
||||||
appId = credentials[0];
|
appId = credentials[0];
|
||||||
var key = credentials[1];
|
var key = credentials[1];
|
||||||
|
|
||||||
var jsKeyPrefix = 'javascript-key=';
|
var jsKeyPrefix = 'javascript-key=';
|
||||||
|
|
||||||
var matchKey = key.indexOf(jsKeyPrefix)
|
var matchKey = key.indexOf(jsKeyPrefix)
|
||||||
if (matchKey == 0) {
|
if (matchKey == 0) {
|
||||||
javascriptKey = key.substring(jsKeyPrefix.length, key.length);
|
javascriptKey = key.substring(jsKeyPrefix.length, key.length);
|
||||||
@@ -183,7 +183,7 @@ function httpAuth(req) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey};
|
return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user