Skip to content

Commit 81b27eb

Browse files
committed
Adds support for CLP in Live query (no support for roles yet)
1 parent ba73e74 commit 81b27eb

File tree

5 files changed

+166
-16
lines changed

5 files changed

+166
-16
lines changed

spec/ParseLiveQueryServer.spec.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,92 @@ describe('ParseLiveQueryServer', function() {
10581058

10591059
});
10601060

1061+
describe('class level permissions', () => {
1062+
it('matches CLP when find is closed', (done) => {
1063+
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
1064+
var acl = new Parse.ACL();
1065+
acl.setReadAccess(testUserId, true);
1066+
// Mock sessionTokenCache will return false when sessionToken is undefined
1067+
var client = {
1068+
sessionToken: 'sessionToken',
1069+
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
1070+
sessionToken: undefined
1071+
})
1072+
};
1073+
var requestId = 0;
1074+
1075+
parseLiveQueryServer._matchesCLP({
1076+
find: {}
1077+
}, { className: 'Yolo' }, client, requestId, 'find').then((isMatched) => {
1078+
expect(isMatched).toBe(false);
1079+
done();
1080+
});
1081+
});
1082+
1083+
it('matches CLP when find is open', (done) => {
1084+
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
1085+
var acl = new Parse.ACL();
1086+
acl.setReadAccess(testUserId, true);
1087+
// Mock sessionTokenCache will return false when sessionToken is undefined
1088+
var client = {
1089+
sessionToken: 'sessionToken',
1090+
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
1091+
sessionToken: undefined
1092+
})
1093+
};
1094+
var requestId = 0;
1095+
1096+
parseLiveQueryServer._matchesCLP({
1097+
find: { '*': true }
1098+
}, { className: 'Yolo' }, client, requestId, 'find').then((isMatched) => {
1099+
expect(isMatched).toBe(true);
1100+
done();
1101+
});
1102+
});
1103+
1104+
it('matches CLP when find is restricted to userIds', (done) => {
1105+
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
1106+
var acl = new Parse.ACL();
1107+
acl.setReadAccess(testUserId, true);
1108+
// Mock sessionTokenCache will return false when sessionToken is undefined
1109+
var client = {
1110+
sessionToken: 'sessionToken',
1111+
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
1112+
sessionToken: 'userId'
1113+
})
1114+
};
1115+
var requestId = 0;
1116+
1117+
parseLiveQueryServer._matchesCLP({
1118+
find: { 'userId': true }
1119+
}, { className: 'Yolo' }, client, requestId, 'find').then((isMatched) => {
1120+
expect(isMatched).toBe(true);
1121+
done();
1122+
});
1123+
});
1124+
1125+
it('matches CLP when find is restricted to userIds', (done) => {
1126+
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
1127+
var acl = new Parse.ACL();
1128+
acl.setReadAccess(testUserId, true);
1129+
// Mock sessionTokenCache will return false when sessionToken is undefined
1130+
var client = {
1131+
sessionToken: 'sessionToken',
1132+
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
1133+
sessionToken: undefined
1134+
})
1135+
};
1136+
var requestId = 0;
1137+
1138+
parseLiveQueryServer._matchesCLP({
1139+
find: { 'userId': true }
1140+
}, { className: 'Yolo' }, client, requestId, 'find').then((isMatched) => {
1141+
expect(isMatched).toBe(false);
1142+
done();
1143+
});
1144+
});
1145+
});
1146+
10611147
it('can validate key when valid key is provided', function() {
10621148
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
10631149
keyPairs: {

src/Controllers/LiveQueryController.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,36 @@ export class LiveQueryController {
1616
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
1717
}
1818

19-
onAfterSave(className: string, currentObject: any, originalObject: any) {
19+
onAfterSave(className: string, currentObject: any, originalObject: any, classLevelPermissions: ?any) {
2020
if (!this.hasLiveQuery(className)) {
2121
return;
2222
}
23-
const req = this._makePublisherRequest(currentObject, originalObject);
23+
const req = this._makePublisherRequest(currentObject, originalObject, classLevelPermissions);
2424
this.liveQueryPublisher.onCloudCodeAfterSave(req);
2525
}
2626

27-
onAfterDelete(className: string, currentObject: any, originalObject: any) {
27+
onAfterDelete(className: string, currentObject: any, originalObject: any, classLevelPermissions: any) {
2828
if (!this.hasLiveQuery(className)) {
2929
return;
3030
}
31-
const req = this._makePublisherRequest(currentObject, originalObject);
31+
const req = this._makePublisherRequest(currentObject, originalObject, classLevelPermissions);
3232
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
3333
}
3434

3535
hasLiveQuery(className: string): boolean {
3636
return this.classNames.has(className);
3737
}
3838

39-
_makePublisherRequest(currentObject: any, originalObject: any): any {
39+
_makePublisherRequest(currentObject: any, originalObject: any, classLevelPermissions: ?any): any {
4040
const req = {
4141
object: currentObject
4242
};
4343
if (currentObject) {
4444
req.original = originalObject;
4545
}
46+
if (classLevelPermissions) {
47+
req.classLevelPermissions = classLevelPermissions;
48+
}
4649
return req;
4750
}
4851
}

src/LiveQuery/ParseLiveQueryServer.js

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import RequestSchema from './RequestSchema';
88
import { matchesQuery, queryHash } from './QueryTools';
99
import { ParsePubSub } from './ParsePubSub';
1010
import { SessionTokenCache } from './SessionTokenCache';
11+
import SchemaController from '../Controllers/SchemaController';
1112
import _ from 'lodash';
1213
import uuid from 'uuid';
1314
import { runLiveQueryEventHandlers } from '../triggers';
@@ -107,6 +108,7 @@ class ParseLiveQueryServer {
107108
logger.verbose(Parse.applicationId + 'afterDelete is triggered');
108109

109110
const deletedParseObject = message.currentParseObject.toJSON();
111+
const classLevelPermissions = message.classLevelPermissions;
110112
const className = deletedParseObject.className;
111113
logger.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id);
112114
logger.verbose('Current client number : %d', this.clients.size);
@@ -128,13 +130,17 @@ class ParseLiveQueryServer {
128130
}
129131
for (const requestId of requestIds) {
130132
const acl = message.currentParseObject.getACL();
131-
// Check ACL
132-
this._matchesACL(acl, client, requestId).then((isMatched) => {
133+
// Check CLP
134+
const op = this._getCLPOperation(subscription.query);
135+
this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op).then(() => {
136+
// Check ACL
137+
return this._matchesACL(acl, client, requestId)
138+
}).then((isMatched) => {
133139
if (!isMatched) {
134140
return null;
135141
}
136142
client.pushDelete(requestId, deletedParseObject);
137-
}, (error) => {
143+
}).catch((error) => {
138144
logger.error('Matching ACL error : ', error);
139145
});
140146
}
@@ -151,6 +157,7 @@ class ParseLiveQueryServer {
151157
if (message.originalParseObject) {
152158
originalParseObject = message.originalParseObject.toJSON();
153159
}
160+
const classLevelPermissions = message.classLevelPermissions;
154161
const currentParseObject = message.currentParseObject.toJSON();
155162
const className = currentParseObject.className;
156163
logger.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id);
@@ -191,11 +198,13 @@ class ParseLiveQueryServer {
191198
const currentACL = message.currentParseObject.getACL();
192199
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
193200
}
194-
195-
Parse.Promise.when(
196-
originalACLCheckingPromise,
197-
currentACLCheckingPromise
198-
).then((isOriginalMatched, isCurrentMatched) => {
201+
const op = this._getCLPOperation(subscription.query);
202+
this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op).then(() => {
203+
return Parse.Promise.when(
204+
originalACLCheckingPromise,
205+
currentACLCheckingPromise
206+
);
207+
}).then((isOriginalMatched, isCurrentMatched) => {
199208
logger.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
200209
originalParseObject,
201210
currentParseObject,
@@ -327,6 +336,52 @@ class ParseLiveQueryServer {
327336
return matchesQuery(parseObject, subscription.query);
328337
}
329338

339+
_matchesCLP(classLevelPermissions: ?any, object: any, client: any, requestId: number, op: string): any {
340+
return Parse.Promise.as().then(() => {
341+
// try to match on user first, less expensive than with roles
342+
const subscriptionInfo = client.getSubscriptionInfo(requestId);
343+
if (typeof subscriptionInfo === 'undefined') {
344+
return Parse.Promise.as(['*']);
345+
}
346+
let foundUserId;
347+
const subscriptionSessionToken = subscriptionInfo.sessionToken;
348+
return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => {
349+
foundUserId = userId;
350+
if (userId) {
351+
return Parse.Promise.as(['*', userId]);
352+
}
353+
return Parse.Promise.as(['*']);
354+
}).then((aclGroup) => {
355+
console.log(aclGroup); // eslint-disable-line
356+
try {
357+
return SchemaController.validatePermission(classLevelPermissions, object.className, aclGroup, op).then(() => {
358+
return Parse.Promise.as(true);
359+
});
360+
} catch(e) {
361+
logger.verbose(`Failed matching CLP for ${object.id} ${foundUserId} ${e}`);
362+
return Parse.Promise.as(false);
363+
}
364+
// TODO: handle roles permissions
365+
// Object.keys(classLevelPermissions).forEach((key) => {
366+
// const perm = classLevelPermissions[key];
367+
// Object.keys(perm).forEach((key) => {
368+
// if (key.indexOf('role'))
369+
// });
370+
// })
371+
// // it's rejected here, check the roles
372+
// var rolesQuery = new Parse.Query(Parse.Role);
373+
// rolesQuery.equalTo("users", user);
374+
// return rolesQuery.find({useMasterKey:true});
375+
});
376+
})
377+
}
378+
379+
_getCLPOperation(query: any) {
380+
return typeof query == 'object'
381+
&& Object.keys(query).length == 1
382+
&& typeof query.objectId === 'string' ? 'get' : 'find';
383+
}
384+
330385
_matchesACL(acl: any, client: any, requestId: number): any {
331386
// Return true directly if ACL isn't present, ACL is public read, or client has master key
332387
if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) {

src/RestWrite.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,8 +1137,11 @@ RestWrite.prototype.runAfterTrigger = function() {
11371137
const updatedObject = this.buildUpdatedObject(extraData);
11381138
updatedObject._handleSaveResponse(this.response.response, this.response.status || 200);
11391139

1140-
// Notifiy LiveQueryServer if possible
1141-
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject);
1140+
this.config.database.loadSchema().then((schemaController) => {
1141+
// Notifiy LiveQueryServer if possible
1142+
const perms = schemaController.perms[updatedObject.className];
1143+
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject, perms);
1144+
});
11421145

11431146
// Run afterSave trigger
11441147
return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config)

src/rest.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ function del(config, auth, className, objectId) {
8181
cacheAdapter.user.del(firstResult.sessionToken);
8282
inflatedObject = Parse.Object.fromJSON(firstResult);
8383
// Notify LiveQuery server if possible
84-
config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject);
84+
config.database.loadSchema().then((schemaController) => {
85+
const perms = schemaController.perms[inflatedObject.className];
86+
config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject, perms);
87+
});
8588
return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config);
8689
}
8790
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,

0 commit comments

Comments
 (0)