From 6a04ef3456b8ea4c9e8ddc2e59ac8ca7e4a75d58 Mon Sep 17 00:00:00 2001 From: Drew Date: Sun, 22 May 2016 09:59:36 -0700 Subject: [PATCH] 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 --- spec/CloudCode.spec.js | 90 +++++++++++++++++++++++++++++ src/Adapters/Cache/InMemoryCache.js | 1 - src/Auth.js | 1 - src/RestWrite.js | 22 ++++--- src/Routers/UsersRouter.js | 1 + src/middlewares.js | 20 +++---- 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 2218a257061..8c2802d3e51 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -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 => { @@ -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(); + }) + }); + }); + }); + }); + }); + }); + }); }); diff --git a/src/Adapters/Cache/InMemoryCache.js b/src/Adapters/Cache/InMemoryCache.js index 37eeb43b02a..2d44292a0a0 100644 --- a/src/Adapters/Cache/InMemoryCache.js +++ b/src/Adapters/Cache/InMemoryCache.js @@ -53,7 +53,6 @@ export class InMemoryCache { if (record.timeout) { clearTimeout(record.timeout); } - delete this.cache[key]; } diff --git a/src/Auth.js b/src/Auth.js index 8f21567903c..634a839b9ad 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -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}); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index f6e758f6bf4..be460d4c487 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -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. @@ -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(() => { @@ -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(); } @@ -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, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index adba752f832..a5e6299c583 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -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.'); } diff --git a/src/middlewares.js b/src/middlewares.js index d534215453f..e46eb625574 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -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; @@ -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); @@ -183,7 +183,7 @@ function httpAuth(req) { } } } - + return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey}; }