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:
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
213
src/Auth.js
213
src/Auth.js
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user