diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index 5595f6baba..6dc34e36f1 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -120,4 +120,33 @@ describe('Auth', () => { expect(userAuth.user instanceof Parse.User).toBe(true); expect(userAuth.user.id).toBe(user.id); }); + + it('should load auth without a config', async () => { + const user = new Parse.User(); + await user.signUp({ + username: 'hello', + password: 'password', + }); + expect(user.getSessionToken()).not.toBeUndefined(); + const userAuth = await getAuthForSessionToken({ + sessionToken: user.getSessionToken(), + }); + expect(userAuth.user instanceof Parse.User).toBe(true); + expect(userAuth.user.id).toBe(user.id); + }); + + it('should load auth with a config', async () => { + const user = new Parse.User(); + await user.signUp({ + username: 'hello', + password: 'password', + }); + expect(user.getSessionToken()).not.toBeUndefined(); + const userAuth = await getAuthForSessionToken({ + sessionToken: user.getSessionToken(), + config: Config.get('test'), + }); + expect(userAuth.user instanceof Parse.User).toBe(true); + expect(userAuth.user.id).toBe(user.id); + }); }); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index d5e294c526..e02c4cc3ca 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -933,7 +933,7 @@ describe('Cloud Code', () => { expect(response.data.result).toEqual('second data'); done(); }) - .catch(e => done.fail(e)); + .catch(done.fail); }); it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => { diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 38cfd9028a..bb839d6d6d 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -2,6 +2,9 @@ const Parse = require('parse/node'); const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer') .ParseLiveQueryServer; const ParseServer = require('../lib/ParseServer').default; +const LiveQueryController = require('../lib/Controllers/LiveQueryController') + .LiveQueryController; +const auth = require('../lib/Auth'); // Global mock info const queryHashValue = 'hash'; @@ -84,29 +87,28 @@ describe('ParseLiveQueryServer', function() { 'ParsePubSub', mockParsePubSub ); - // Make mock SessionTokenCache - const mockSessionTokenCache = function() { - this.getUserId = function(sessionToken) { + spyOn(auth, 'getAuthForSessionToken').and.callFake( + ({ sessionToken, cacheController }) => { if (typeof sessionToken === 'undefined') { - return Promise.resolve(undefined); + return Promise.reject(); } if (sessionToken === null) { return Promise.reject(); } - return Promise.resolve(testUserId); - }; - }; - jasmine.mockLibrary( - '../lib/LiveQuery/SessionTokenCache', - 'SessionTokenCache', - mockSessionTokenCache + if (sessionToken === 'pleaseThrow') { + return Promise.reject(); + } + return Promise.resolve( + new auth.Auth({ cacheController, user: { id: testUserId } }) + ); + } ); done(); }); it('can be initialized', function() { const httpServer = {}; - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer); + const parseLiveQueryServer = new ParseLiveQueryServer(httpServer); expect(parseLiveQueryServer.clientId).toBeUndefined(); expect(parseLiveQueryServer.clients.size).toBe(0); @@ -177,8 +179,97 @@ describe('ParseLiveQueryServer', function() { }); }); + it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + function setPermissionsOnClass(className, permissions, doPut) { + const request = require('request'); + let op = request.post; + if (doPut) { + op = request.put; + } + return new Promise((resolve, reject) => { + op( + { + url: Parse.serverURL + '/schemas/' + className, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + json: true, + body: { + classLevelPermissions: permissions, + }, + }, + (error, response, body) => { + if (error) { + return reject(error); + } + if (body.error) { + return reject(body); + } + return resolve(body); + } + ); + }); + } + + let saveSpy; + let deleteSpy; + reconfigureServer({ + liveQuery: { + classNames: ['Yolo'], + }, + }) + .then(parseServer => { + saveSpy = spyOn(parseServer.config.liveQueryController, 'onAfterSave'); + deleteSpy = spyOn( + parseServer.config.liveQueryController, + 'onAfterDelete' + ); + return setPermissionsOnClass('Yolo', { + create: { '*': true }, + delete: { '*': true }, + }); + }) + .then(() => { + const obj = new Parse.Object('Yolo'); + return obj.save(); + }) + .then(obj => { + return obj.destroy(); + }) + .then(() => { + expect(saveSpy).toHaveBeenCalled(); + const saveArgs = saveSpy.calls.mostRecent().args; + expect(saveArgs.length).toBe(4); + expect(saveArgs[0]).toBe('Yolo'); + expect(saveArgs[3]).toEqual({ + get: {}, + addField: {}, + create: { '*': true }, + find: {}, + update: {}, + delete: { '*': true }, + }); + + expect(deleteSpy).toHaveBeenCalled(); + const deleteArgs = deleteSpy.calls.mostRecent().args; + expect(deleteArgs.length).toBe(4); + expect(deleteArgs[0]).toBe('Yolo'); + expect(deleteArgs[3]).toEqual({ + get: {}, + addField: {}, + create: { '*': true }, + find: {}, + update: {}, + delete: { '*': true }, + }); + done(); + }) + .catch(done.fail); + }); + it('can handle connect command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1, }; @@ -198,7 +289,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle subscribe command without clientId', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); @@ -207,7 +298,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle subscribe command with new query', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); @@ -254,7 +345,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle subscribe command with existing query', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add two mock clients const clientId = 1; addMockClient(parseLiveQueryServer, clientId); @@ -318,7 +409,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command without clientId', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {}); @@ -327,7 +418,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command without not existed client', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: 1, }; @@ -338,7 +429,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command without not existed query', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; addMockClient(parseLiveQueryServer, clientId); @@ -353,7 +444,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); @@ -393,7 +484,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set connect command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket @@ -415,7 +506,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set subscribe command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleSubscribe = jasmine.createSpy( '_handleSubscribe' @@ -441,7 +532,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set unsubscribe command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy( '_handleSubscribe' @@ -467,7 +558,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set update command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough(); spyOn(parseLiveQueryServer, '_handleUnsubscribe').and.callThrough(); @@ -502,7 +593,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set missing command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -518,7 +609,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set unknown command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -534,7 +625,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); parseWebSocket.clientId = 1; @@ -552,7 +643,7 @@ describe('ParseLiveQueryServer', function() { }; const spy = spyOn(cloudCodeHandler, 'handler').and.callThrough(); Parse.Cloud.onLiveQueryEvent(cloudCodeHandler.handler); - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); parseWebSocket.clientId = 1; @@ -570,7 +661,7 @@ describe('ParseLiveQueryServer', function() { // TODO: Test server can set disconnect command message handler for a parseWebSocket it('has no subscription and can handle object delete command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ @@ -586,7 +677,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object delete command which does not match any subscription', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ @@ -619,7 +710,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object delete command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ @@ -655,7 +746,7 @@ describe('ParseLiveQueryServer', function() { }); it('has no subscription and can handle object save command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Make sure we do not crash in this case @@ -663,7 +754,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object save command which does not match any subscription', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Add mock client @@ -694,7 +785,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object enter command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client @@ -731,7 +822,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object update command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client @@ -764,7 +855,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object leave command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client @@ -801,7 +892,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object create command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Add mock client @@ -834,7 +925,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match subscription for null or undefined parse object', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { match: jasmine.createSpy('match'), @@ -851,7 +942,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match subscription', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { query: {}, @@ -866,7 +957,7 @@ describe('ParseLiveQueryServer', function() { }); it('can inflate parse object', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request const objectJSON = { className: 'testClassName', @@ -908,7 +999,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match undefined ACL', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const client = {}; const requestId = 0; @@ -921,7 +1012,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with none exist requestId', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); const client = { getSubscriptionInfo: jasmine @@ -939,7 +1030,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with public read access', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); const client = { @@ -960,7 +1051,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with valid subscription sessionToken', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); const client = { @@ -981,7 +1072,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with valid client sessionToken', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1004,7 +1095,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with invalid subscription and client sessionToken', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1027,7 +1118,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with subscription sessionToken checking error', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return error when sessionToken is null, this is just @@ -1050,7 +1141,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with client sessionToken checking error', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return error when sessionToken is null @@ -1073,7 +1164,7 @@ describe('ParseLiveQueryServer', function() { }); it("won't match ACL that doesn't have public read or any roles", function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { @@ -1094,7 +1185,7 @@ describe('ParseLiveQueryServer', function() { }); it("won't match non-public ACL with role when there is no user", function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess('livequery', true); @@ -1110,14 +1201,15 @@ describe('ParseLiveQueryServer', function() { .then(function(isMatched) { expect(isMatched).toBe(false); done(); - }); + }) + .catch(done.fail); }); it("won't match ACL with role based read access set to false", function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); - acl.setRoleReadAccess('liveQueryRead', false); + acl.setRoleReadAccess('otherLiveQueryRead', true); const client = { getSubscriptionInfo: jasmine .createSpy('getSubscriptionInfo') @@ -1128,15 +1220,28 @@ describe('ParseLiveQueryServer', function() { const requestId = 0; spyOn(Parse, 'Query').and.callFake(function() { + let shouldReturn = false; return { equalTo() { + shouldReturn = true; // Nothing to do here + return this; + }, + containedIn() { + shouldReturn = false; + return this; }, find() { + if (!shouldReturn) { + return Promise.resolve([]); + } //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role(); - liveQueryRole.set('name', 'liveQueryRead'); - return [liveQueryRole]; + const liveQueryRole = new Parse.Role( + 'liveQueryRead', + new Parse.ACL() + ); + liveQueryRole.id = 'abcdef1234'; + return Promise.resolve([liveQueryRole]); }, }; }); @@ -1147,10 +1252,17 @@ describe('ParseLiveQueryServer', function() { expect(isMatched).toBe(false); done(); }); + + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); it('will match ACL with role based read access set to true', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess('liveQueryRead', true); @@ -1164,15 +1276,28 @@ describe('ParseLiveQueryServer', function() { const requestId = 0; spyOn(Parse, 'Query').and.callFake(function() { + let shouldReturn = false; return { equalTo() { + shouldReturn = true; // Nothing to do here + return this; + }, + containedIn() { + shouldReturn = false; + return this; }, find() { + if (!shouldReturn) { + return Promise.resolve([]); + } //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role(); - liveQueryRole.set('name', 'liveQueryRead'); - return [liveQueryRole]; + const liveQueryRole = new Parse.Role( + 'liveQueryRead', + new Parse.ACL() + ); + liveQueryRole.id = 'abcdef1234'; + return Promise.resolve([liveQueryRole]); }, }; }); @@ -1183,6 +1308,139 @@ describe('ParseLiveQueryServer', function() { expect(isMatched).toBe(true); done(); }); + + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); + }); + + describe('class level permissions', () => { + it('matches CLP when find is closed', done => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + const client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: undefined, + }), + }; + const requestId = 0; + + parseLiveQueryServer + ._matchesCLP( + { + find: {}, + }, + { className: 'Yolo' }, + client, + requestId, + 'find' + ) + .then(isMatched => { + expect(isMatched).toBe(false); + done(); + }); + }); + + it('matches CLP when find is open', done => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + const client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: undefined, + }), + }; + const requestId = 0; + + parseLiveQueryServer + ._matchesCLP( + { + find: { '*': true }, + }, + { className: 'Yolo' }, + client, + requestId, + 'find' + ) + .then(isMatched => { + expect(isMatched).toBe(true); + done(); + }); + }); + + it('matches CLP when find is restricted to userIds', done => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + const client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: 'userId', + }), + }; + const requestId = 0; + + parseLiveQueryServer + ._matchesCLP( + { + find: { userId: true }, + }, + { className: 'Yolo' }, + client, + requestId, + 'find' + ) + .then(isMatched => { + expect(isMatched).toBe(true); + done(); + }); + }); + + it('matches CLP when find is restricted to userIds', done => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + const client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: undefined, + }), + }; + const requestId = 0; + + parseLiveQueryServer + ._matchesCLP( + { + find: { userId: true }, + }, + { className: 'Yolo' }, + client, + requestId, + 'find' + ) + .then(isMatched => { + expect(isMatched).toBe(false); + done(); + }); + }); }); it('can validate key when valid key is provided', function() { @@ -1309,7 +1567,7 @@ describe('ParseLiveQueryServer', function() { }); it('will match non-public ACL when client has master key', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { @@ -1329,7 +1587,7 @@ describe('ParseLiveQueryServer', function() { }); it("won't match non-public ACL when client has no master key", function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { @@ -1348,6 +1606,29 @@ describe('ParseLiveQueryServer', function() { }); }); + it('should properly pull auth from cache', () => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const promise = parseLiveQueryServer.getAuthForSessionToken('sessionToken'); + const secondPromise = parseLiveQueryServer.getAuthForSessionToken( + 'sessionToken' + ); + // should be in the cache + expect(parseLiveQueryServer.authCache.get('sessionToken')).toBe(promise); + // should be the same promise returned + expect(promise).toBe(secondPromise); + // the auth should be called only once + expect(auth.getAuthForSessionToken.calls.count()).toBe(1); + }); + + it('should delete from cache throwing auth calls', async () => { + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const promise = parseLiveQueryServer.getAuthForSessionToken('pleaseThrow'); + expect(parseLiveQueryServer.authCache.get('pleaseThrow')).toBe(promise); + // after the promise finishes, it should have removed it from the cache + expect(await promise).toEqual({}); + expect(parseLiveQueryServer.authCache.get('pleaseThrow')).toBe(undefined); + }); + afterEach(function() { jasmine.restoreLibrary( '../lib/LiveQuery/ParseWebSocketServer', @@ -1358,10 +1639,6 @@ describe('ParseLiveQueryServer', function() { jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'queryHash'); jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery'); jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub'); - jasmine.restoreLibrary( - '../lib/LiveQuery/SessionTokenCache', - 'SessionTokenCache' - ); }); // Helper functions to add mock client and subscription to a liveQueryServer @@ -1443,3 +1720,139 @@ describe('ParseLiveQueryServer', function() { return message; } }); + +describe('LiveQueryController', () => { + it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + function setPermissionsOnClass(className, permissions, doPut) { + const request = require('request'); + let op = request.post; + if (doPut) { + op = request.put; + } + return new Promise((resolve, reject) => { + op( + { + url: Parse.serverURL + '/schemas/' + className, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + json: true, + body: { + classLevelPermissions: permissions, + }, + }, + (error, response, body) => { + if (error) { + return reject(error); + } + if (body.error) { + return reject(body); + } + return resolve(body); + } + ); + }); + } + + let saveSpy; + let deleteSpy; + reconfigureServer({ + liveQuery: { + classNames: ['Yolo'], + }, + }) + .then(parseServer => { + saveSpy = spyOn( + parseServer.config.liveQueryController, + 'onAfterSave' + ).and.callThrough(); + deleteSpy = spyOn( + parseServer.config.liveQueryController, + 'onAfterDelete' + ).and.callThrough(); + return setPermissionsOnClass('Yolo', { + create: { '*': true }, + delete: { '*': true }, + }); + }) + .then(() => { + const obj = new Parse.Object('Yolo'); + return obj.save(); + }) + .then(obj => { + return obj.destroy(); + }) + .then(() => { + expect(saveSpy).toHaveBeenCalled(); + const saveArgs = saveSpy.calls.mostRecent().args; + expect(saveArgs.length).toBe(4); + expect(saveArgs[0]).toBe('Yolo'); + expect(saveArgs[3]).toEqual({ + get: {}, + addField: {}, + create: { '*': true }, + find: {}, + update: {}, + delete: { '*': true }, + }); + + expect(deleteSpy).toHaveBeenCalled(); + const deleteArgs = deleteSpy.calls.mostRecent().args; + expect(deleteArgs.length).toBe(4); + expect(deleteArgs[0]).toBe('Yolo'); + expect(deleteArgs[3]).toEqual({ + get: {}, + addField: {}, + create: { '*': true }, + find: {}, + update: {}, + delete: { '*': true }, + }); + done(); + }) + .catch(done.fail); + }); + + it('should properly pack message request on afterSave', () => { + const controller = new LiveQueryController({ + classNames: ['Yolo'], + }); + const spy = spyOn(controller.liveQueryPublisher, 'onCloudCodeAfterSave'); + controller.onAfterSave('Yolo', { o: 1 }, { o: 2 }, { yolo: true }); + expect(spy).toHaveBeenCalled(); + const args = spy.calls.mostRecent().args; + expect(args.length).toBe(1); + expect(args[0]).toEqual({ + object: { o: 1 }, + original: { o: 2 }, + classLevelPermissions: { yolo: true }, + }); + }); + + it('should properly pack message request on afterDelete', () => { + const controller = new LiveQueryController({ + classNames: ['Yolo'], + }); + const spy = spyOn(controller.liveQueryPublisher, 'onCloudCodeAfterDelete'); + controller.onAfterDelete('Yolo', { o: 1 }, { o: 2 }, { yolo: true }); + expect(spy).toHaveBeenCalled(); + const args = spy.calls.mostRecent().args; + expect(args.length).toBe(1); + expect(args[0]).toEqual({ + object: { o: 1 }, + original: { o: 2 }, + classLevelPermissions: { yolo: true }, + }); + }); + + it('should properly pack message request', () => { + const controller = new LiveQueryController({ + classNames: ['Yolo'], + }); + expect(controller._makePublisherRequest({})).toEqual({ + object: {}, + original: undefined, + }); + }); +}); diff --git a/spec/helper.js b/spec/helper.js index be599e39b8..607f2fcf1d 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -144,19 +144,22 @@ const reconfigureServer = changedConfiguration => { }); } try { + let parseServer = undefined; const newConfiguration = Object.assign( {}, defaultConfiguration, changedConfiguration, { __indexBuildCompletionCallbackForTests: indexBuildPromise => - indexBuildPromise.then(resolve, reject), + indexBuildPromise.then(() => { + resolve(parseServer); + }, reject), mountPath: '/1', port, } ); cache.clear(); - const parseServer = ParseServer.start(newConfiguration); + parseServer = ParseServer.start(newConfiguration); parseServer.app.use(require('./testing-routes').router); parseServer.expressApp.use('/1', err => { console.error(err); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 3b28126133..adaf980532 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1335,7 +1335,7 @@ class DatabaseController { } addPointerPermissions( - schema: any, + schema: SchemaController.SchemaController, className: string, operation: string, query: any, @@ -1343,10 +1343,10 @@ class DatabaseController { ) { // Check if class has public permission for operation // If the BaseCLP pass, let go through - if (schema.testBaseCLP(className, aclGroup, operation)) { + if (schema.testPermissionsForClassName(className, aclGroup, operation)) { return query; } - const perms = schema.schemaData[className].classLevelPermissions; + const perms = schema.getClassLevelPermissions(className); const field = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 89834e2588..7ad6d977ec 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -16,19 +16,37 @@ export class LiveQueryController { this.liveQueryPublisher = new ParseCloudCodePublisher(config); } - onAfterSave(className: string, currentObject: any, originalObject: any) { + onAfterSave( + className: string, + currentObject: any, + originalObject: any, + classLevelPermissions: ?any + ) { if (!this.hasLiveQuery(className)) { return; } - const req = this._makePublisherRequest(currentObject, originalObject); + const req = this._makePublisherRequest( + currentObject, + originalObject, + classLevelPermissions + ); this.liveQueryPublisher.onCloudCodeAfterSave(req); } - onAfterDelete(className: string, currentObject: any, originalObject: any) { + onAfterDelete( + className: string, + currentObject: any, + originalObject: any, + classLevelPermissions: any + ) { if (!this.hasLiveQuery(className)) { return; } - const req = this._makePublisherRequest(currentObject, originalObject); + const req = this._makePublisherRequest( + currentObject, + originalObject, + classLevelPermissions + ); this.liveQueryPublisher.onCloudCodeAfterDelete(req); } @@ -36,13 +54,20 @@ export class LiveQueryController { return this.classNames.has(className); } - _makePublisherRequest(currentObject: any, originalObject: any): any { + _makePublisherRequest( + currentObject: any, + originalObject: any, + classLevelPermissions: ?any + ): any { const req = { object: currentObject, }; if (currentObject) { req.original = originalObject; } + if (classLevelPermissions) { + req.classLevelPermissions = classLevelPermissions; + } return req; } } diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b82295f4ab..40e240ce26 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -1122,18 +1122,28 @@ export default class SchemaController { return Promise.resolve(this); } - // Validates the base CLP for an operation - testBaseCLP(className: string, aclGroup: string[], operation: string) { - const classSchema = this.schemaData[className]; - if ( - !classSchema || - !classSchema.classLevelPermissions || - !classSchema.classLevelPermissions[operation] - ) { + testPermissionsForClassName( + className: string, + aclGroup: string[], + operation: string + ) { + return SchemaController.testPermissions( + this.getClassLevelPermissions(className), + aclGroup, + operation + ); + } + + // Tests that the class level permission let pass the operation for a given aclGroup + static testPermissions( + classPermissions: ?any, + aclGroup: string[], + operation: string + ): boolean { + if (!classPermissions || !classPermissions[operation]) { return true; } - const perms = classSchema.classLevelPermissions[operation]; - // Handle the public scenario quickly + const perms = classPermissions[operation]; if (perms['*']) { return true; } @@ -1149,21 +1159,22 @@ export default class SchemaController { } // Validates an operation passes class-level-permissions set in the schema - validatePermission(className: string, aclGroup: string[], operation: string) { - if (this.testBaseCLP(className, aclGroup, operation)) { - return Promise.resolve(); - } - const classSchema = this.schemaData[className]; + static validatePermission( + classPermissions: ?any, + className: string, + aclGroup: string[], + operation: string + ) { if ( - !classSchema || - !classSchema.classLevelPermissions || - !classSchema.classLevelPermissions[operation] + SchemaController.testPermissions(classPermissions, aclGroup, operation) ) { - return true; + return Promise.resolve(); } - const classPerms = classSchema.classLevelPermissions; - const perms = classSchema.classLevelPermissions[operation]; + if (!classPermissions || !classPermissions[operation]) { + return true; + } + const perms = classPermissions[operation]; // If only for authenticated users // make sure we have an aclGroup if (perms['requiresAuthentication']) { @@ -1201,8 +1212,8 @@ export default class SchemaController { // Process the readUserFields later if ( - Array.isArray(classPerms[permissionField]) && - classPerms[permissionField].length > 0 + Array.isArray(classPermissions[permissionField]) && + classPermissions[permissionField].length > 0 ) { return Promise.resolve(); } @@ -1212,6 +1223,23 @@ export default class SchemaController { ); } + // Validates an operation passes class-level-permissions set in the schema + validatePermission(className: string, aclGroup: string[], operation: string) { + return SchemaController.validatePermission( + this.getClassLevelPermissions(className), + className, + aclGroup, + operation + ); + } + + getClassLevelPermissions(className: string): any { + return ( + this.schemaData[className] && + this.schemaData[className].classLevelPermissions + ); + } + // Returns the expected type for a className+key combination // or undefined if the schema is not set getExpectedType( diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index ea8d709e01..27157de5be 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -7,10 +7,13 @@ import logger from '../logger'; import RequestSchema from './RequestSchema'; import { matchesQuery, queryHash } from './QueryTools'; import { ParsePubSub } from './ParsePubSub'; -import { SessionTokenCache } from './SessionTokenCache'; +import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import uuid from 'uuid'; import { runLiveQueryEventHandlers } from '../triggers'; +import { getAuthForSessionToken, Auth } from '../Auth'; +import { getCacheController } from '../Controllers'; +import LRU from 'lru-cache'; class ParseLiveQueryServer { clients: Map; @@ -21,12 +24,13 @@ class ParseLiveQueryServer { // The subscriber we use to get object update from publisher subscriber: Object; - constructor(server: any, config: any) { + constructor(server: any, config: any = {}) { this.server = server; this.clients = new Map(); this.subscriptions = new Map(); - config = config || {}; + config.appId = config.appId || Parse.applicationId; + config.masterKey = config.masterKey || Parse.masterKey; // Store keys, convert obj to map const keyPairs = config.keyPairs || {}; @@ -38,14 +42,20 @@ class ParseLiveQueryServer { // Initialize Parse Parse.Object.disableSingleInstance(); - const serverURL = config.serverURL || Parse.serverURL; Parse.serverURL = serverURL; - const appId = config.appId || Parse.applicationId; - const javascriptKey = Parse.javaScriptKey; - const masterKey = config.masterKey || Parse.masterKey; - Parse.initialize(appId, javascriptKey, masterKey); + Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey); + + // The cache controller is a proper cache controller + // with access to User and Roles + this.cacheController = getCacheController(config); + // This auth cache stores the promises for each auth resolution. + // The main benefit is to be able to reuse the same user / session token resolution. + this.authCache = new LRU({ + max: 500, // 500 concurrent + maxAge: 60 * 60 * 1000, // 1h + }); // Initialize websocket server this.parseWebSocketServer = new ParseWebSocketServer( server, @@ -81,9 +91,6 @@ class ParseLiveQueryServer { ); } }); - - // Initialize sessionToken cache - this.sessionTokenCache = new SessionTokenCache(config.cacheTimeout); } // Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes. @@ -111,6 +118,7 @@ class ParseLiveQueryServer { logger.verbose(Parse.applicationId + 'afterDelete is triggered'); const deletedParseObject = message.currentParseObject.toJSON(); + const classLevelPermissions = message.classLevelPermissions; const className = deletedParseObject.className; logger.verbose( 'ClassName: %j | ObjectId: %s', @@ -141,18 +149,28 @@ class ParseLiveQueryServer { } for (const requestId of requestIds) { const acl = message.currentParseObject.getACL(); - // Check ACL - this._matchesACL(acl, client, requestId).then( - isMatched => { + // Check CLP + const op = this._getCLPOperation(subscription.query); + this._matchesCLP( + classLevelPermissions, + message.currentParseObject, + client, + requestId, + op + ) + .then(() => { + // Check ACL + return this._matchesACL(acl, client, requestId); + }) + .then(isMatched => { if (!isMatched) { return null; } client.pushDelete(requestId, deletedParseObject); - }, - error => { + }) + .catch(error => { logger.error('Matching ACL error : ', error); - } - ); + }); } } } @@ -167,6 +185,7 @@ class ParseLiveQueryServer { if (message.originalParseObject) { originalParseObject = message.originalParseObject.toJSON(); } + const classLevelPermissions = message.classLevelPermissions; const currentParseObject = message.currentParseObject.toJSON(); const className = currentParseObject.className; logger.verbose( @@ -227,45 +246,55 @@ class ParseLiveQueryServer { requestId ); } - - Promise.all([ - originalACLCheckingPromise, - currentACLCheckingPromise, - ]).then( - ([isOriginalMatched, isCurrentMatched]) => { - logger.verbose( - 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', - originalParseObject, - currentParseObject, - isOriginalSubscriptionMatched, - isCurrentSubscriptionMatched, - isOriginalMatched, - isCurrentMatched, - subscription.hash - ); - - // Decide event type - let type; - if (isOriginalMatched && isCurrentMatched) { - type = 'Update'; - } else if (isOriginalMatched && !isCurrentMatched) { - type = 'Leave'; - } else if (!isOriginalMatched && isCurrentMatched) { - if (originalParseObject) { - type = 'Enter'; + const op = this._getCLPOperation(subscription.query); + this._matchesCLP( + classLevelPermissions, + message.currentParseObject, + client, + requestId, + op + ) + .then(() => { + return Promise.all([ + originalACLCheckingPromise, + currentACLCheckingPromise, + ]); + }) + .then( + ([isOriginalMatched, isCurrentMatched]) => { + logger.verbose( + 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', + originalParseObject, + currentParseObject, + isOriginalSubscriptionMatched, + isCurrentSubscriptionMatched, + isOriginalMatched, + isCurrentMatched, + subscription.hash + ); + + // Decide event type + let type; + if (isOriginalMatched && isCurrentMatched) { + type = 'Update'; + } else if (isOriginalMatched && !isCurrentMatched) { + type = 'Leave'; + } else if (!isOriginalMatched && isCurrentMatched) { + if (originalParseObject) { + type = 'Enter'; + } else { + type = 'Create'; + } } else { - type = 'Create'; + return null; } - } else { - return null; + const functionName = 'push' + type; + client[functionName](requestId, currentParseObject); + }, + error => { + logger.error('Matching ACL error : ', error); } - const functionName = 'push' + type; - client[functionName](requestId, currentParseObject); - }, - error => { - logger.error('Matching ACL error : ', error); - } - ); + ); } } } @@ -374,98 +403,149 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } - _matchesACL(acl: any, client: any, requestId: number): any { + getAuthForSessionToken( + sessionToken: ?string + ): Promise<{ auth: ?Auth, userId: ?string }> { + if (!sessionToken) { + return Promise.resolve({}); + } + const fromCache = this.authCache.get(sessionToken); + if (fromCache) { + return fromCache; + } + const authPromise = getAuthForSessionToken({ + cacheController: this.cacheController, + sessionToken: sessionToken, + }) + .then(auth => { + return { auth, userId: auth && auth.user && auth.user.id }; + }) + .catch(() => { + // If you can't continue, let's just wrap it up and delete it. + // Next time, one will try again + this.authCache.del(sessionToken); + return {}; + }); + this.authCache.set(sessionToken, authPromise); + return authPromise; + } + + async _matchesCLP( + classLevelPermissions: ?any, + object: any, + client: any, + requestId: number, + op: string + ): any { + // try to match on user first, less expensive than with roles + const subscriptionInfo = client.getSubscriptionInfo(requestId); + const aclGroup = ['*']; + let userId; + if (typeof subscriptionInfo !== 'undefined') { + const { userId } = await this.getAuthForSessionToken( + subscriptionInfo.sessionToken + ); + if (userId) { + aclGroup.push(userId); + } + } + try { + await SchemaController.validatePermission( + classLevelPermissions, + object.className, + aclGroup, + op + ); + return true; + } catch (e) { + logger.verbose(`Failed matching CLP for ${object.id} ${userId} ${e}`); + return false; + } + // TODO: handle roles permissions + // Object.keys(classLevelPermissions).forEach((key) => { + // const perm = classLevelPermissions[key]; + // Object.keys(perm).forEach((key) => { + // if (key.indexOf('role')) + // }); + // }) + // // it's rejected here, check the roles + // var rolesQuery = new Parse.Query(Parse.Role); + // rolesQuery.equalTo("users", user); + // return rolesQuery.find({useMasterKey:true}); + } + + _getCLPOperation(query: any) { + return typeof query === 'object' && + Object.keys(query).length == 1 && + typeof query.objectId === 'string' + ? 'get' + : 'find'; + } + + async _matchesACL( + acl: any, + client: any, + requestId: number + ): Promise { // Return true directly if ACL isn't present, ACL is public read, or client has master key if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { - return Promise.resolve(true); + return true; } // Check subscription sessionToken matches ACL first const subscriptionInfo = client.getSubscriptionInfo(requestId); if (typeof subscriptionInfo === 'undefined') { - return Promise.resolve(false); + return false; } - const subscriptionSessionToken = subscriptionInfo.sessionToken; - return this.sessionTokenCache - .getUserId(subscriptionSessionToken) - .then(userId => { - return acl.getReadAccess(userId); - }) - .then(isSubscriptionSessionTokenMatched => { - if (isSubscriptionSessionTokenMatched) { - return Promise.resolve(true); + // TODO: get auth there and de-duplicate code below to work with the same Auth obj. + const { auth, userId } = await this.getAuthForSessionToken( + subscriptionInfo.sessionToken + ); + const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId); + if (isSubscriptionSessionTokenMatched) { + return true; + } + + // Check if the user has any roles that match the ACL + return Promise.resolve() + .then(async () => { + // Resolve false right away if the acl doesn't have any roles + const acl_has_roles = Object.keys(acl.permissionsById).some(key => + key.startsWith('role:') + ); + if (!acl_has_roles) { + return false; } - // Check if the user has any roles that match the ACL - return new Promise((resolve, reject) => { - // Resolve false right away if the acl doesn't have any roles - const acl_has_roles = Object.keys(acl.permissionsById).some(key => - key.startsWith('role:') - ); - if (!acl_has_roles) { - return resolve(false); + const roleNames = await auth.getUserRoles(); + // Finally, see if any of the user's roles allow them read access + for (const role of roleNames) { + // We use getReadAccess as `role` is in the form `role:roleName` + if (acl.getReadAccess(role)) { + return true; } - - this.sessionTokenCache - .getUserId(subscriptionSessionToken) - .then(userId => { - // Pass along a null if there is no user id - if (!userId) { - return Promise.resolve(null); - } - - // Prepare a user object to query for roles - // To eliminate a query for the user, create one locally with the id - var user = new Parse.User(); - user.id = userId; - return user; - }) - .then(user => { - // Pass along an empty array (of roles) if no user - if (!user) { - return Promise.resolve([]); - } - - // Then get the user's roles - var rolesQuery = new Parse.Query(Parse.Role); - rolesQuery.equalTo('users', user); - return rolesQuery.find({ useMasterKey: true }); - }) - .then(roles => { - // Finally, see if any of the user's roles allow them read access - for (const role of roles) { - if (acl.getRoleReadAccess(role)) { - return resolve(true); - } - } - resolve(false); - }) - .catch(error => { - reject(error); - }); - }); + } + return false; }) - .then(isRoleMatched => { + .then(async isRoleMatched => { if (isRoleMatched) { return Promise.resolve(true); } // Check client sessionToken matches ACL const clientSessionToken = client.sessionToken; - return this.sessionTokenCache - .getUserId(clientSessionToken) - .then(userId => { - return acl.getReadAccess(userId); - }); - }) - .then( - isMatched => { - return Promise.resolve(isMatched); - }, - () => { - return Promise.resolve(false); + if (clientSessionToken) { + const { userId } = await this.getAuthForSessionToken( + clientSessionToken + ); + return acl.getReadAccess(userId); + } else { + return isRoleMatched; } - ); + }) + .catch(() => { + return false; + }); } _handleConnect(parseWebsocket: any, request: any): any { diff --git a/src/RestWrite.js b/src/RestWrite.js index 75a5ef0dfa..315402cb00 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1440,12 +1440,18 @@ RestWrite.prototype.runAfterTrigger = function() { this.response.status || 200 ); - // Notifiy LiveQueryServer if possible - this.config.liveQueryController.onAfterSave( - updatedObject.className, - updatedObject, - originalObject - ); + this.config.database.loadSchema().then(schemaController => { + // Notifiy LiveQueryServer if possible + const perms = schemaController.getClassLevelPermissions( + updatedObject.className + ); + this.config.liveQueryController.onAfterSave( + updatedObject.className, + updatedObject, + originalObject, + perms + ); + }); // Run afterSave trigger return triggers diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 1703c3d4db..b7c02fa2f5 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -361,7 +361,7 @@ export class UsersRouter extends ClassesRouter { // be used to enumerate valid emails return Promise.resolve({ response: {}, - }) + }); } else { throw err; } diff --git a/src/rest.js b/src/rest.js index ff1533b267..3a9ee338ae 100644 --- a/src/rest.js +++ b/src/rest.js @@ -101,7 +101,7 @@ function del(config, auth, className, objectId) { enforceRoleSecurity('delete', className, auth); - var inflatedObject; + let inflatedObject; return Promise.resolve() .then(() => { @@ -113,7 +113,7 @@ function del(config, auth, className, objectId) { if (hasTriggers || hasLiveQuery || className == '_Session') { return new RestQuery(config, auth, className, { objectId }) .forWrite() - .execute() + .execute({ op: 'delete' }) .then(response => { if (response && response.results && response.results.length) { const firstResult = response.results[0]; @@ -172,7 +172,15 @@ function del(config, auth, className, objectId) { }) .then(() => { // Notify LiveQuery server if possible - config.liveQueryController.onAfterDelete(className, inflatedObject); + config.database.loadSchema().then(schemaController => { + const perms = schemaController.getClassLevelPermissions(className); + config.liveQueryController.onAfterDelete( + className, + inflatedObject, + null, + perms + ); + }); return triggers.maybeRunTrigger( triggers.Types.afterDelete, auth, diff --git a/src/triggers.js b/src/triggers.js index ac47383065..ead83cac5c 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -269,7 +269,7 @@ export function getResponseObject(request, resolve, reject) { if (error instanceof Parse.Error) { reject(error); } else if (error instanceof Error) { - reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error.message)) + reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error.message)); } else { reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error)); }