From 82f8d5bfe827785878355b6d8f67850c08ec1004 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 15:56:35 -0400 Subject: [PATCH 01/15] Auth module refactoring in order to be reusable --- spec/Auth.spec.js | 33 ++++++- spec/CloudCode.spec.js | 5 +- spec/ParseRole.spec.js | 12 ++- src/Auth.js | 207 +++++++++++++++++++++++++---------------- 4 files changed, 167 insertions(+), 90 deletions(-) diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index a4d97639f0..81aacb01aa 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -1,6 +1,6 @@ describe('Auth', () => { - const Auth = require('../lib/Auth.js').Auth; - + const { Auth, getAuthForSessionToken } = require('../lib/Auth.js'); + const Config = require('../lib/Config'); describe('getUserRoles', () => { let auth; let config; @@ -90,4 +90,33 @@ describe('Auth', () => { }); }); + + it('should load auth without a config', async () => { + const user = new Parse.User(); + await user.signUp({ + username: 'hello', + password: 'password' + }); + expect(user.getSessionToken()).not.toBeUndefined(); + const userAuth = await getAuthForSessionToken({ + sessionToken: user.getSessionToken() + }); + expect(userAuth.user instanceof Parse.User).toBe(true); + expect(userAuth.user.id).toBe(user.id); + }); + + it('should load auth with a config', async () => { + const user = new Parse.User(); + await user.signUp({ + username: 'hello', + password: 'password' + }); + expect(user.getSessionToken()).not.toBeUndefined(); + const userAuth = await getAuthForSessionToken({ + sessionToken: user.getSessionToken(), + config: Config.get('test'), + }); + expect(userAuth.user instanceof Parse.User).toBe(true); + expect(userAuth.user.id).toBe(user.id); + }); }); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 4d52bbe369..ba8956a684 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -832,10 +832,7 @@ describe('Cloud Code', () => { expect(body.result).toEqual('second data'); done(); }) - .catch(error => { - fail(JSON.stringify(error)); - done(); - }); + .catch(done.fail); }); it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => { diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 19f34bb447..283f1cafaa 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -142,7 +142,7 @@ describe('Parse Role testing', () => { }); - it("should recursively load roles", (done) => { + function testLoadRoles(config, done) { const rolesNames = ["FooRole", "BarRole", "BazRole"]; const roleIds = {}; createTestUser().then((user) => { @@ -159,7 +159,7 @@ describe('Parse Role testing', () => { return createRole(rolesNames[2], anotherRole, null); }).then((lastRole) => { roleIds[lastRole.get("name")] = lastRole.id; - const auth = new Auth({ config: Config.get("test"), isMaster: true, user: user }); + const auth = new Auth({ config, isMaster: true, user: user }); return auth._loadRoles(); }) }).then((roles) => { @@ -172,6 +172,14 @@ describe('Parse Role testing', () => { fail("should succeed") done(); }); + } + + it("should recursively load roles", (done) => { + testLoadRoles(Config.get('test'), done); + }); + + it("should recursively load roles without config", (done) => { + testLoadRoles(undefined, done); }); it("_Role object should not save without name.", (done) => { diff --git a/src/Auth.js b/src/Auth.js index 8658f13025..9dc66fe3bc 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -5,8 +5,9 @@ const Parse = require('parse/node'); // An Auth object tells you who is requesting something and whether // the master key was used. // userObject is a Parse.User and can be null if there's no user. -function Auth({ config, isMaster = false, isReadOnly = false, user, installationId } = {}) { +function Auth({ config, cacheController = undefined, isMaster = false, isReadOnly = false, user, installationId }) { this.config = config; + this.cacheController = cacheController || (config && config.cacheController); this.installationId = installationId; this.isMaster = isMaster; this.user = user; @@ -48,43 +49,54 @@ function nobody(config) { // Returns a promise that resolves to an Auth object -var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) { - return config.cacheController.user.get(sessionToken).then((userJSON) => { +var getAuthForSessionToken = async function({ config, cacheController, sessionToken, installationId }) { + cacheController = cacheController || (config && config.cacheController); + if (cacheController) { + const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser})); } + } + let results; + if (config) { var restOptions = { limit: 1, include: 'user' }; var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions); - return query.execute().then((response) => { - var results = response.results; - if (results.length !== 1 || !results[0]['user']) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); - } + results = (await query.execute()).results; + } else { + results = (await new Parse.Query(Parse.Session) + .limit(1) + .include('user') + .equalTo('sessionToken', sessionToken) + .find({ useMasterKey: true })).map((obj) => obj.toJSON()) + } - var now = new Date(), - expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; - if (expiresAt < now) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token is expired.'); - } - var obj = results[0]['user']; - delete obj.password; - obj['className'] = '_User'; - obj['sessionToken'] = sessionToken; - config.cacheController.user.put(sessionToken, obj); - const userObject = Parse.Object.fromJSON(obj); - return new Auth({config, isMaster: false, installationId, user: userObject}); - }); - }); + if (results.length !== 1 || !results[0]['user']) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + } + var now = new Date(), + expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; + if (expiresAt < now) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, + 'Session token is expired.'); + } + var obj = results[0]['user']; + delete obj.password; + obj['className'] = '_User'; + obj['sessionToken'] = sessionToken; + if (cacheController) { + cacheController.user.put(sessionToken, obj); + } + const userObject = Parse.Object.fromJSON(obj); + return new Auth({config, isMaster: false, installationId, user: userObject }); }; -var getAuthForLegacySessionToken = function({config, sessionToken, installationId } = {}) { +var getAuthForLegacySessionToken = function({config, sessionToken, installationId }) { var restOptions = { limit: 1 }; @@ -116,84 +128,115 @@ Auth.prototype.getUserRoles = function() { return this.rolePromise; }; -// Iterates through the role tree and compiles a users roles -Auth.prototype._loadRoles = function() { - var cacheAdapter = this.config.cacheController; - return cacheAdapter.role.get(this.user.id).then((cachedRoles) => { - if (cachedRoles != null) { - this.fetchedRoles = true; - this.userRoles = cachedRoles; - return Promise.resolve(cachedRoles); - } - - var restWhere = { +Auth.prototype.getRolesForUser = function() { + if (this.config) { + const restWhere = { 'users': { __type: 'Pointer', className: '_User', objectId: this.user.id } }; - // First get the role ids this user is directly a member of - var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); - return query.execute().then((response) => { - var results = response.results; - if (!results.length) { - this.userRoles = []; - this.fetchedRoles = true; - this.rolePromise = null; - - cacheAdapter.role.put(this.user.id, Array(...this.userRoles)); - return Promise.resolve(this.userRoles); - } - var rolesMap = results.reduce((m, r) => { - m.names.push(r.name); - m.ids.push(r.objectId); - return m; - }, {ids: [], names: []}); - - // run the recursive finding - return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) - .then((roleNames) => { - this.userRoles = roleNames.map((r) => { - return 'role:' + r; - }); - this.fetchedRoles = true; - this.rolePromise = null; - cacheAdapter.role.put(this.user.id, Array(...this.userRoles)); - return Promise.resolve(this.userRoles); - }); + const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); + return query.execute().then(({ results }) => { + return results; }); + } + + return new Parse.Query(Parse.Role) + .equalTo('users', this.user) + .find({ useMasterKey: true }) + .then((results) => results.map((obj) => obj.toJSON())); +} + +// Iterates through the role tree and compiles a users roles +Auth.prototype._loadRoles = async function() { + if (this.cacheController) { + const cachedRoles = await this.cacheController.role.get(this.user.id); + if (cachedRoles != null) { + this.fetchedRoles = true; + this.userRoles = cachedRoles; + return cachedRoles; + } + } + + // First get the role ids this user is directly a member of + const results = await this.getRolesForUser(); + if (!results.length) { + this.userRoles = []; + this.fetchedRoles = true; + this.rolePromise = null; + + this.cacheRoles(); + return this.userRoles; + } + + const rolesMap = results.reduce((m, r) => { + m.names.push(r.name); + m.ids.push(r.objectId); + return m; + }, {ids: [], names: []}); + + // run the recursive finding + const roleNames = await this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names); + this.userRoles = roleNames.map((r) => { + return 'role:' + r; }); + this.fetchedRoles = true; + this.rolePromise = null; + this.cacheRoles(); + return this.userRoles; }; -// Given a list of roleIds, find all the parent roles, returns a promise with all names -Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) { - const ins = roleIDs.filter((roleID) => { - return queriedRoles[roleID] !== true; - }).map((roleID) => { - // mark as queried - queriedRoles[roleID] = true; +Auth.prototype.cacheRoles = function() { + if (!this.cacheController) { + return false; + } + this.cacheController.role.put(this.user.id, Array(...this.userRoles)); + return true; +} + +Auth.prototype.getRolesByIds = function(ins) { + const roles = ins.map((id) => { return { __type: 'Pointer', className: '_Role', - objectId: roleID + objectId: id } }); + const restWhere = { 'roles': { '$in': roles }}; + + // Build an OR query across all parentRoles + if (!this.config) { + return new Parse.Query(Parse.Role) + .containedIn('roles', ins.map((id) => { + const role = new Parse.Object(Parse.Role); + role.id = id; + return role; + })) + .find({ useMasterKey: true }) + .then((results) => results.map((obj) => obj.toJSON())); + } + + return new RestQuery(this.config, master(this.config), '_Role', restWhere, {}) + .execute() + .then(({ results }) => results); +} + +// Given a list of roleIds, find all the parent roles, returns a promise with all names +Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) { + const ins = roleIDs.filter((roleID) => { + const wasQueried = queriedRoles[roleID] !== true; + queriedRoles[roleID] = true; + return wasQueried; + }); // all roles are accounted for, return the names if (ins.length == 0) { return Promise.resolve([...new Set(names)]); } - // Build an OR query across all parentRoles - let restWhere; - if (ins.length == 1) { - restWhere = { 'roles': ins[0] }; - } else { - restWhere = { 'roles': { '$in': ins }} - } - const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); - return query.execute().then((response) => { - var results = response.results; + + return this.getRolesByIds(ins).then((results) => { // Nothing found if (!results.length) { return Promise.resolve(names); From 455ab164dce7281c4d957fbc57da64a16e5357b9 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 20:45:20 -0400 Subject: [PATCH 02/15] Ensure cache controller is properly forwarded from helpers --- src/Auth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Auth.js b/src/Auth.js index 9dc66fe3bc..d4623ac9e7 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -49,13 +49,13 @@ function nobody(config) { // Returns a promise that resolves to an Auth object -var getAuthForSessionToken = async function({ config, cacheController, sessionToken, installationId }) { +const getAuthForSessionToken = async function({ config, cacheController, sessionToken, installationId }) { cacheController = cacheController || (config && config.cacheController); if (cacheController) { const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); - return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser})); + return Promise.resolve(new Auth({config, cacheController, isMaster: false, installationId, user: cachedUser})); } } @@ -93,7 +93,7 @@ var getAuthForSessionToken = async function({ config, cacheController, sessionTo cacheController.user.put(sessionToken, obj); } const userObject = Parse.Object.fromJSON(obj); - return new Auth({config, isMaster: false, installationId, user: userObject }); + return new Auth({config, cacheController, isMaster: false, installationId, user: userObject }); }; var getAuthForLegacySessionToken = function({config, sessionToken, installationId }) { From cf5c32cd3fef57e694aa1203ea460e2575330201 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Thu, 9 Aug 2018 08:43:58 -0400 Subject: [PATCH 03/15] Nits --- src/Auth.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Auth.js b/src/Auth.js index d4623ac9e7..c4ec748b7a 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -61,12 +61,12 @@ const getAuthForSessionToken = async function({ config, cacheController, session let results; if (config) { - var restOptions = { + const restOptions = { limit: 1, include: 'user' }; - var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions); + const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions); results = (await query.execute()).results; } else { results = (await new Parse.Query(Parse.Session) @@ -79,13 +79,13 @@ const getAuthForSessionToken = async function({ config, cacheController, session if (results.length !== 1 || !results[0]['user']) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); } - var now = new Date(), + const now = new Date(), expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; if (expiresAt < now) { throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.'); } - var obj = results[0]['user']; + const obj = results[0]['user']; delete obj.password; obj['className'] = '_User'; obj['sessionToken'] = sessionToken; @@ -93,14 +93,14 @@ const getAuthForSessionToken = async function({ config, cacheController, session cacheController.user.put(sessionToken, obj); } const userObject = Parse.Object.fromJSON(obj); - return new Auth({config, cacheController, isMaster: false, installationId, user: userObject }); + return new Auth({ config, cacheController, isMaster: false, installationId, user: userObject }); }; -var getAuthForLegacySessionToken = function({config, sessionToken, installationId }) { +var getAuthForLegacySessionToken = function({ config, sessionToken, installationId }) { var restOptions = { limit: 1 }; - var query = new RestQuery(config, master(config), '_User', { sessionToken: sessionToken}, restOptions); + var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions); return query.execute().then((response) => { var results = response.results; if (results.length !== 1) { @@ -109,7 +109,7 @@ var getAuthForLegacySessionToken = function({config, sessionToken, installationI const obj = results[0]; obj.className = '_User'; const userObject = Parse.Object.fromJSON(obj); - return new Auth({config, isMaster: false, installationId, user: userObject}); + return new Auth({ config, isMaster: false, installationId, user: userObject }); }); } @@ -138,9 +138,7 @@ Auth.prototype.getRolesForUser = function() { } }; const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); - return query.execute().then(({ results }) => { - return results; - }); + return query.execute().then(({ results }) => results); } return new Parse.Query(Parse.Role) @@ -149,7 +147,7 @@ Auth.prototype.getRolesForUser = function() { .then((results) => results.map((obj) => obj.toJSON())); } -// Iterates through the role tree and compiles a users roles +// Iterates through the role tree and compiles a user's roles Auth.prototype._loadRoles = async function() { if (this.cacheController) { const cachedRoles = await this.cacheController.role.get(this.user.id); From d3fea5c98f4e59fbb465410a4add8fa7b0448345 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 25 Nov 2017 20:55:13 -0500 Subject: [PATCH 04/15] Adds support for static validation --- src/Controllers/SchemaController.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 7c133f269c..14a374866c 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -861,12 +861,14 @@ export default class SchemaController { // Validates the base CLP for an operation testBaseCLP(className: string, aclGroup: string[], operation: string) { - if (!this.perms[className] || !this.perms[className][operation]) { + return SchemaController.testBaseCLP(this.perms[className], aclGroup, operation); + } + + static testBaseCLP(classPermissions: ?any, className: string, aclGroup: string[], operation: string) { + if (!classPermissions || !classPermissions[operation]) { return true; } - const classPerms = this.perms[className]; - const perms = classPerms[operation]; - // Handle the public scenario quickly + const perms = classPermissions[operation]; if (perms['*']) { return true; } @@ -878,18 +880,15 @@ export default class SchemaController { } // Validates an operation passes class-level-permissions set in the schema - validatePermission(className: string, aclGroup: string[], operation: string) { - - if (this.testBaseCLP(className, aclGroup, operation)) { + static validatePermission(classPermissions: ?any, className: string, aclGroup: string[], operation: string) { + if (SchemaController.testBaseCLP(classPermissions, aclGroup, operation)) { return Promise.resolve(); } - if (!this.perms[className] || !this.perms[className][operation]) { + if (!classPermissions || !classPermissions[operation]) { return true; } - const classPerms = this.perms[className]; - const perms = classPerms[operation]; - + const perms = classPermissions[operation]; // If only for authenticated users // make sure we have an aclGroup if (perms['requiresAuthentication']) { @@ -917,13 +916,18 @@ export default class SchemaController { } // Process the readUserFields later - if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) { + if (Array.isArray(classPermissions[permissionField]) && classPermissions[permissionField].length > 0) { return Promise.resolve(); } throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Permission denied for action ${operation} on class ${className}.`); } + // Validates an operation passes class-level-permissions set in the schema + validatePermission(className, aclGroup, operation) { + return SchemaController.validatePermission(this.perms[className], className, aclGroup, operation); + } + // Returns the expected type for a className+key combination // or undefined if the schema is not set getExpectedType(className: string, fieldName: string): ?(SchemaField | string) { From 0f7b52367c4212bb5a5b61b1677b1a6290639d6d Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 25 Nov 2017 20:55:33 -0500 Subject: [PATCH 05/15] Adds support for CLP in Live query (no support for roles yet) --- spec/ParseLiveQueryServer.spec.js | 86 ++++++++++++++++++++++++++ src/Controllers/LiveQueryController.js | 13 ++-- src/LiveQuery/ParseLiveQueryServer.js | 73 +++++++++++++++++++--- src/RestWrite.js | 7 ++- src/rest.js | 5 +- 5 files changed, 166 insertions(+), 18 deletions(-) diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index de318ecfa3..668756aa91 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -1079,6 +1079,92 @@ describe('ParseLiveQueryServer', function() { }); + describe('class level permissions', () => { + it('matches CLP when find is closed', (done) => { + var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + var acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + var client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ + sessionToken: undefined + }) + }; + var 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) => { + var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + var acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + var client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ + sessionToken: undefined + }) + }; + var 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) => { + var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + var acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + var client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ + sessionToken: 'userId' + }) + }; + var 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) => { + var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + var acl = new Parse.ACL(); + acl.setReadAccess(testUserId, true); + // Mock sessionTokenCache will return false when sessionToken is undefined + var client = { + sessionToken: 'sessionToken', + getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ + sessionToken: undefined + }) + }; + var 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() { const parseLiveQueryServer = new ParseLiveQueryServer({}, { keyPairs: { diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 7f741c359c..841aabf897 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -16,19 +16,19 @@ export class LiveQueryController { this.liveQueryPublisher = new ParseCloudCodePublisher(config); } - onAfterSave(className: string, currentObject: any, originalObject: any) { + onAfterSave(className: string, currentObject: any, originalObject: any, classLevelPermissions: ?any) { if (!this.hasLiveQuery(className)) { return; } - const req = this._makePublisherRequest(currentObject, originalObject); + const req = this._makePublisherRequest(currentObject, originalObject, classLevelPermissions); this.liveQueryPublisher.onCloudCodeAfterSave(req); } - onAfterDelete(className: string, currentObject: any, originalObject: any) { + onAfterDelete(className: string, currentObject: any, originalObject: any, classLevelPermissions: any) { if (!this.hasLiveQuery(className)) { return; } - const req = this._makePublisherRequest(currentObject, originalObject); + const req = this._makePublisherRequest(currentObject, originalObject, classLevelPermissions); this.liveQueryPublisher.onCloudCodeAfterDelete(req); } @@ -36,13 +36,16 @@ export class LiveQueryController { return this.classNames.has(className); } - _makePublisherRequest(currentObject: any, originalObject: any): any { + _makePublisherRequest(currentObject: any, originalObject: any, classLevelPermissions: ?any): any { const req = { object: currentObject }; if (currentObject) { req.original = originalObject; } + if (classLevelPermissions) { + req.classLevelPermissions = classLevelPermissions; + } return req; } } diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index fb89ca81b7..3a840fa80d 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -8,6 +8,7 @@ import RequestSchema from './RequestSchema'; import { matchesQuery, queryHash } from './QueryTools'; import { ParsePubSub } from './ParsePubSub'; import { SessionTokenCache } from './SessionTokenCache'; +import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import uuid from 'uuid'; import { runLiveQueryEventHandlers } from '../triggers'; @@ -107,6 +108,7 @@ class ParseLiveQueryServer { logger.verbose(Parse.applicationId + 'afterDelete is triggered'); const deletedParseObject = message.currentParseObject.toJSON(); + const classLevelPermissions = message.classLevelPermissions; const className = deletedParseObject.className; logger.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id); logger.verbose('Current client number : %d', this.clients.size); @@ -128,13 +130,17 @@ class ParseLiveQueryServer { } for (const requestId of requestIds) { const acl = message.currentParseObject.getACL(); - // Check ACL - this._matchesACL(acl, client, requestId).then((isMatched) => { + // Check CLP + const op = this._getCLPOperation(subscription.query); + this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op).then(() => { + // Check ACL + return this._matchesACL(acl, client, requestId) + }).then((isMatched) => { if (!isMatched) { return null; } client.pushDelete(requestId, deletedParseObject); - }, (error) => { + }).catch((error) => { logger.error('Matching ACL error : ', error); }); } @@ -151,6 +157,7 @@ class ParseLiveQueryServer { if (message.originalParseObject) { originalParseObject = message.originalParseObject.toJSON(); } + const classLevelPermissions = message.classLevelPermissions; const currentParseObject = message.currentParseObject.toJSON(); const className = currentParseObject.className; logger.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id); @@ -191,13 +198,13 @@ class ParseLiveQueryServer { const currentACL = message.currentParseObject.getACL(); currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId); } - - Promise.all( - [ - originalACLCheckingPromise, - currentACLCheckingPromise - ] - ).then(([isOriginalMatched, isCurrentMatched]) => { + 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, @@ -329,6 +336,52 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } + _matchesCLP(classLevelPermissions: ?any, object: any, client: any, requestId: number, op: string): any { + return Parse.Promise.as().then(() => { + // try to match on user first, less expensive than with roles + const subscriptionInfo = client.getSubscriptionInfo(requestId); + if (typeof subscriptionInfo === 'undefined') { + return Parse.Promise.as(['*']); + } + let foundUserId; + const subscriptionSessionToken = subscriptionInfo.sessionToken; + return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => { + foundUserId = userId; + if (userId) { + return Parse.Promise.as(['*', userId]); + } + return Parse.Promise.as(['*']); + }).then((aclGroup) => { + console.log(aclGroup); // eslint-disable-line + try { + return SchemaController.validatePermission(classLevelPermissions, object.className, aclGroup, op).then(() => { + return Parse.Promise.as(true); + }); + } catch(e) { + logger.verbose(`Failed matching CLP for ${object.id} ${foundUserId} ${e}`); + return Parse.Promise.as(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'; + } + _matchesACL(acl: any, client: any, requestId: number): any { // Return true directly if ACL isn't present, ACL is public read, or client has master key if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { diff --git a/src/RestWrite.js b/src/RestWrite.js index b91491ed9e..5ade50c4dc 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1138,8 +1138,11 @@ RestWrite.prototype.runAfterTrigger = function() { const updatedObject = this.buildUpdatedObject(extraData); updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); - // Notifiy LiveQueryServer if possible - this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject); + this.config.database.loadSchema().then((schemaController) => { + // Notifiy LiveQueryServer if possible + const perms = schemaController.perms[updatedObject.className]; + this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject, perms); + }); // Run afterSave trigger return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config) diff --git a/src/rest.js b/src/rest.js index 7ff4c852d9..863d873a3d 100644 --- a/src/rest.js +++ b/src/rest.js @@ -82,7 +82,10 @@ function del(config, auth, className, objectId) { cacheAdapter.user.del(firstResult.sessionToken); inflatedObject = Parse.Object.fromJSON(firstResult); // Notify LiveQuery server if possible - config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject); + config.database.loadSchema().then((schemaController) => { + const perms = schemaController.perms[inflatedObject.className]; + config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject, perms); + }); return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config); } throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, From 8c81112ee0856fdc820c91a70b81beabbd06eca8 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 25 Nov 2017 21:54:41 -0500 Subject: [PATCH 06/15] Adds e2e test to validate liveQuery hooks is properly called --- spec/ParseLiveQueryServer.spec.js | 79 +++++++++++++++++++++++++++++++ spec/helper.js | 7 ++- src/rest.js | 2 +- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 668756aa91..55672dab32 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -143,6 +143,85 @@ describe('ParseLiveQueryServer', function() { }); }); + it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + function setPermissionsOnClass(className, permissions, doPut) { + var request = require('request'); + let op = request.post; + if (doPut) + { + op = request.put; + } + return new Promise((resolve, reject) => { + op({ + url: Parse.serverURL + '/schemas/' + className, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + json: true, + body: { + classLevelPermissions: permissions + } + }, (error, response, body) => { + if (error) { + return reject(error); + } + if (body.error) { + return reject(body); + } + return resolve(body); + }) + }); + } + + let saveSpy; + let deleteSpy; + reconfigureServer({ + liveQuery: { + classNames: ['Yolo'] + } + }).then((parseServer) => { + saveSpy = spyOn(parseServer.config.liveQueryController, 'onAfterSave'); + deleteSpy = spyOn(parseServer.config.liveQueryController, 'onAfterDelete'); + return setPermissionsOnClass('Yolo', { + create: {'*': true}, + delete: {'*': true} + }) + }).then(() => { + const obj = new Parse.Object('Yolo'); + return obj.save(); + }).then((obj) => { + return obj.destroy(); + }).then(() => { + expect(saveSpy).toHaveBeenCalled(); + const saveArgs = saveSpy.calls.mostRecent().args; + expect(saveArgs.length).toBe(4); + expect(saveArgs[0]).toBe('Yolo'); + expect(saveArgs[3]).toEqual({ + get: {}, + addField: {}, + create: {'*': true}, + find: {}, + update: {}, + delete: {'*': true}, + }); + + expect(deleteSpy).toHaveBeenCalled(); + const deleteArgs = deleteSpy.calls.mostRecent().args; + expect(deleteArgs.length).toBe(4); + expect(deleteArgs[0]).toBe('Yolo'); + expect(deleteArgs[3]).toEqual({ + get: {}, + addField: {}, + create: {'*': true}, + find: {}, + update: {}, + delete: {'*': true}, + }); + done(); + }).catch(done.fail); + }); + it('can handle connect command', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const parseWebSocket = { diff --git a/spec/helper.js b/spec/helper.js index efa3f3f70a..d56e7c157d 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -127,13 +127,16 @@ const reconfigureServer = changedConfiguration => { }); } try { + var parseServer; const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { - __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject), + __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(() => { + resolve(parseServer); + }, reject), mountPath: '/1', port, }); cache.clear(); - const parseServer = ParseServer.start(newConfiguration); + parseServer = ParseServer.start(newConfiguration); parseServer.app.use(require('./testing-routes').router); parseServer.expressApp.use('/1', (err) => { console.error(err); diff --git a/src/rest.js b/src/rest.js index 863d873a3d..100dddc652 100644 --- a/src/rest.js +++ b/src/rest.js @@ -84,7 +84,7 @@ function del(config, auth, className, objectId) { // Notify LiveQuery server if possible config.database.loadSchema().then((schemaController) => { const perms = schemaController.perms[inflatedObject.className]; - config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject, perms); + config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject, null, perms); }); return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config); } From 1cf580125401dfe508aa6ca8c1faad660eb3ccbd Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 25 Nov 2017 22:42:33 -0500 Subject: [PATCH 07/15] Adds tests over LiveQueryController to ensure data is correctly transmitted --- spec/ParseLiveQueryServer.spec.js | 158 ++++++++++++++++++++++++++---- 1 file changed, 141 insertions(+), 17 deletions(-) diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 55672dab32..79eff33be6 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -1,6 +1,7 @@ const Parse = require('parse/node'); const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer; const ParseServer = require('../lib/ParseServer').default; +const LiveQueryController = require('../lib/Controllers/LiveQueryController').LiveQueryController; // Global mock info const queryHashValue = 'hash'; @@ -145,7 +146,7 @@ describe('ParseLiveQueryServer', function() { it('properly passes the CLP to afterSave/afterDelete hook', function(done) { function setPermissionsOnClass(className, permissions, doPut) { - var request = require('request'); + const request = require('request'); let op = request.post; if (doPut) { @@ -1160,17 +1161,17 @@ describe('ParseLiveQueryServer', function() { describe('class level permissions', () => { it('matches CLP when find is closed', (done) => { - var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); - var acl = new Parse.ACL(); + const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined - var client = { + const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined }) }; - var requestId = 0; + const requestId = 0; parseLiveQueryServer._matchesCLP({ find: {} @@ -1181,17 +1182,17 @@ describe('ParseLiveQueryServer', function() { }); it('matches CLP when find is open', (done) => { - var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); - var acl = new Parse.ACL(); + const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined - var client = { + const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined }) }; - var requestId = 0; + const requestId = 0; parseLiveQueryServer._matchesCLP({ find: { '*': true } @@ -1202,17 +1203,17 @@ describe('ParseLiveQueryServer', function() { }); it('matches CLP when find is restricted to userIds', (done) => { - var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); - var acl = new Parse.ACL(); + const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined - var client = { + const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'userId' }) }; - var requestId = 0; + const requestId = 0; parseLiveQueryServer._matchesCLP({ find: { 'userId': true } @@ -1223,17 +1224,17 @@ describe('ParseLiveQueryServer', function() { }); it('matches CLP when find is restricted to userIds', (done) => { - var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); - var acl = new Parse.ACL(); + const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined - var client = { + const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined }) }; - var requestId = 0; + const requestId = 0; parseLiveQueryServer._matchesCLP({ find: { 'userId': true } @@ -1451,3 +1452,126 @@ describe('ParseLiveQueryServer', function() { return message; } }); + +describe('LiveQueryController', () => { + it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + function setPermissionsOnClass(className, permissions, doPut) { + const request = require('request'); + let op = request.post; + if (doPut) + { + op = request.put; + } + return new Promise((resolve, reject) => { + op({ + url: Parse.serverURL + '/schemas/' + className, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + json: true, + body: { + classLevelPermissions: permissions + } + }, (error, response, body) => { + if (error) { + return reject(error); + } + if (body.error) { + return reject(body); + } + return resolve(body); + }) + }); + } + + let saveSpy; + let deleteSpy; + reconfigureServer({ + liveQuery: { + classNames: ['Yolo'] + } + }).then((parseServer) => { + saveSpy = spyOn(parseServer.config.liveQueryController, 'onAfterSave').and.callThrough(); + deleteSpy = spyOn(parseServer.config.liveQueryController, 'onAfterDelete').and.callThrough(); + return setPermissionsOnClass('Yolo', { + create: {'*': true}, + delete: {'*': true} + }) + }).then(() => { + const obj = new Parse.Object('Yolo'); + return obj.save(); + }).then((obj) => { + return obj.destroy(); + }).then(() => { + expect(saveSpy).toHaveBeenCalled(); + const saveArgs = saveSpy.calls.mostRecent().args; + expect(saveArgs.length).toBe(4); + expect(saveArgs[0]).toBe('Yolo'); + expect(saveArgs[3]).toEqual({ + get: {}, + addField: {}, + create: {'*': true}, + find: {}, + update: {}, + delete: {'*': true}, + }); + + expect(deleteSpy).toHaveBeenCalled(); + const deleteArgs = deleteSpy.calls.mostRecent().args; + expect(deleteArgs.length).toBe(4); + expect(deleteArgs[0]).toBe('Yolo'); + expect(deleteArgs[3]).toEqual({ + get: {}, + addField: {}, + create: {'*': true}, + find: {}, + update: {}, + delete: {'*': true}, + }); + done(); + }).catch(done.fail); + }); + + it('should properly pack message request on afterSave', () => { + const controller = new LiveQueryController({ + classNames: ['Yolo'], + }); + const spy = spyOn(controller.liveQueryPublisher, 'onCloudCodeAfterSave'); + controller.onAfterSave('Yolo', {o: 1}, {o:2}, {yolo: true}); + expect(spy).toHaveBeenCalled() + const args = spy.calls.mostRecent().args; + expect(args.length).toBe(1); + expect(args[0]).toEqual({ + object: {o: 1}, + original: {o: 2}, + classLevelPermissions: {yolo: true} + }) + }); + + it('should properly pack message request on afterDelete', () => { + const controller = new LiveQueryController({ + classNames: ['Yolo'], + }); + const spy = spyOn(controller.liveQueryPublisher, 'onCloudCodeAfterDelete'); + controller.onAfterDelete('Yolo', {o: 1}, {o:2}, {yolo: true}); + expect(spy).toHaveBeenCalled() + const args = spy.calls.mostRecent().args; + expect(args.length).toBe(1); + expect(args[0]).toEqual({ + object: {o: 1}, + original: {o: 2}, + classLevelPermissions: {yolo: true} + }) + }); + + it('should properly pack message request', () => { + const controller = new LiveQueryController({ + classNames: ['Yolo'], + }); + expect(controller._makePublisherRequest({})).toEqual({ + object: {}, + original: undefined, + }); + }); +}); From 35409d37f5d43cef7ec88e78ea13ef132293881f Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 27 Nov 2017 22:17:57 -0500 Subject: [PATCH 08/15] nits --- src/LiveQuery/ParseLiveQueryServer.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 3a840fa80d..78c4a1dfab 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -352,7 +352,6 @@ class ParseLiveQueryServer { } return Parse.Promise.as(['*']); }).then((aclGroup) => { - console.log(aclGroup); // eslint-disable-line try { return SchemaController.validatePermission(classLevelPermissions, object.className, aclGroup, op).then(() => { return Parse.Promise.as(true); @@ -377,7 +376,7 @@ class ParseLiveQueryServer { } _getCLPOperation(query: any) { - return typeof query == 'object' + return typeof query === 'object' && Object.keys(query).length == 1 && typeof query.objectId === 'string' ? 'get' : 'find'; } From b303f7a70abb9e87825de458588a120b4e3ff888 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Mon, 19 Feb 2018 09:41:05 -0500 Subject: [PATCH 09/15] Fixes for flow types --- spec/helper.js | 2 +- src/Controllers/SchemaController.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/helper.js b/spec/helper.js index d56e7c157d..cabaef04f2 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -127,7 +127,7 @@ const reconfigureServer = changedConfiguration => { }); } try { - var parseServer; + let parseServer = undefined; const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(() => { resolve(parseServer); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 14a374866c..71d7066d4d 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -864,7 +864,7 @@ export default class SchemaController { return SchemaController.testBaseCLP(this.perms[className], aclGroup, operation); } - static testBaseCLP(classPermissions: ?any, className: string, aclGroup: string[], operation: string) { + static testBaseCLP(classPermissions: ?any, aclGroup: string[], operation: string) { if (!classPermissions || !classPermissions[operation]) { return true; } @@ -924,7 +924,7 @@ export default class SchemaController { } // Validates an operation passes class-level-permissions set in the schema - validatePermission(className, aclGroup, operation) { + validatePermission(className: string, aclGroup: string[], operation: string) { return SchemaController.validatePermission(this.perms[className], className, aclGroup, operation); } From 45b63a27a864a12233808106af0fdb8bbc1d3b86 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 17:54:38 -0400 Subject: [PATCH 10/15] Removes usage of Parse.Promise --- src/LiveQuery/ParseLiveQueryServer.js | 67 ++++++++++++--------------- src/rest.js | 2 +- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 78c4a1dfab..f28bd40fac 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -336,43 +336,36 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } - _matchesCLP(classLevelPermissions: ?any, object: any, client: any, requestId: number, op: string): any { - return Parse.Promise.as().then(() => { - // try to match on user first, less expensive than with roles - const subscriptionInfo = client.getSubscriptionInfo(requestId); - if (typeof subscriptionInfo === 'undefined') { - return Parse.Promise.as(['*']); - } - let foundUserId; - const subscriptionSessionToken = subscriptionInfo.sessionToken; - return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => { - foundUserId = userId; - if (userId) { - return Parse.Promise.as(['*', userId]); - } - return Parse.Promise.as(['*']); - }).then((aclGroup) => { - try { - return SchemaController.validatePermission(classLevelPermissions, object.className, aclGroup, op).then(() => { - return Parse.Promise.as(true); - }); - } catch(e) { - logger.verbose(`Failed matching CLP for ${object.id} ${foundUserId} ${e}`); - return Parse.Promise.as(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}); - }); - }) + 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); + if (typeof subscriptionInfo === 'undefined') { + return Promise.resolve(['*']); + } + const subscriptionSessionToken = subscriptionInfo.sessionToken; + const userId = await this.sessionTokenCache.getUserId(subscriptionSessionToken); + const aclGroup = ['*'] + if (userId) { + aclGroup.push(userId); + } + try { + await SchemaController.validatePermission(classLevelPermissions, object.className, aclGroup, op); + return Promise.resolve(true); + } catch(e) { + logger.verbose(`Failed matching CLP for ${object.id} ${userId} ${e}`); + return Promise.resolve(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) { diff --git a/src/rest.js b/src/rest.js index 100dddc652..b36a691f77 100644 --- a/src/rest.js +++ b/src/rest.js @@ -68,7 +68,7 @@ function del(config, auth, className, objectId) { if (hasTriggers || hasLiveQuery || className == '_Session') { return new RestQuery(config, auth, className, { objectId }) .forWrite() - .execute() + .execute({ op: 'delete' }) .then((response) => { if (response && response.results && response.results.length) { const firstResult = response.results[0]; From 055c768a530992e4c460beb94d6470cf1377c486 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 20:44:45 -0400 Subject: [PATCH 11/15] Use the Auth module for authentication and caches --- spec/ParseLiveQueryServer.spec.js | 158 ++++++++++++++------------ spec/SessionTokenCache.spec.js | 50 -------- src/LiveQuery/ParseLiveQueryServer.js | 126 ++++++++------------ src/LiveQuery/SessionTokenCache.js | 49 -------- 4 files changed, 137 insertions(+), 246 deletions(-) delete mode 100644 spec/SessionTokenCache.spec.js delete mode 100644 src/LiveQuery/SessionTokenCache.js diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 79eff33be6..85d258b1bd 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -2,6 +2,7 @@ const Parse = require('parse/node'); const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer; const ParseServer = require('../lib/ParseServer').default; const LiveQueryController = require('../lib/Controllers/LiveQueryController').LiveQueryController; +const auth = require('../lib/Auth'); // Global mock info const queryHashValue = 'hash'; @@ -58,25 +59,21 @@ describe('ParseLiveQueryServer', function() { } }; jasmine.mockLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub); - // Make mock SessionTokenCache - const mockSessionTokenCache = function(){ - this.getUserId = function(sessionToken){ - if (typeof sessionToken === 'undefined') { - return Promise.resolve(undefined); - } - if (sessionToken === null) { - return Promise.reject(); - } - return Promise.resolve(testUserId); - }; - }; - jasmine.mockLibrary('../lib/LiveQuery/SessionTokenCache', 'SessionTokenCache', mockSessionTokenCache); + spyOn(auth, 'getAuthForSessionToken').and.callFake(({ sessionToken, cacheController }) => { + if (typeof sessionToken === 'undefined') { + return Promise.reject(/*new auth.Auth({ user: undefined })*/); + } + if (sessionToken === null) { + return Promise.reject(); + } + return Promise.resolve(new auth.Auth({ cacheController, user: { id: testUserId }})); + }); done(); }); it('can be initialized', function() { const httpServer = {}; - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer); + const parseLiveQueryServer = new ParseLiveQueryServer(httpServer); expect(parseLiveQueryServer.clientId).toBeUndefined(); expect(parseLiveQueryServer.clients.size).toBe(0); @@ -224,7 +221,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle connect command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1 }; @@ -242,7 +239,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle subscribe command without clientId', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = { }; parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); @@ -252,7 +249,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle subscribe command with new query', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); @@ -296,7 +293,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle subscribe command with existing query', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add two mock clients const clientId = 1; addMockClient(parseLiveQueryServer, clientId); @@ -348,7 +345,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command without clientId', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = { }; parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {}); @@ -358,7 +355,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command without not existed client', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: 1 }; @@ -369,7 +366,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command without not existed query', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; addMockClient(parseLiveQueryServer, clientId); @@ -384,7 +381,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle unsubscribe command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); @@ -415,7 +412,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set connect command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket @@ -437,7 +434,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set subscribe command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket @@ -461,7 +458,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set unsubscribe command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket @@ -481,7 +478,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set update command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough(); spyOn(parseLiveQueryServer, '_handleUnsubscribe').and.callThrough(); @@ -514,7 +511,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set missing command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -530,7 +527,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set unknown command message handler for a parseWebSocket', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -546,7 +543,7 @@ describe('ParseLiveQueryServer', function() { }); it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); parseWebSocket.clientId = 1; @@ -564,7 +561,7 @@ describe('ParseLiveQueryServer', function() { } const spy = spyOn(cloudCodeHandler, 'handler').and.callThrough(); Parse.Cloud.onLiveQueryEvent(cloudCodeHandler.handler); - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); parseWebSocket.clientId = 1; @@ -583,7 +580,7 @@ describe('ParseLiveQueryServer', function() { // TODO: Test server can set disconnect command message handler for a parseWebSocket it('has no subscription and can handle object delete command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ @@ -599,7 +596,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object delete command which does not match any subscription', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ @@ -632,7 +629,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object delete command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ @@ -668,7 +665,7 @@ describe('ParseLiveQueryServer', function() { }); it('has no subscription and can handle object save command', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Make sure we do not crash in this case @@ -676,7 +673,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object save command which does not match any subscription', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Add mock client @@ -707,7 +704,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object enter command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client @@ -744,7 +741,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object update command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client @@ -777,7 +774,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object leave command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client @@ -814,7 +811,7 @@ describe('ParseLiveQueryServer', function() { }); it('can handle object create command which matches some subscriptions', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Add mock client @@ -847,7 +844,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match subscription for null or undefined parse object', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { match: jasmine.createSpy('match') @@ -860,7 +857,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match subscription', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { query: {} @@ -873,7 +870,7 @@ describe('ParseLiveQueryServer', function() { }); it('can inflate parse object', function() { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request const objectJSON = { "className":"testClassName", @@ -915,7 +912,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match undefined ACL', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const client = {}; const requestId = 0; @@ -926,7 +923,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with none exist requestId', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined) @@ -940,7 +937,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with public read access', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); const client = { @@ -957,7 +954,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with valid subscription sessionToken', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); const client = { @@ -974,7 +971,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with valid client sessionToken', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -993,7 +990,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with invalid subscription and client sessionToken', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1012,7 +1009,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with subscription sessionToken checking error', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return error when sessionToken is null, this is just @@ -1031,7 +1028,7 @@ describe('ParseLiveQueryServer', function() { }); it('can match ACL with client sessionToken checking error', function(done) { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return error when sessionToken is null @@ -1051,7 +1048,7 @@ describe('ParseLiveQueryServer', function() { it('won\'t match ACL that doesn\'t have public read or any roles', function(done){ - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { @@ -1070,7 +1067,7 @@ describe('ParseLiveQueryServer', function() { it('won\'t match non-public ACL with role when there is no user', function(done){ - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess("livequery", true); @@ -1083,16 +1080,16 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { expect(isMatched).toBe(false); done(); - }); + }).catch(done.fail); }); it('won\'t match ACL with role based read access set to false', function(done){ - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); - acl.setRoleReadAccess("liveQueryRead", false); + acl.setRoleReadAccess("otherLiveQueryRead", true); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'sessionToken' @@ -1101,17 +1098,27 @@ describe('ParseLiveQueryServer', function() { const requestId = 0; spyOn(Parse, "Query").and.callFake(function(){ + let shouldReturn = false; return { equalTo() { + shouldReturn = true; // Nothing to do here + return this; + }, + containedIn() { + shouldReturn = false; + return this; }, find() { + if (!shouldReturn) { + return Promise.resolve([]); + } //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role(); - liveQueryRole.set('name', 'liveQueryRead'); - return [ + const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); + liveQueryRole.id = 'abcdef1234'; + return Promise.resolve([ liveQueryRole - ]; + ]); } } }); @@ -1125,7 +1132,7 @@ describe('ParseLiveQueryServer', function() { it('will match ACL with role based read access set to true', function(done){ - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess("liveQueryRead", true); @@ -1137,17 +1144,27 @@ describe('ParseLiveQueryServer', function() { const requestId = 0; spyOn(Parse, "Query").and.callFake(function(){ + let shouldReturn = false; return { equalTo() { + shouldReturn = true; // Nothing to do here + return this; + }, + containedIn() { + shouldReturn = false; + return this; }, find() { + if (!shouldReturn) { + return Promise.resolve([]); + } //Return a role with the name "liveQueryRead" as that is what was set on the ACL - const liveQueryRole = new Parse.Role(); - liveQueryRole.set('name', 'liveQueryRead'); - return [ + const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); + liveQueryRole.id = 'abcdef1234'; + return Promise.resolve([ liveQueryRole - ]; + ]); } } }); @@ -1161,7 +1178,7 @@ describe('ParseLiveQueryServer', function() { describe('class level permissions', () => { it('matches CLP when find is closed', (done) => { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1182,7 +1199,7 @@ describe('ParseLiveQueryServer', function() { }); it('matches CLP when find is open', (done) => { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1203,7 +1220,7 @@ describe('ParseLiveQueryServer', function() { }); it('matches CLP when find is restricted to userIds', (done) => { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1224,7 +1241,7 @@ describe('ParseLiveQueryServer', function() { }); it('matches CLP when find is restricted to userIds', (done) => { - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined @@ -1338,7 +1355,7 @@ describe('ParseLiveQueryServer', function() { it('will match non-public ACL when client has master key', function(done){ - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { @@ -1357,7 +1374,7 @@ describe('ParseLiveQueryServer', function() { it('won\'t match non-public ACL when client has no master key', function(done){ - const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); + const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { @@ -1381,7 +1398,6 @@ describe('ParseLiveQueryServer', function() { jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'queryHash'); jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery'); jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub'); - jasmine.restoreLibrary('../lib/LiveQuery/SessionTokenCache', 'SessionTokenCache'); }); // Helper functions to add mock client and subscription to a liveQueryServer diff --git a/spec/SessionTokenCache.spec.js b/spec/SessionTokenCache.spec.js deleted file mode 100644 index 584a2c094d..0000000000 --- a/spec/SessionTokenCache.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -const SessionTokenCache = require('../lib/LiveQuery/SessionTokenCache').SessionTokenCache; - -describe('SessionTokenCache', function() { - - beforeEach(function(done) { - const Parse = require('parse/node'); - - spyOn(Parse, "Query").and.returnValue({ - first: jasmine.createSpy("first").and.returnValue(Promise.resolve(new Parse.Object("_Session", { - user: new Parse.User({id:"userId"}) - }))), - equalTo: function(){} - }) - - done(); - }); - - it('can get undefined userId', function(done) { - const sessionTokenCache = new SessionTokenCache(); - - sessionTokenCache.getUserId(undefined).then(() => { - }, (error) => { - expect(error).not.toBeNull(); - done(); - }); - }); - - it('can get existing userId', function(done) { - const sessionTokenCache = new SessionTokenCache(); - const sessionToken = 'sessionToken'; - const userId = 'userId' - sessionTokenCache.cache.set(sessionToken, userId); - - sessionTokenCache.getUserId(sessionToken).then((userIdFromCache) => { - expect(userIdFromCache).toBe(userId); - done(); - }); - }); - - it('can get new userId', function(done) { - const sessionTokenCache = new SessionTokenCache(); - - sessionTokenCache.getUserId('sessionToken').then((userIdFromCache) => { - expect(userIdFromCache).toBe('userId'); - expect(sessionTokenCache.cache.length).toBe(1); - done(); - }); - }); - -}); diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index f28bd40fac..f82477f918 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -7,11 +7,12 @@ import logger from '../logger'; import RequestSchema from './RequestSchema'; import { matchesQuery, queryHash } from './QueryTools'; import { ParsePubSub } from './ParsePubSub'; -import { SessionTokenCache } from './SessionTokenCache'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import uuid from 'uuid'; import { runLiveQueryEventHandlers } from '../triggers'; +import { getAuthForSessionToken } from '../Auth'; +import { getCacheController } from '../Controllers'; class ParseLiveQueryServer { clients: Map; @@ -22,12 +23,13 @@ class ParseLiveQueryServer { // The subscriber we use to get object update from publisher subscriber: Object; - constructor(server: any, config: any) { + constructor(server: any, config: any = {}) { this.server = server; this.clients = new Map(); this.subscriptions = new Map(); - config = config || {}; + config.appId = config.appId || Parse.applicationId; + config.masterKey = config.masterKey || Parse.masterKey; // Store keys, convert obj to map const keyPairs = config.keyPairs || {}; @@ -39,13 +41,11 @@ class ParseLiveQueryServer { // Initialize Parse Parse.Object.disableSingleInstance(); - const serverURL = config.serverURL || Parse.serverURL; Parse.serverURL = serverURL; - const appId = config.appId || Parse.applicationId; - const javascriptKey = Parse.javaScriptKey; - const masterKey = config.masterKey || Parse.masterKey; - Parse.initialize(appId, javascriptKey, masterKey); + Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey); + + this.cacheController = getCacheController(config) // Initialize websocket server this.parseWebSocketServer = new ParseWebSocketServer( @@ -78,9 +78,6 @@ class ParseLiveQueryServer { logger.error('Get message %s from unknown channel %j', message, channel); } }); - - // Initialize sessionToken cache - this.sessionTokenCache = new SessionTokenCache(config.cacheTimeout); } // Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes. @@ -336,6 +333,13 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } + async getUserId(sessionToken: ?string): ?string { + try { + const auth = await getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken }); + return auth && auth.user && auth.user.id; // return the ID of the found user + } catch(e) { /* ignore errors */ } + } + 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); @@ -343,8 +347,8 @@ class ParseLiveQueryServer { return Promise.resolve(['*']); } const subscriptionSessionToken = subscriptionInfo.sessionToken; - const userId = await this.sessionTokenCache.getUserId(subscriptionSessionToken); - const aclGroup = ['*'] + const aclGroup = ['*']; + const userId = await this.getUserId(subscriptionSessionToken); if (userId) { aclGroup.push(userId); } @@ -374,7 +378,7 @@ class ParseLiveQueryServer { && typeof query.objectId === 'string' ? 'get' : 'find'; } - _matchesACL(acl: any, client: any, requestId: number): any { + async _matchesACL(acl: any, client: any, requestId: number): any { // Return true directly if ACL isn't present, ACL is public read, or client has master key if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { return Promise.resolve(true); @@ -386,65 +390,33 @@ class ParseLiveQueryServer { } const subscriptionSessionToken = subscriptionInfo.sessionToken; - return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => { - return acl.getReadAccess(userId); - }).then((isSubscriptionSessionTokenMatched) => { - if (isSubscriptionSessionTokenMatched) { - return Promise.resolve(true); - } - - // Check if the user has any roles that match the ACL - return new Promise((resolve, reject) => { - - // Resolve false right away if the acl doesn't have any roles - const acl_has_roles = Object.keys(acl.permissionsById).some(key => key.startsWith("role:")); - if (!acl_has_roles) { - return resolve(false); - } - - this.sessionTokenCache.getUserId(subscriptionSessionToken) - .then((userId) => { - - // Pass along a null if there is no user id - if (!userId) { - return Promise.resolve(null); - } - - // Prepare a user object to query for roles - // To eliminate a query for the user, create one locally with the id - var user = new Parse.User(); - user.id = userId; - return user; - - }) - .then((user) => { + // TODO: get auth there and de-duplicate code below to work with the same Auth obj. + const userId = await this.getUserId(subscriptionSessionToken); + const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId); + if (isSubscriptionSessionTokenMatched) { + return Promise.resolve(true); + } - // Pass along an empty array (of roles) if no user - if (!user) { - return Promise.resolve([]); - } + // Check if the user has any roles that match the ACL + return Promise.resolve().then(async () => { - // 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); - }); + // 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; + } - }); - }).then((isRoleMatched) => { + const auth = await getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: subscriptionSessionToken }); + const roleNames = await auth.getUserRoles(); + // Finally, see if any of the user's roles allow them read access + for (const role of roleNames) { + // We use getReadAccess as `role` is in the form `role:roleName` + if (acl.getReadAccess(role)) { + return true; + } + } + return false; + }).then(async (isRoleMatched) => { if(isRoleMatched) { return Promise.resolve(true); @@ -452,13 +424,15 @@ class ParseLiveQueryServer { // Check client sessionToken matches ACL const clientSessionToken = client.sessionToken; - return this.sessionTokenCache.getUserId(clientSessionToken).then((userId) => { + if (clientSessionToken) { + const userId = await this.getUserId(clientSessionToken); return acl.getReadAccess(userId); - }); - }).then((isMatched) => { - return Promise.resolve(isMatched); - }, () => { - return Promise.resolve(false); + } else { + return isRoleMatched; + } + }).catch((error) => { + error; + return false; }); } diff --git a/src/LiveQuery/SessionTokenCache.js b/src/LiveQuery/SessionTokenCache.js deleted file mode 100644 index 249f557956..0000000000 --- a/src/LiveQuery/SessionTokenCache.js +++ /dev/null @@ -1,49 +0,0 @@ -import Parse from 'parse/node'; -import LRU from 'lru-cache'; -import logger from '../logger'; - -function userForSessionToken(sessionToken){ - var q = new Parse.Query("_Session"); - q.equalTo("sessionToken", sessionToken); - return q.first({useMasterKey:true}).then(function(session){ - if(!session){ - return Promise.reject("No session found for session token"); - } - return session.get("user"); - }); -} - -class SessionTokenCache { - cache: Object; - - constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) { - this.cache = new LRU({ - max: maxSize, - maxAge: timeout - }); - } - - getUserId(sessionToken: string): any { - if (!sessionToken) { - return Promise.reject('Empty sessionToken'); - } - const userId = this.cache.get(sessionToken); - if (userId) { - logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken); - return Promise.resolve(userId); - } - return userForSessionToken(sessionToken).then((user) => { - logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken); - const userId = user.id; - this.cache.set(sessionToken, userId); - return Promise.resolve(userId); - }, (error) => { - logger.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error); - return Promise.reject(error); - }); - } -} - -export { - SessionTokenCache -} From ca8bcd9a7b770adfee9c6c39fee71da3eb45692c Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 21:04:17 -0400 Subject: [PATCH 12/15] Cleaner implementation of getting auth --- src/LiveQuery/ParseLiveQueryServer.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index f82477f918..acc2404e8d 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -11,7 +11,7 @@ import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import uuid from 'uuid'; import { runLiveQueryEventHandlers } from '../triggers'; -import { getAuthForSessionToken } from '../Auth'; +import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; class ParseLiveQueryServer { @@ -333,11 +333,12 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } - async getUserId(sessionToken: ?string): ?string { + async getAuthForSessionToken(sessionToken: ?string): { auth: ?Auth, userId: ?string } { try { const auth = await getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken }); - return auth && auth.user && auth.user.id; // return the ID of the found user + return { auth, userId: auth && auth.user && auth.user.id }// return the ID of the found user } catch(e) { /* ignore errors */ } + return {}; } async _matchesCLP(classLevelPermissions: ?any, object: any, client: any, requestId: number, op: string): any { @@ -348,7 +349,7 @@ class ParseLiveQueryServer { } const subscriptionSessionToken = subscriptionInfo.sessionToken; const aclGroup = ['*']; - const userId = await this.getUserId(subscriptionSessionToken); + const { userId } = await this.getAuthForSessionToken(subscriptionSessionToken); if (userId) { aclGroup.push(userId); } @@ -391,7 +392,7 @@ class ParseLiveQueryServer { const subscriptionSessionToken = subscriptionInfo.sessionToken; // TODO: get auth there and de-duplicate code below to work with the same Auth obj. - const userId = await this.getUserId(subscriptionSessionToken); + const { auth, userId } = await this.getAuthForSessionToken(subscriptionSessionToken); const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId); if (isSubscriptionSessionTokenMatched) { return Promise.resolve(true); @@ -406,7 +407,6 @@ class ParseLiveQueryServer { return false; } - const auth = await getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: subscriptionSessionToken }); const roleNames = await auth.getUserRoles(); // Finally, see if any of the user's roles allow them read access for (const role of roleNames) { @@ -425,7 +425,7 @@ class ParseLiveQueryServer { // Check client sessionToken matches ACL const clientSessionToken = client.sessionToken; if (clientSessionToken) { - const userId = await this.getUserId(clientSessionToken); + const { userId } = await this.getAuthForSessionToken(clientSessionToken); return acl.getReadAccess(userId); } else { return isRoleMatched; From e3570594f11a48c9a081fc1764adc0a15ab2dc8a Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 21:47:06 -0400 Subject: [PATCH 13/15] Adds authCache that stores auth promises --- src/LiveQuery/ParseLiveQueryServer.js | 49 ++++++++++++++++++++------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index acc2404e8d..a60b1d5314 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -13,6 +13,7 @@ import uuid from 'uuid'; import { runLiveQueryEventHandlers } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; +import LRU from 'lru-cache'; class ParseLiveQueryServer { clients: Map; @@ -45,8 +46,17 @@ class ParseLiveQueryServer { Parse.serverURL = serverURL; Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey); + // The cache controller is a proper cache controller + // With access to User and Roles this.cacheController = getCacheController(config) + // This auth cache stores the promises for each auth resolution + // The main benefit is to be able to reuse the same user / session token resolution + // And to chain + this.authCache = new LRU({ + max: 500, // 500 concurrent + maxAge: 60 * 60 * 1000 // 1h + }); // Initialize websocket server this.parseWebSocketServer = new ParseWebSocketServer( server, @@ -333,25 +343,39 @@ class ParseLiveQueryServer { return matchesQuery(parseObject, subscription.query); } - async getAuthForSessionToken(sessionToken: ?string): { auth: ?Auth, userId: ?string } { + getAuthForSessionToken(sessionToken: ?string): Promise<{ auth: ?Auth, userId: ?string }> { + if (!sessionToken) { + return Promise.resolve({}); + } + const fromCache = this.authCache.get(sessionToken); + if (fromCache) { + return fromCache; + } try { - const auth = await getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken }); - return { auth, userId: auth && auth.user && auth.user.id }// return the ID of the found user + const authPromise = getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken }) + .then((auth) => { + return { auth, userId: auth && auth.user && auth.user.id }; + }, () => { + // If you can't continue, let's just wrap it up and delete it. + // Next time, one will try again + this.authCache.del(sessionToken); + }); + this.authCache.set(sessionToken, authPromise); + return authPromise; } catch(e) { /* ignore errors */ } - return {}; + return Promise.resolve({}); } 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); - if (typeof subscriptionInfo === 'undefined') { - return Promise.resolve(['*']); - } - const subscriptionSessionToken = subscriptionInfo.sessionToken; const aclGroup = ['*']; - const { userId } = await this.getAuthForSessionToken(subscriptionSessionToken); - if (userId) { - aclGroup.push(userId); + 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); @@ -390,9 +414,8 @@ class ParseLiveQueryServer { return Promise.resolve(false); } - const subscriptionSessionToken = subscriptionInfo.sessionToken; // TODO: get auth there and de-duplicate code below to work with the same Auth obj. - const { auth, userId } = await this.getAuthForSessionToken(subscriptionSessionToken); + const { auth, userId } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId); if (isSubscriptionSessionTokenMatched) { return Promise.resolve(true); From 08bba4fc2e02b459a8831fb38f1a7c3af08cd348 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 8 Aug 2018 22:28:28 -0400 Subject: [PATCH 14/15] Proper testing of the caching --- spec/ParseLiveQueryServer.spec.js | 25 ++++++++++++++++- src/LiveQuery/ParseLiveQueryServer.js | 39 +++++++++++++-------------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 85d258b1bd..9c299a2877 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -61,11 +61,14 @@ describe('ParseLiveQueryServer', function() { jasmine.mockLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub); spyOn(auth, 'getAuthForSessionToken').and.callFake(({ sessionToken, cacheController }) => { if (typeof sessionToken === 'undefined') { - return Promise.reject(/*new auth.Auth({ user: undefined })*/); + return Promise.reject(); } if (sessionToken === null) { return Promise.reject(); } + if (sessionToken === 'pleaseThrow') { + return Promise.reject(); + } return Promise.resolve(new auth.Auth({ cacheController, user: { id: testUserId }})); }); done(); @@ -1388,7 +1391,27 @@ describe('ParseLiveQueryServer', function() { expect(isMatched).toBe(false); done(); }); + }); + 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(){ diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index a60b1d5314..b19a2ca7b6 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -351,19 +351,17 @@ class ParseLiveQueryServer { if (fromCache) { return fromCache; } - try { - const authPromise = getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken }) - .then((auth) => { - return { auth, userId: auth && auth.user && auth.user.id }; - }, () => { - // If you can't continue, let's just wrap it up and delete it. - // Next time, one will try again - this.authCache.del(sessionToken); - }); - this.authCache.set(sessionToken, authPromise); - return authPromise; - } catch(e) { /* ignore errors */ } - return Promise.resolve({}); + const authPromise = getAuthForSessionToken({ cacheController: this.cacheController, sessionToken: sessionToken }) + .then((auth) => { + return { auth, userId: auth && auth.user && auth.user.id }; + }, () => { + // 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 { @@ -379,10 +377,10 @@ class ParseLiveQueryServer { } try { await SchemaController.validatePermission(classLevelPermissions, object.className, aclGroup, op); - return Promise.resolve(true); + return true; } catch(e) { logger.verbose(`Failed matching CLP for ${object.id} ${userId} ${e}`); - return Promise.resolve(false); + return false; } // TODO: handle roles permissions // Object.keys(classLevelPermissions).forEach((key) => { @@ -403,22 +401,22 @@ class ParseLiveQueryServer { && typeof query.objectId === 'string' ? 'get' : 'find'; } - async _matchesACL(acl: any, client: any, requestId: number): any { + async _matchesACL(acl: any, client: any, requestId: number): Promise { // Return true directly if ACL isn't present, ACL is public read, or client has master key if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) { - return Promise.resolve(true); + return true; } // Check subscription sessionToken matches ACL first const subscriptionInfo = client.getSubscriptionInfo(requestId); if (typeof subscriptionInfo === 'undefined') { - return Promise.resolve(false); + return false; } // TODO: get auth there and de-duplicate code below to work with the same Auth obj. const { auth, userId } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId); if (isSubscriptionSessionTokenMatched) { - return Promise.resolve(true); + return true; } // Check if the user has any roles that match the ACL @@ -453,8 +451,7 @@ class ParseLiveQueryServer { } else { return isRoleMatched; } - }).catch((error) => { - error; + }).catch(() => { return false; }); } From bd1cd5bb3760eccfb5b8226f74d8769f3b9da34f Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Thu, 9 Aug 2018 09:02:11 -0400 Subject: [PATCH 15/15] nits --- src/Controllers/DatabaseController.js | 4 ++-- src/Controllers/SchemaController.js | 10 +++++----- src/LiveQuery/ParseLiveQueryServer.js | 9 ++++----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 372be94f81..9924c5c113 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1007,10 +1007,10 @@ class DatabaseController { }) } - addPointerPermissions(schema: any, className: string, operation: string, query: any, aclGroup: any[] = []) { + addPointerPermissions(schema: SchemaController.SchemaController, className: string, operation: string, query: any, aclGroup: any[] = []) { // Check if class has public permission for operation // If the BaseCLP pass, let go through - if (schema.testBaseCLP(className, aclGroup, operation)) { + if (schema.testPermissionsForClassName(className, aclGroup, operation)) { return query; } const perms = schema.perms[className]; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 71d7066d4d..856a6980be 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -859,12 +859,12 @@ export default class SchemaController { return Promise.resolve(this); } - // Validates the base CLP for an operation - testBaseCLP(className: string, aclGroup: string[], operation: string) { - return SchemaController.testBaseCLP(this.perms[className], aclGroup, operation); + testPermissionsForClassName(className: string, aclGroup: string[], operation: string) { + return SchemaController.testPermissions(this.perms[className], aclGroup, operation); } - static testBaseCLP(classPermissions: ?any, aclGroup: string[], operation: string) { + // 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; } @@ -881,7 +881,7 @@ export default class SchemaController { // Validates an operation passes class-level-permissions set in the schema static validatePermission(classPermissions: ?any, className: string, aclGroup: string[], operation: string) { - if (SchemaController.testBaseCLP(classPermissions, aclGroup, operation)) { + if (SchemaController.testPermissions(classPermissions, aclGroup, operation)) { return Promise.resolve(); } diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index b19a2ca7b6..875d334124 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -47,12 +47,11 @@ class ParseLiveQueryServer { Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey); // The cache controller is a proper cache controller - // With access to User and Roles + // 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 - // And to chain + // 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 @@ -354,7 +353,7 @@ class ParseLiveQueryServer { 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);