Live query CLP (#4387)
* Auth module refactoring in order to be reusable * Ensure cache controller is properly forwarded from helpers * Nits * Adds support for static validation * Adds support for CLP in Live query (no support for roles yet) * Adds e2e test to validate liveQuery hooks is properly called * Adds tests over LiveQueryController to ensure data is correctly transmitted * nits * Fixes for flow types * Removes usage of Parse.Promise * Use the Auth module for authentication and caches * Cleaner implementation of getting auth * Adds authCache that stores auth promises * Proper testing of the caching * nits
This commit is contained in:
@@ -120,4 +120,33 @@ describe('Auth', () => {
|
|||||||
expect(userAuth.user instanceof Parse.User).toBe(true);
|
expect(userAuth.user instanceof Parse.User).toBe(true);
|
||||||
expect(userAuth.user.id).toBe(user.id);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -933,7 +933,7 @@ describe('Cloud Code', () => {
|
|||||||
expect(response.data.result).toEqual('second data');
|
expect(response.data.result).toEqual('second data');
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(e => done.fail(e));
|
.catch(done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => {
|
it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const Parse = require('parse/node');
|
|||||||
const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer')
|
const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer')
|
||||||
.ParseLiveQueryServer;
|
.ParseLiveQueryServer;
|
||||||
const ParseServer = require('../lib/ParseServer').default;
|
const ParseServer = require('../lib/ParseServer').default;
|
||||||
|
const LiveQueryController = require('../lib/Controllers/LiveQueryController')
|
||||||
|
.LiveQueryController;
|
||||||
|
const auth = require('../lib/Auth');
|
||||||
|
|
||||||
// Global mock info
|
// Global mock info
|
||||||
const queryHashValue = 'hash';
|
const queryHashValue = 'hash';
|
||||||
@@ -84,29 +87,28 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
'ParsePubSub',
|
'ParsePubSub',
|
||||||
mockParsePubSub
|
mockParsePubSub
|
||||||
);
|
);
|
||||||
// Make mock SessionTokenCache
|
spyOn(auth, 'getAuthForSessionToken').and.callFake(
|
||||||
const mockSessionTokenCache = function() {
|
({ sessionToken, cacheController }) => {
|
||||||
this.getUserId = function(sessionToken) {
|
|
||||||
if (typeof sessionToken === 'undefined') {
|
if (typeof sessionToken === 'undefined') {
|
||||||
return Promise.resolve(undefined);
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
if (sessionToken === null) {
|
if (sessionToken === null) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
return Promise.resolve(testUserId);
|
if (sessionToken === 'pleaseThrow') {
|
||||||
};
|
return Promise.reject();
|
||||||
};
|
}
|
||||||
jasmine.mockLibrary(
|
return Promise.resolve(
|
||||||
'../lib/LiveQuery/SessionTokenCache',
|
new auth.Auth({ cacheController, user: { id: testUserId } })
|
||||||
'SessionTokenCache',
|
);
|
||||||
mockSessionTokenCache
|
}
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be initialized', function() {
|
it('can be initialized', function() {
|
||||||
const httpServer = {};
|
const httpServer = {};
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer);
|
const parseLiveQueryServer = new ParseLiveQueryServer(httpServer);
|
||||||
|
|
||||||
expect(parseLiveQueryServer.clientId).toBeUndefined();
|
expect(parseLiveQueryServer.clientId).toBeUndefined();
|
||||||
expect(parseLiveQueryServer.clients.size).toBe(0);
|
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() {
|
it('can handle connect command', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
const parseWebSocket = {
|
const parseWebSocket = {
|
||||||
clientId: -1,
|
clientId: -1,
|
||||||
};
|
};
|
||||||
@@ -198,7 +289,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle subscribe command without clientId', function() {
|
it('can handle subscribe command without clientId', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
const incompleteParseConn = {};
|
const incompleteParseConn = {};
|
||||||
parseLiveQueryServer._handleSubscribe(incompleteParseConn, {});
|
parseLiveQueryServer._handleSubscribe(incompleteParseConn, {});
|
||||||
|
|
||||||
@@ -207,7 +298,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle subscribe command with new query', function() {
|
it('can handle subscribe command with new query', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Add mock client
|
// Add mock client
|
||||||
const clientId = 1;
|
const clientId = 1;
|
||||||
const client = addMockClient(parseLiveQueryServer, clientId);
|
const client = addMockClient(parseLiveQueryServer, clientId);
|
||||||
@@ -254,7 +345,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle subscribe command with existing query', function() {
|
it('can handle subscribe command with existing query', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Add two mock clients
|
// Add two mock clients
|
||||||
const clientId = 1;
|
const clientId = 1;
|
||||||
addMockClient(parseLiveQueryServer, clientId);
|
addMockClient(parseLiveQueryServer, clientId);
|
||||||
@@ -318,7 +409,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle unsubscribe command without clientId', function() {
|
it('can handle unsubscribe command without clientId', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
const incompleteParseConn = {};
|
const incompleteParseConn = {};
|
||||||
parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {});
|
parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {});
|
||||||
|
|
||||||
@@ -327,7 +418,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle unsubscribe command without not existed client', function() {
|
it('can handle unsubscribe command without not existed client', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
const parseWebSocket = {
|
const parseWebSocket = {
|
||||||
clientId: 1,
|
clientId: 1,
|
||||||
};
|
};
|
||||||
@@ -338,7 +429,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle unsubscribe command without not existed query', function() {
|
it('can handle unsubscribe command without not existed query', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Add mock client
|
// Add mock client
|
||||||
const clientId = 1;
|
const clientId = 1;
|
||||||
addMockClient(parseLiveQueryServer, clientId);
|
addMockClient(parseLiveQueryServer, clientId);
|
||||||
@@ -353,7 +444,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle unsubscribe command', function() {
|
it('can handle unsubscribe command', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Add mock client
|
// Add mock client
|
||||||
const clientId = 1;
|
const clientId = 1;
|
||||||
const client = addMockClient(parseLiveQueryServer, clientId);
|
const client = addMockClient(parseLiveQueryServer, clientId);
|
||||||
@@ -393,7 +484,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can set connect command message handler for a parseWebSocket', 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
|
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||||
parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe');
|
parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe');
|
||||||
// Make mock parseWebsocket
|
// Make mock parseWebsocket
|
||||||
@@ -415,7 +506,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can set subscribe command message handler for a parseWebSocket', 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
|
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||||
parseLiveQueryServer._handleSubscribe = jasmine.createSpy(
|
parseLiveQueryServer._handleSubscribe = jasmine.createSpy(
|
||||||
'_handleSubscribe'
|
'_handleSubscribe'
|
||||||
@@ -441,7 +532,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can set unsubscribe command message handler for a parseWebSocket', 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
|
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||||
parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy(
|
parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy(
|
||||||
'_handleSubscribe'
|
'_handleSubscribe'
|
||||||
@@ -467,7 +558,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can set update command message handler for a parseWebSocket', 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
|
// Register mock connect/subscribe/unsubscribe handler for the server
|
||||||
spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough();
|
spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough();
|
||||||
spyOn(parseLiveQueryServer, '_handleUnsubscribe').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() {
|
it('can set missing command message handler for a parseWebSocket', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Make mock parseWebsocket
|
// Make mock parseWebsocket
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const parseWebSocket = new EventEmitter();
|
const parseWebSocket = new EventEmitter();
|
||||||
@@ -518,7 +609,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can set unknown command message handler for a parseWebSocket', 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
|
// Make mock parseWebsocket
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const parseWebSocket = new EventEmitter();
|
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() {
|
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 EventEmitter = require('events');
|
||||||
const parseWebSocket = new EventEmitter();
|
const parseWebSocket = new EventEmitter();
|
||||||
parseWebSocket.clientId = 1;
|
parseWebSocket.clientId = 1;
|
||||||
@@ -552,7 +643,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
};
|
};
|
||||||
const spy = spyOn(cloudCodeHandler, 'handler').and.callThrough();
|
const spy = spyOn(cloudCodeHandler, 'handler').and.callThrough();
|
||||||
Parse.Cloud.onLiveQueryEvent(cloudCodeHandler.handler);
|
Parse.Cloud.onLiveQueryEvent(cloudCodeHandler.handler);
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const parseWebSocket = new EventEmitter();
|
const parseWebSocket = new EventEmitter();
|
||||||
parseWebSocket.clientId = 1;
|
parseWebSocket.clientId = 1;
|
||||||
@@ -570,7 +661,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
// TODO: Test server can set disconnect command message handler for a parseWebSocket
|
// TODO: Test server can set disconnect command message handler for a parseWebSocket
|
||||||
|
|
||||||
it('has no subscription and can handle object delete command', function() {
|
it('has no subscription and can handle object delete command', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Make deletedParseObject
|
// Make deletedParseObject
|
||||||
const parseObject = new Parse.Object(testClassName);
|
const parseObject = new Parse.Object(testClassName);
|
||||||
parseObject._finishFetch({
|
parseObject._finishFetch({
|
||||||
@@ -586,7 +677,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle object delete command which does not match any subscription', 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
|
// Make deletedParseObject
|
||||||
const parseObject = new Parse.Object(testClassName);
|
const parseObject = new Parse.Object(testClassName);
|
||||||
parseObject._finishFetch({
|
parseObject._finishFetch({
|
||||||
@@ -619,7 +710,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle object delete command which matches some subscriptions', function(done) {
|
it('can handle object delete command which matches some subscriptions', function(done) {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Make deletedParseObject
|
// Make deletedParseObject
|
||||||
const parseObject = new Parse.Object(testClassName);
|
const parseObject = new Parse.Object(testClassName);
|
||||||
parseObject._finishFetch({
|
parseObject._finishFetch({
|
||||||
@@ -655,7 +746,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has no subscription and can handle object save command', 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
|
// Make mock request message
|
||||||
const message = generateMockMessage();
|
const message = generateMockMessage();
|
||||||
// Make sure we do not crash in this case
|
// 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) {
|
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
|
// Make mock request message
|
||||||
const message = generateMockMessage();
|
const message = generateMockMessage();
|
||||||
// Add mock client
|
// Add mock client
|
||||||
@@ -694,7 +785,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle object enter command which matches some subscriptions', function(done) {
|
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
|
// Make mock request message
|
||||||
const message = generateMockMessage(true);
|
const message = generateMockMessage(true);
|
||||||
// Add mock client
|
// Add mock client
|
||||||
@@ -731,7 +822,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle object update command which matches some subscriptions', function(done) {
|
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
|
// Make mock request message
|
||||||
const message = generateMockMessage(true);
|
const message = generateMockMessage(true);
|
||||||
// Add mock client
|
// Add mock client
|
||||||
@@ -764,7 +855,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle object leave command which matches some subscriptions', function(done) {
|
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
|
// Make mock request message
|
||||||
const message = generateMockMessage(true);
|
const message = generateMockMessage(true);
|
||||||
// Add mock client
|
// Add mock client
|
||||||
@@ -801,7 +892,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can handle object create command which matches some subscriptions', function(done) {
|
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
|
// Make mock request message
|
||||||
const message = generateMockMessage();
|
const message = generateMockMessage();
|
||||||
// Add mock client
|
// Add mock client
|
||||||
@@ -834,7 +925,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match subscription for null or undefined parse object', 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
|
// Make mock subscription
|
||||||
const subscription = {
|
const subscription = {
|
||||||
match: jasmine.createSpy('match'),
|
match: jasmine.createSpy('match'),
|
||||||
@@ -851,7 +942,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match subscription', function() {
|
it('can match subscription', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Make mock subscription
|
// Make mock subscription
|
||||||
const subscription = {
|
const subscription = {
|
||||||
query: {},
|
query: {},
|
||||||
@@ -866,7 +957,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can inflate parse object', function() {
|
it('can inflate parse object', function() {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
// Make mock request
|
// Make mock request
|
||||||
const objectJSON = {
|
const objectJSON = {
|
||||||
className: 'testClassName',
|
className: 'testClassName',
|
||||||
@@ -908,7 +999,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match undefined ACL', function(done) {
|
it('can match undefined ACL', function(done) {
|
||||||
const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
|
const parseLiveQueryServer = new ParseLiveQueryServer({});
|
||||||
const client = {};
|
const client = {};
|
||||||
const requestId = 0;
|
const requestId = 0;
|
||||||
|
|
||||||
@@ -921,7 +1012,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match ACL with none exist requestId', function(done) {
|
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 acl = new Parse.ACL();
|
||||||
const client = {
|
const client = {
|
||||||
getSubscriptionInfo: jasmine
|
getSubscriptionInfo: jasmine
|
||||||
@@ -939,7 +1030,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match ACL with public read access', function(done) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(true);
|
acl.setPublicReadAccess(true);
|
||||||
const client = {
|
const client = {
|
||||||
@@ -960,7 +1051,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match ACL with valid subscription sessionToken', function(done) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setReadAccess(testUserId, true);
|
acl.setReadAccess(testUserId, true);
|
||||||
const client = {
|
const client = {
|
||||||
@@ -981,7 +1072,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('can match ACL with valid client sessionToken', function(done) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setReadAccess(testUserId, true);
|
acl.setReadAccess(testUserId, true);
|
||||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
// 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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setReadAccess(testUserId, true);
|
acl.setReadAccess(testUserId, true);
|
||||||
// Mock sessionTokenCache will return false when sessionToken is undefined
|
// 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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setReadAccess(testUserId, true);
|
acl.setReadAccess(testUserId, true);
|
||||||
// Mock sessionTokenCache will return error when sessionToken is null, this is just
|
// 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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setReadAccess(testUserId, true);
|
acl.setReadAccess(testUserId, true);
|
||||||
// Mock sessionTokenCache will return error when sessionToken is null
|
// 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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(false);
|
acl.setPublicReadAccess(false);
|
||||||
const client = {
|
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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(false);
|
acl.setPublicReadAccess(false);
|
||||||
acl.setRoleReadAccess('livequery', true);
|
acl.setRoleReadAccess('livequery', true);
|
||||||
@@ -1110,14 +1201,15 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
.then(function(isMatched) {
|
.then(function(isMatched) {
|
||||||
expect(isMatched).toBe(false);
|
expect(isMatched).toBe(false);
|
||||||
done();
|
done();
|
||||||
});
|
})
|
||||||
|
.catch(done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("won't match ACL with role based read access set to false", function(done) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(false);
|
acl.setPublicReadAccess(false);
|
||||||
acl.setRoleReadAccess('liveQueryRead', false);
|
acl.setRoleReadAccess('otherLiveQueryRead', true);
|
||||||
const client = {
|
const client = {
|
||||||
getSubscriptionInfo: jasmine
|
getSubscriptionInfo: jasmine
|
||||||
.createSpy('getSubscriptionInfo')
|
.createSpy('getSubscriptionInfo')
|
||||||
@@ -1128,15 +1220,28 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
const requestId = 0;
|
const requestId = 0;
|
||||||
|
|
||||||
spyOn(Parse, 'Query').and.callFake(function() {
|
spyOn(Parse, 'Query').and.callFake(function() {
|
||||||
|
let shouldReturn = false;
|
||||||
return {
|
return {
|
||||||
equalTo() {
|
equalTo() {
|
||||||
|
shouldReturn = true;
|
||||||
// Nothing to do here
|
// Nothing to do here
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
containedIn() {
|
||||||
|
shouldReturn = false;
|
||||||
|
return this;
|
||||||
},
|
},
|
||||||
find() {
|
find() {
|
||||||
|
if (!shouldReturn) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
|
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
|
||||||
const liveQueryRole = new Parse.Role();
|
const liveQueryRole = new Parse.Role(
|
||||||
liveQueryRole.set('name', 'liveQueryRead');
|
'liveQueryRead',
|
||||||
return [liveQueryRole];
|
new Parse.ACL()
|
||||||
|
);
|
||||||
|
liveQueryRole.id = 'abcdef1234';
|
||||||
|
return Promise.resolve([liveQueryRole]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1147,10 +1252,17 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
expect(isMatched).toBe(false);
|
expect(isMatched).toBe(false);
|
||||||
done();
|
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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(false);
|
acl.setPublicReadAccess(false);
|
||||||
acl.setRoleReadAccess('liveQueryRead', true);
|
acl.setRoleReadAccess('liveQueryRead', true);
|
||||||
@@ -1164,15 +1276,28 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
const requestId = 0;
|
const requestId = 0;
|
||||||
|
|
||||||
spyOn(Parse, 'Query').and.callFake(function() {
|
spyOn(Parse, 'Query').and.callFake(function() {
|
||||||
|
let shouldReturn = false;
|
||||||
return {
|
return {
|
||||||
equalTo() {
|
equalTo() {
|
||||||
|
shouldReturn = true;
|
||||||
// Nothing to do here
|
// Nothing to do here
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
containedIn() {
|
||||||
|
shouldReturn = false;
|
||||||
|
return this;
|
||||||
},
|
},
|
||||||
find() {
|
find() {
|
||||||
|
if (!shouldReturn) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
|
//Return a role with the name "liveQueryRead" as that is what was set on the ACL
|
||||||
const liveQueryRole = new Parse.Role();
|
const liveQueryRole = new Parse.Role(
|
||||||
liveQueryRole.set('name', 'liveQueryRead');
|
'liveQueryRead',
|
||||||
return [liveQueryRole];
|
new Parse.ACL()
|
||||||
|
);
|
||||||
|
liveQueryRole.id = 'abcdef1234';
|
||||||
|
return Promise.resolve([liveQueryRole]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1183,6 +1308,139 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
expect(isMatched).toBe(true);
|
expect(isMatched).toBe(true);
|
||||||
done();
|
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() {
|
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) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(false);
|
acl.setPublicReadAccess(false);
|
||||||
const client = {
|
const client = {
|
||||||
@@ -1329,7 +1587,7 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("won't match non-public ACL when client has no master key", function(done) {
|
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();
|
const acl = new Parse.ACL();
|
||||||
acl.setPublicReadAccess(false);
|
acl.setPublicReadAccess(false);
|
||||||
const client = {
|
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() {
|
afterEach(function() {
|
||||||
jasmine.restoreLibrary(
|
jasmine.restoreLibrary(
|
||||||
'../lib/LiveQuery/ParseWebSocketServer',
|
'../lib/LiveQuery/ParseWebSocketServer',
|
||||||
@@ -1358,10 +1639,6 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'queryHash');
|
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'queryHash');
|
||||||
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery');
|
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery');
|
||||||
jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub');
|
jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub');
|
||||||
jasmine.restoreLibrary(
|
|
||||||
'../lib/LiveQuery/SessionTokenCache',
|
|
||||||
'SessionTokenCache'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper functions to add mock client and subscription to a liveQueryServer
|
// Helper functions to add mock client and subscription to a liveQueryServer
|
||||||
@@ -1443,3 +1720,139 @@ describe('ParseLiveQueryServer', function() {
|
|||||||
return message;
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -144,19 +144,22 @@ const reconfigureServer = changedConfiguration => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
let parseServer = undefined;
|
||||||
const newConfiguration = Object.assign(
|
const newConfiguration = Object.assign(
|
||||||
{},
|
{},
|
||||||
defaultConfiguration,
|
defaultConfiguration,
|
||||||
changedConfiguration,
|
changedConfiguration,
|
||||||
{
|
{
|
||||||
__indexBuildCompletionCallbackForTests: indexBuildPromise =>
|
__indexBuildCompletionCallbackForTests: indexBuildPromise =>
|
||||||
indexBuildPromise.then(resolve, reject),
|
indexBuildPromise.then(() => {
|
||||||
|
resolve(parseServer);
|
||||||
|
}, reject),
|
||||||
mountPath: '/1',
|
mountPath: '/1',
|
||||||
port,
|
port,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
cache.clear();
|
cache.clear();
|
||||||
const parseServer = ParseServer.start(newConfiguration);
|
parseServer = ParseServer.start(newConfiguration);
|
||||||
parseServer.app.use(require('./testing-routes').router);
|
parseServer.app.use(require('./testing-routes').router);
|
||||||
parseServer.expressApp.use('/1', err => {
|
parseServer.expressApp.use('/1', err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@@ -1335,7 +1335,7 @@ class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addPointerPermissions(
|
addPointerPermissions(
|
||||||
schema: any,
|
schema: SchemaController.SchemaController,
|
||||||
className: string,
|
className: string,
|
||||||
operation: string,
|
operation: string,
|
||||||
query: any,
|
query: any,
|
||||||
@@ -1343,10 +1343,10 @@ class DatabaseController {
|
|||||||
) {
|
) {
|
||||||
// Check if class has public permission for operation
|
// Check if class has public permission for operation
|
||||||
// If the BaseCLP pass, let go through
|
// If the BaseCLP pass, let go through
|
||||||
if (schema.testBaseCLP(className, aclGroup, operation)) {
|
if (schema.testPermissionsForClassName(className, aclGroup, operation)) {
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
const perms = schema.schemaData[className].classLevelPermissions;
|
const perms = schema.getClassLevelPermissions(className);
|
||||||
const field =
|
const field =
|
||||||
['get', 'find'].indexOf(operation) > -1
|
['get', 'find'].indexOf(operation) > -1
|
||||||
? 'readUserFields'
|
? 'readUserFields'
|
||||||
|
|||||||
@@ -16,19 +16,37 @@ export class LiveQueryController {
|
|||||||
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
|
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)) {
|
if (!this.hasLiveQuery(className)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const req = this._makePublisherRequest(currentObject, originalObject);
|
const req = this._makePublisherRequest(
|
||||||
|
currentObject,
|
||||||
|
originalObject,
|
||||||
|
classLevelPermissions
|
||||||
|
);
|
||||||
this.liveQueryPublisher.onCloudCodeAfterSave(req);
|
this.liveQueryPublisher.onCloudCodeAfterSave(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
onAfterDelete(className: string, currentObject: any, originalObject: any) {
|
onAfterDelete(
|
||||||
|
className: string,
|
||||||
|
currentObject: any,
|
||||||
|
originalObject: any,
|
||||||
|
classLevelPermissions: any
|
||||||
|
) {
|
||||||
if (!this.hasLiveQuery(className)) {
|
if (!this.hasLiveQuery(className)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const req = this._makePublisherRequest(currentObject, originalObject);
|
const req = this._makePublisherRequest(
|
||||||
|
currentObject,
|
||||||
|
originalObject,
|
||||||
|
classLevelPermissions
|
||||||
|
);
|
||||||
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
|
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,13 +54,20 @@ export class LiveQueryController {
|
|||||||
return this.classNames.has(className);
|
return this.classNames.has(className);
|
||||||
}
|
}
|
||||||
|
|
||||||
_makePublisherRequest(currentObject: any, originalObject: any): any {
|
_makePublisherRequest(
|
||||||
|
currentObject: any,
|
||||||
|
originalObject: any,
|
||||||
|
classLevelPermissions: ?any
|
||||||
|
): any {
|
||||||
const req = {
|
const req = {
|
||||||
object: currentObject,
|
object: currentObject,
|
||||||
};
|
};
|
||||||
if (currentObject) {
|
if (currentObject) {
|
||||||
req.original = originalObject;
|
req.original = originalObject;
|
||||||
}
|
}
|
||||||
|
if (classLevelPermissions) {
|
||||||
|
req.classLevelPermissions = classLevelPermissions;
|
||||||
|
}
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1122,18 +1122,28 @@ export default class SchemaController {
|
|||||||
return Promise.resolve(this);
|
return Promise.resolve(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates the base CLP for an operation
|
testPermissionsForClassName(
|
||||||
testBaseCLP(className: string, aclGroup: string[], operation: string) {
|
className: string,
|
||||||
const classSchema = this.schemaData[className];
|
aclGroup: string[],
|
||||||
if (
|
operation: string
|
||||||
!classSchema ||
|
) {
|
||||||
!classSchema.classLevelPermissions ||
|
return SchemaController.testPermissions(
|
||||||
!classSchema.classLevelPermissions[operation]
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
const perms = classSchema.classLevelPermissions[operation];
|
const perms = classPermissions[operation];
|
||||||
// Handle the public scenario quickly
|
|
||||||
if (perms['*']) {
|
if (perms['*']) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1149,21 +1159,22 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validates an operation passes class-level-permissions set in the schema
|
// Validates an operation passes class-level-permissions set in the schema
|
||||||
validatePermission(className: string, aclGroup: string[], operation: string) {
|
static validatePermission(
|
||||||
if (this.testBaseCLP(className, aclGroup, operation)) {
|
classPermissions: ?any,
|
||||||
|
className: string,
|
||||||
|
aclGroup: string[],
|
||||||
|
operation: string
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
SchemaController.testPermissions(classPermissions, aclGroup, operation)
|
||||||
|
) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
const classSchema = this.schemaData[className];
|
|
||||||
if (
|
if (!classPermissions || !classPermissions[operation]) {
|
||||||
!classSchema ||
|
|
||||||
!classSchema.classLevelPermissions ||
|
|
||||||
!classSchema.classLevelPermissions[operation]
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const classPerms = classSchema.classLevelPermissions;
|
const perms = classPermissions[operation];
|
||||||
const perms = classSchema.classLevelPermissions[operation];
|
|
||||||
|
|
||||||
// If only for authenticated users
|
// If only for authenticated users
|
||||||
// make sure we have an aclGroup
|
// make sure we have an aclGroup
|
||||||
if (perms['requiresAuthentication']) {
|
if (perms['requiresAuthentication']) {
|
||||||
@@ -1201,8 +1212,8 @@ export default class SchemaController {
|
|||||||
|
|
||||||
// Process the readUserFields later
|
// Process the readUserFields later
|
||||||
if (
|
if (
|
||||||
Array.isArray(classPerms[permissionField]) &&
|
Array.isArray(classPermissions[permissionField]) &&
|
||||||
classPerms[permissionField].length > 0
|
classPermissions[permissionField].length > 0
|
||||||
) {
|
) {
|
||||||
return Promise.resolve();
|
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
|
// Returns the expected type for a className+key combination
|
||||||
// or undefined if the schema is not set
|
// or undefined if the schema is not set
|
||||||
getExpectedType(
|
getExpectedType(
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import logger from '../logger';
|
|||||||
import RequestSchema from './RequestSchema';
|
import RequestSchema from './RequestSchema';
|
||||||
import { matchesQuery, queryHash } from './QueryTools';
|
import { matchesQuery, queryHash } from './QueryTools';
|
||||||
import { ParsePubSub } from './ParsePubSub';
|
import { ParsePubSub } from './ParsePubSub';
|
||||||
import { SessionTokenCache } from './SessionTokenCache';
|
import SchemaController from '../Controllers/SchemaController';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
import { runLiveQueryEventHandlers } from '../triggers';
|
import { runLiveQueryEventHandlers } from '../triggers';
|
||||||
|
import { getAuthForSessionToken, Auth } from '../Auth';
|
||||||
|
import { getCacheController } from '../Controllers';
|
||||||
|
import LRU from 'lru-cache';
|
||||||
|
|
||||||
class ParseLiveQueryServer {
|
class ParseLiveQueryServer {
|
||||||
clients: Map;
|
clients: Map;
|
||||||
@@ -21,12 +24,13 @@ class ParseLiveQueryServer {
|
|||||||
// The subscriber we use to get object update from publisher
|
// The subscriber we use to get object update from publisher
|
||||||
subscriber: Object;
|
subscriber: Object;
|
||||||
|
|
||||||
constructor(server: any, config: any) {
|
constructor(server: any, config: any = {}) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.clients = new Map();
|
this.clients = new Map();
|
||||||
this.subscriptions = 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
|
// Store keys, convert obj to map
|
||||||
const keyPairs = config.keyPairs || {};
|
const keyPairs = config.keyPairs || {};
|
||||||
@@ -38,14 +42,20 @@ class ParseLiveQueryServer {
|
|||||||
|
|
||||||
// Initialize Parse
|
// Initialize Parse
|
||||||
Parse.Object.disableSingleInstance();
|
Parse.Object.disableSingleInstance();
|
||||||
|
|
||||||
const serverURL = config.serverURL || Parse.serverURL;
|
const serverURL = config.serverURL || Parse.serverURL;
|
||||||
Parse.serverURL = serverURL;
|
Parse.serverURL = serverURL;
|
||||||
const appId = config.appId || Parse.applicationId;
|
Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey);
|
||||||
const javascriptKey = Parse.javaScriptKey;
|
|
||||||
const masterKey = config.masterKey || Parse.masterKey;
|
|
||||||
Parse.initialize(appId, javascriptKey, 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
|
// Initialize websocket server
|
||||||
this.parseWebSocketServer = new ParseWebSocketServer(
|
this.parseWebSocketServer = new ParseWebSocketServer(
|
||||||
server,
|
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.
|
// 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');
|
logger.verbose(Parse.applicationId + 'afterDelete is triggered');
|
||||||
|
|
||||||
const deletedParseObject = message.currentParseObject.toJSON();
|
const deletedParseObject = message.currentParseObject.toJSON();
|
||||||
|
const classLevelPermissions = message.classLevelPermissions;
|
||||||
const className = deletedParseObject.className;
|
const className = deletedParseObject.className;
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
'ClassName: %j | ObjectId: %s',
|
'ClassName: %j | ObjectId: %s',
|
||||||
@@ -141,18 +149,28 @@ class ParseLiveQueryServer {
|
|||||||
}
|
}
|
||||||
for (const requestId of requestIds) {
|
for (const requestId of requestIds) {
|
||||||
const acl = message.currentParseObject.getACL();
|
const acl = message.currentParseObject.getACL();
|
||||||
// Check ACL
|
// Check CLP
|
||||||
this._matchesACL(acl, client, requestId).then(
|
const op = this._getCLPOperation(subscription.query);
|
||||||
isMatched => {
|
this._matchesCLP(
|
||||||
|
classLevelPermissions,
|
||||||
|
message.currentParseObject,
|
||||||
|
client,
|
||||||
|
requestId,
|
||||||
|
op
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// Check ACL
|
||||||
|
return this._matchesACL(acl, client, requestId);
|
||||||
|
})
|
||||||
|
.then(isMatched => {
|
||||||
if (!isMatched) {
|
if (!isMatched) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
client.pushDelete(requestId, deletedParseObject);
|
client.pushDelete(requestId, deletedParseObject);
|
||||||
},
|
})
|
||||||
error => {
|
.catch(error => {
|
||||||
logger.error('Matching ACL error : ', error);
|
logger.error('Matching ACL error : ', error);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +185,7 @@ class ParseLiveQueryServer {
|
|||||||
if (message.originalParseObject) {
|
if (message.originalParseObject) {
|
||||||
originalParseObject = message.originalParseObject.toJSON();
|
originalParseObject = message.originalParseObject.toJSON();
|
||||||
}
|
}
|
||||||
|
const classLevelPermissions = message.classLevelPermissions;
|
||||||
const currentParseObject = message.currentParseObject.toJSON();
|
const currentParseObject = message.currentParseObject.toJSON();
|
||||||
const className = currentParseObject.className;
|
const className = currentParseObject.className;
|
||||||
logger.verbose(
|
logger.verbose(
|
||||||
@@ -227,45 +246,55 @@ class ParseLiveQueryServer {
|
|||||||
requestId
|
requestId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
Promise.all([
|
// Decide event type
|
||||||
originalACLCheckingPromise,
|
let type;
|
||||||
currentACLCheckingPromise,
|
if (isOriginalMatched && isCurrentMatched) {
|
||||||
]).then(
|
type = 'Update';
|
||||||
([isOriginalMatched, isCurrentMatched]) => {
|
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||||
logger.verbose(
|
type = 'Leave';
|
||||||
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||||
originalParseObject,
|
if (originalParseObject) {
|
||||||
currentParseObject,
|
type = 'Enter';
|
||||||
isOriginalSubscriptionMatched,
|
} else {
|
||||||
isCurrentSubscriptionMatched,
|
type = 'Create';
|
||||||
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 {
|
} else {
|
||||||
type = 'Create';
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
const functionName = 'push' + type;
|
||||||
return null;
|
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);
|
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<boolean> {
|
||||||
// Return true directly if ACL isn't present, ACL is public read, or client has master key
|
// Return true directly if ACL isn't present, ACL is public read, or client has master key
|
||||||
if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) {
|
if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) {
|
||||||
return Promise.resolve(true);
|
return true;
|
||||||
}
|
}
|
||||||
// Check subscription sessionToken matches ACL first
|
// Check subscription sessionToken matches ACL first
|
||||||
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||||
if (typeof subscriptionInfo === 'undefined') {
|
if (typeof subscriptionInfo === 'undefined') {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionSessionToken = subscriptionInfo.sessionToken;
|
// TODO: get auth there and de-duplicate code below to work with the same Auth obj.
|
||||||
return this.sessionTokenCache
|
const { auth, userId } = await this.getAuthForSessionToken(
|
||||||
.getUserId(subscriptionSessionToken)
|
subscriptionInfo.sessionToken
|
||||||
.then(userId => {
|
);
|
||||||
return acl.getReadAccess(userId);
|
const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId);
|
||||||
})
|
if (isSubscriptionSessionTokenMatched) {
|
||||||
.then(isSubscriptionSessionTokenMatched => {
|
return true;
|
||||||
if (isSubscriptionSessionTokenMatched) {
|
}
|
||||||
return Promise.resolve(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
|
const roleNames = await auth.getUserRoles();
|
||||||
return new Promise((resolve, reject) => {
|
// Finally, see if any of the user's roles allow them read access
|
||||||
// Resolve false right away if the acl doesn't have any roles
|
for (const role of roleNames) {
|
||||||
const acl_has_roles = Object.keys(acl.permissionsById).some(key =>
|
// We use getReadAccess as `role` is in the form `role:roleName`
|
||||||
key.startsWith('role:')
|
if (acl.getReadAccess(role)) {
|
||||||
);
|
return true;
|
||||||
if (!acl_has_roles) {
|
|
||||||
return resolve(false);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.sessionTokenCache
|
return false;
|
||||||
.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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then(isRoleMatched => {
|
.then(async isRoleMatched => {
|
||||||
if (isRoleMatched) {
|
if (isRoleMatched) {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check client sessionToken matches ACL
|
// Check client sessionToken matches ACL
|
||||||
const clientSessionToken = client.sessionToken;
|
const clientSessionToken = client.sessionToken;
|
||||||
return this.sessionTokenCache
|
if (clientSessionToken) {
|
||||||
.getUserId(clientSessionToken)
|
const { userId } = await this.getAuthForSessionToken(
|
||||||
.then(userId => {
|
clientSessionToken
|
||||||
return acl.getReadAccess(userId);
|
);
|
||||||
});
|
return acl.getReadAccess(userId);
|
||||||
})
|
} else {
|
||||||
.then(
|
return isRoleMatched;
|
||||||
isMatched => {
|
|
||||||
return Promise.resolve(isMatched);
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
}
|
||||||
);
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleConnect(parseWebsocket: any, request: any): any {
|
_handleConnect(parseWebsocket: any, request: any): any {
|
||||||
|
|||||||
@@ -1440,12 +1440,18 @@ RestWrite.prototype.runAfterTrigger = function() {
|
|||||||
this.response.status || 200
|
this.response.status || 200
|
||||||
);
|
);
|
||||||
|
|
||||||
// Notifiy LiveQueryServer if possible
|
this.config.database.loadSchema().then(schemaController => {
|
||||||
this.config.liveQueryController.onAfterSave(
|
// Notifiy LiveQueryServer if possible
|
||||||
updatedObject.className,
|
const perms = schemaController.getClassLevelPermissions(
|
||||||
updatedObject,
|
updatedObject.className
|
||||||
originalObject
|
);
|
||||||
);
|
this.config.liveQueryController.onAfterSave(
|
||||||
|
updatedObject.className,
|
||||||
|
updatedObject,
|
||||||
|
originalObject,
|
||||||
|
perms
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// Run afterSave trigger
|
// Run afterSave trigger
|
||||||
return triggers
|
return triggers
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
// be used to enumerate valid emails
|
// be used to enumerate valid emails
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
response: {},
|
response: {},
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/rest.js
14
src/rest.js
@@ -101,7 +101,7 @@ function del(config, auth, className, objectId) {
|
|||||||
|
|
||||||
enforceRoleSecurity('delete', className, auth);
|
enforceRoleSecurity('delete', className, auth);
|
||||||
|
|
||||||
var inflatedObject;
|
let inflatedObject;
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -113,7 +113,7 @@ function del(config, auth, className, objectId) {
|
|||||||
if (hasTriggers || hasLiveQuery || className == '_Session') {
|
if (hasTriggers || hasLiveQuery || className == '_Session') {
|
||||||
return new RestQuery(config, auth, className, { objectId })
|
return new RestQuery(config, auth, className, { objectId })
|
||||||
.forWrite()
|
.forWrite()
|
||||||
.execute()
|
.execute({ op: 'delete' })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response && response.results && response.results.length) {
|
if (response && response.results && response.results.length) {
|
||||||
const firstResult = response.results[0];
|
const firstResult = response.results[0];
|
||||||
@@ -172,7 +172,15 @@ function del(config, auth, className, objectId) {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Notify LiveQuery server if possible
|
// 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(
|
return triggers.maybeRunTrigger(
|
||||||
triggers.Types.afterDelete,
|
triggers.Types.afterDelete,
|
||||||
auth,
|
auth,
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export function getResponseObject(request, resolve, reject) {
|
|||||||
if (error instanceof Parse.Error) {
|
if (error instanceof Parse.Error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
} else if (error instanceof 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 {
|
} else {
|
||||||
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
|
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user