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 1/3] 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 2/3] 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 3/3] 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);