Auth module refactoring in order to be reusable (#4940)

* Auth module refactoring in order to be reusable

* Ensure cache controller is properly forwarded from helpers

* Nits
This commit is contained in:
Florent Vilmart
2018-08-09 13:02:06 -04:00
committed by GitHub
parent 5d91c1057f
commit 2ae603574c
4 changed files with 169 additions and 94 deletions

View File

@@ -1,6 +1,6 @@
describe('Auth', () => { describe('Auth', () => {
const Auth = require('../lib/Auth.js').Auth; const { Auth, getAuthForSessionToken } = require('../lib/Auth.js');
const Config = require('../lib/Config');
describe('getUserRoles', () => { describe('getUserRoles', () => {
let auth; let auth;
let config; 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);
});
}); });

View File

@@ -832,10 +832,7 @@ describe('Cloud Code', () => {
expect(body.result).toEqual('second data'); expect(body.result).toEqual('second data');
done(); done();
}) })
.catch(error => { .catch(done.fail);
fail(JSON.stringify(error));
done();
});
}); });
it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => { it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => {

View File

@@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
}); });
it("should recursively load roles", (done) => { function testLoadRoles(config, done) {
const rolesNames = ["FooRole", "BarRole", "BazRole"]; const rolesNames = ["FooRole", "BarRole", "BazRole"];
const roleIds = {}; const roleIds = {};
createTestUser().then((user) => { createTestUser().then((user) => {
@@ -159,7 +159,7 @@ describe('Parse Role testing', () => {
return createRole(rolesNames[2], anotherRole, null); return createRole(rolesNames[2], anotherRole, null);
}).then((lastRole) => { }).then((lastRole) => {
roleIds[lastRole.get("name")] = lastRole.id; 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(); return auth._loadRoles();
}) })
}).then((roles) => { }).then((roles) => {
@@ -172,6 +172,14 @@ describe('Parse Role testing', () => {
fail("should succeed") fail("should succeed")
done(); 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) => { it("_Role object should not save without name.", (done) => {

View File

@@ -5,8 +5,9 @@ const Parse = require('parse/node');
// An Auth object tells you who is requesting something and whether // An Auth object tells you who is requesting something and whether
// the master key was used. // the master key was used.
// userObject is a Parse.User and can be null if there's no user. // 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.config = config;
this.cacheController = cacheController || (config && config.cacheController);
this.installationId = installationId; this.installationId = installationId;
this.isMaster = isMaster; this.isMaster = isMaster;
this.user = user; this.user = user;
@@ -48,47 +49,58 @@ function nobody(config) {
// Returns a promise that resolves to an Auth object // Returns a promise that resolves to an Auth object
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) { const getAuthForSessionToken = async function({ config, cacheController, sessionToken, installationId }) {
return config.cacheController.user.get(sessionToken).then((userJSON) => { cacheController = cacheController || (config && config.cacheController);
if (cacheController) {
const userJSON = await cacheController.user.get(sessionToken);
if (userJSON) { if (userJSON) {
const cachedUser = Parse.Object.fromJSON(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}));
} }
}
var restOptions = { let results;
if (config) {
const restOptions = {
limit: 1, limit: 1,
include: 'user' include: 'user'
}; };
var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions); const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
return query.execute().then((response) => { results = (await query.execute()).results;
var results = response.results; } else {
if (results.length !== 1 || !results[0]['user']) { results = (await new Parse.Query(Parse.Session)
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); .limit(1)
} .include('user')
.equalTo('sessionToken', sessionToken)
.find({ useMasterKey: true })).map((obj) => obj.toJSON())
}
var now = new Date(), if (results.length !== 1 || !results[0]['user']) {
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
if (expiresAt < now) { }
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, const now = new Date(),
'Session token is expired.'); expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
} if (expiresAt < now) {
var obj = results[0]['user']; throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
delete obj.password; 'Session token is expired.');
obj['className'] = '_User'; }
obj['sessionToken'] = sessionToken; const obj = results[0]['user'];
config.cacheController.user.put(sessionToken, obj); delete obj.password;
const userObject = Parse.Object.fromJSON(obj); obj['className'] = '_User';
return new Auth({config, isMaster: false, installationId, user: userObject}); obj['sessionToken'] = sessionToken;
}); if (cacheController) {
}); cacheController.user.put(sessionToken, obj);
}
const userObject = Parse.Object.fromJSON(obj);
return new Auth({ config, cacheController, isMaster: false, installationId, user: userObject });
}; };
var getAuthForLegacySessionToken = function({config, sessionToken, installationId } = {}) { var getAuthForLegacySessionToken = function({ config, sessionToken, installationId }) {
var restOptions = { var restOptions = {
limit: 1 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) => { return query.execute().then((response) => {
var results = response.results; var results = response.results;
if (results.length !== 1) { if (results.length !== 1) {
@@ -97,7 +109,7 @@ var getAuthForLegacySessionToken = function({config, sessionToken, installationI
const obj = results[0]; const obj = results[0];
obj.className = '_User'; obj.className = '_User';
const userObject = Parse.Object.fromJSON(obj); const userObject = Parse.Object.fromJSON(obj);
return new Auth({config, isMaster: false, installationId, user: userObject}); return new Auth({ config, isMaster: false, installationId, user: userObject });
}); });
} }
@@ -116,84 +128,113 @@ Auth.prototype.getUserRoles = function() {
return this.rolePromise; return this.rolePromise;
}; };
// Iterates through the role tree and compiles a users roles Auth.prototype.getRolesForUser = function() {
Auth.prototype._loadRoles = function() { if (this.config) {
var cacheAdapter = this.config.cacheController; const restWhere = {
return cacheAdapter.role.get(this.user.id).then((cachedRoles) => {
if (cachedRoles != null) {
this.fetchedRoles = true;
this.userRoles = cachedRoles;
return Promise.resolve(cachedRoles);
}
var restWhere = {
'users': { 'users': {
__type: 'Pointer', __type: 'Pointer',
className: '_User', className: '_User',
objectId: this.user.id objectId: this.user.id
} }
}; };
// First get the role ids this user is directly a member of const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); return query.execute().then(({ results }) => results);
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 new Parse.Query(Parse.Role)
return Promise.resolve(this.userRoles); .equalTo('users', this.user)
} .find({ useMasterKey: true })
var rolesMap = results.reduce((m, r) => { .then((results) => results.map((obj) => obj.toJSON()));
m.names.push(r.name); }
m.ids.push(r.objectId);
return m;
}, {ids: [], names: []});
// run the recursive finding // Iterates through the role tree and compiles a user's roles
return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) Auth.prototype._loadRoles = async function() {
.then((roleNames) => { if (this.cacheController) {
this.userRoles = roleNames.map((r) => { const cachedRoles = await this.cacheController.role.get(this.user.id);
return 'role:' + r; if (cachedRoles != null) {
}); this.fetchedRoles = true;
this.fetchedRoles = true; this.userRoles = cachedRoles;
this.rolePromise = null; return cachedRoles;
cacheAdapter.role.put(this.user.id, Array(...this.userRoles)); }
return Promise.resolve(this.userRoles); }
});
}); // 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;
}; };
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: 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 // Given a list of roleIds, find all the parent roles, returns a promise with all names
Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) { Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) {
const ins = roleIDs.filter((roleID) => { const ins = roleIDs.filter((roleID) => {
return queriedRoles[roleID] !== true; const wasQueried = queriedRoles[roleID] !== true;
}).map((roleID) => {
// mark as queried
queriedRoles[roleID] = true; queriedRoles[roleID] = true;
return { return wasQueried;
__type: 'Pointer',
className: '_Role',
objectId: roleID
}
}); });
// all roles are accounted for, return the names // all roles are accounted for, return the names
if (ins.length == 0) { if (ins.length == 0) {
return Promise.resolve([...new Set(names)]); return Promise.resolve([...new Set(names)]);
} }
// Build an OR query across all parentRoles
let restWhere; return this.getRolesByIds(ins).then((results) => {
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;
// Nothing found // Nothing found
if (!results.length) { if (!results.length) {
return Promise.resolve(names); return Promise.resolve(names);