From 3eb8dd55f64ca966fac1fb22aaaa62d590d4e199 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 13 Aug 2015 08:58:41 -0700 Subject: [PATCH] Add util methods to ACL and clean up related model resolutions --- common/models/acl.js | 72 ++++++++++++++++++ common/models/role-mapping.js | 24 +++--- common/models/role.js | 54 ++++++++----- common/models/scope.js | 17 +++-- test/role.test.js | 139 ++++++++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 35 deletions(-) diff --git a/common/models/acl.js b/common/models/acl.js index 0bc48c7d..2a7306b1 100644 --- a/common/models/acl.js +++ b/common/models/acl.js @@ -496,4 +496,76 @@ module.exports = function(ACL) { if (callback) callback(null, access.permission !== ACL.DENY); }); }; + + ACL.resolveRelatedModels = function() { + if (!this.roleModel) { + var reg = this.registry; + this.roleModel = reg.getModelByType(loopback.Role); + this.roleMappingModel = reg.getModelByType(loopback.RoleMapping); + this.userModel = reg.getModelByType(loopback.User); + this.applicationModel = reg.getModelByType(loopback.Application); + } + }; + + /** + * Resolve a principal by type/id + * @param {String} type Principal type - ROLE/APP/USER + * @param {String|Number} id Principal id or name + * @param {Function} cb Callback function + */ + ACL.resolvePrincipal = function(type, id, cb) { + type = type || ACL.ROLE; + this.resolveRelatedModels(); + switch (type) { + case ACL.ROLE: + this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb); + break; + case ACL.USER: + this.userModel.findOne( + {where: {or: [{username: id}, {email: id}, {id: id}]}}, cb); + break; + case ACL.APP: + this.applicationModel.findOne( + {where: {or: [{name: id}, {email: id}, {id: id}]}}, cb); + break; + default: + process.nextTick(function() { + var err = new Error('Invalid principal type: ' + type); + err.statusCode = 400; + cb(err); + }); + } + }; + + /** + * Check if the given principal is mapped to the role + * @param {String} principalType Principal type + * @param {String|*} principalId Principal id/name + * @param {String|*} role Role id/name + * @param {Function} cb Callback function + */ + ACL.isMappedToRole = function(principalType, principalId, role, cb) { + var self = this; + this.resolvePrincipal(principalType, principalId, + function(err, principal) { + if (err) return cb(err); + if (principal != null) { + principalId = principal.id; + } + principalType = principalType || 'ROLE'; + self.resolvePrincipal('ROLE', role, function(err, role) { + if (err || !role) return cb(err, role); + self.roleMappingModel.findOne({ + where: { + roleId: role.id, + principalType: principalType, + principalId: String(principalId) + } + }, function(err, result) { + if (err) return cb(err); + return cb(null, !!result); + }); + }); + }); + }; }; diff --git a/common/models/role-mapping.js b/common/models/role-mapping.js index 059193ae..ee728483 100644 --- a/common/models/role-mapping.js +++ b/common/models/role-mapping.js @@ -17,6 +17,15 @@ module.exports = function(RoleMapping) { RoleMapping.APP = RoleMapping.APPLICATION = 'APP'; RoleMapping.ROLE = 'ROLE'; + RoleMapping.resolveRelatedModels = function() { + if (!this.userModel) { + var reg = this.registry; + this.roleModel = reg.getModelByType(loopback.Role); + this.userModel = reg.getModelByType(loopback.User); + this.applicationModel = reg.getModelByType(loopback.Application); + } + }; + /** * Get the application principal * @callback {Function} callback @@ -24,11 +33,10 @@ module.exports = function(RoleMapping) { * @param {Application} application */ RoleMapping.prototype.application = function(callback) { - var registry = this.constructor.registry; + this.constructor.resolveRelatedModels(); if (this.principalType === RoleMapping.APPLICATION) { - var applicationModel = this.constructor.Application || - registry.getModelByType('Application'); + var applicationModel = this.constructor.applicationModel; applicationModel.findById(this.principalId, callback); } else { process.nextTick(function() { @@ -44,10 +52,9 @@ module.exports = function(RoleMapping) { * @param {User} user */ RoleMapping.prototype.user = function(callback) { - var RoleMapping = this.constructor; + this.constructor.resolveRelatedModels(); if (this.principalType === RoleMapping.USER) { - var userModel = RoleMapping.User || - RoleMapping.registry.getModelByType('User'); + var userModel = this.constructor.userModel; userModel.findById(this.principalId, callback); } else { process.nextTick(function() { @@ -63,11 +70,10 @@ module.exports = function(RoleMapping) { * @param {User} childUser */ RoleMapping.prototype.childRole = function(callback) { - var registry = this.constructor.registry; + this.constructor.resolveRelatedModels(); if (this.principalType === RoleMapping.ROLE) { - var roleModel = this.constructor.Role || - registry.getModelByType(loopback.Role); + var roleModel = this.constructor.roleModel; roleModel.findById(this.principalId, callback); } else { process.nextTick(function() { diff --git a/common/models/role.js b/common/models/role.js index 927f5c90..a176fec1 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -6,9 +6,6 @@ var async = require('async'); var AccessContext = require('../../lib/access-context').AccessContext; var RoleMapping = loopback.RoleMapping; -var Role = loopback.Role; -var User = loopback.User; -var Application = loopback.Application; assert(RoleMapping, 'RoleMapping model must be defined before Role model'); @@ -31,19 +28,19 @@ module.exports = function(Role) { return new Date(); }; + Role.resolveRelatedModels = function() { + if (!this.userModel) { + var reg = this.registry; + this.roleMappingModel = reg.getModelByType(loopback.RoleMapping); + this.userModel = reg.getModelByType(loopback.User); + this.applicationModel = reg.getModelByType(loopback.Application); + } + }; + // Set up the connection to users/applications/roles once the model - Role.once('dataSourceAttached', function() { - var registry = Role.registry; - var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping); - var principalTypesToModels = {}; + Role.once('dataSourceAttached', function(roleModel) { - principalTypesToModels[RoleMapping.USER] = User; - principalTypesToModels[RoleMapping.APPLICATION] = Application; - principalTypesToModels[RoleMapping.ROLE] = Role; - - Object.keys(principalTypesToModels).forEach(function(principalType) { - var model = principalTypesToModels[principalType]; - var pluralName = model.pluralModelName.toLowerCase(); + ['users', 'applications', 'roles'].forEach(function(rel) { /** * Fetch all users assigned to this role * @function Role.prototype#users @@ -62,8 +59,23 @@ module.exports = function(Role) { * @param {object} [query] query object passed to model find call * @param {Function} [callback] */ - Role.prototype[pluralName] = function(query, callback) { - listByPrincipalType(model, principalType, query, callback); + Role.prototype[rel] = function(query, callback) { + roleModel.resolveRelatedModels(); + var relsToModels = { + users: roleModel.userModel, + applications: roleModel.applicationModel, + roles: roleModel + }; + + var ACL = loopback.ACL; + var relsToTypes = { + users: ACL.USER, + applications: ACL.APP, + roles: ACL.ROLE + }; + + var model = relsToModels[rel]; + listByPrincipalType(model, relsToTypes[rel], query, callback); }; }); @@ -81,7 +93,7 @@ module.exports = function(Role) { query = {}; } - roleMappingModel.find({ + roleModel.roleMappingModel.find({ where: {roleId: this.id, principalType: principalType} }, function(err, mappings) { var ids; @@ -272,7 +284,7 @@ module.exports = function(Role) { context = new AccessContext(context); } - var registry = this.registry; + this.resolveRelatedModels(); debug('isInRole(): %s', role); context.debug(); @@ -309,7 +321,7 @@ module.exports = function(Role) { return; } - var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping); + var roleMappingModel = this.roleMappingModel; this.findOne({where: {name: role}}, function(err, result) { if (err) { if (callback) callback(err); @@ -364,7 +376,7 @@ module.exports = function(Role) { context = new AccessContext(context); } var roles = []; - var registry = this.registry; + this.resolveRelatedModels(); var addRole = function(role) { if (role && roles.indexOf(role) === -1) { @@ -391,7 +403,7 @@ module.exports = function(Role) { }); }); - var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping); + var roleMappingModel = this.roleMappingModel; context.principals.forEach(function(p) { // Check against the role mappings var principalType = p.type || undefined; diff --git a/common/models/scope.js b/common/models/scope.js index 362c9958..3c713b53 100644 --- a/common/models/scope.js +++ b/common/models/scope.js @@ -13,6 +13,13 @@ var loopback = require('../../lib/loopback'); */ module.exports = function(Scope) { + Scope.resolveRelatedModels = function() { + if (!this.aclModel) { + var reg = this.registry; + this.aclModel = reg.getModelByType(loopback.ACL); + } + }; + /** * Check if the given scope is allowed to access the model/property * @param {String} scope The scope name @@ -24,17 +31,17 @@ module.exports = function(Scope) { * @param {AccessRequest} result The access permission */ Scope.checkPermission = function(scope, model, property, accessType, callback) { - var ACL = loopback.ACL; - var registry = this.registry; - assert(ACL, + this.resolveRelatedModels(); + var aclModel = this.aclModel; + assert(aclModel, 'ACL model must be defined before Scope.checkPermission is called'); this.findOne({where: {name: scope}}, function(err, scope) { if (err) { if (callback) callback(err); } else { - var aclModel = registry.getModelByType(ACL); - aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); + aclModel.checkPermission( + aclModel.SCOPE, scope.id, model, property, accessType, callback); } }); }; diff --git a/test/role.test.js b/test/role.test.js index b1aad27e..3053a0af 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -6,6 +6,8 @@ var RoleMapping = loopback.RoleMapping; var User = loopback.User; var Application = loopback.Application; var ACL = loopback.ACL; +var async = require('async'); +var expect = require('chai').expect; function checkResult(err, result) { // console.log(err, result); @@ -19,9 +21,18 @@ describe('role model', function() { ds = loopback.createDataSource({connector: 'memory'}); // Re-attach the models so that they can have isolated store to avoid // pollutions from other tests + ACL.attachTo(ds); User.attachTo(ds); Role.attachTo(ds); RoleMapping.attachTo(ds); + Application.attachTo(ds); + ACL.roleModel = Role; + ACL.roleMappingModel = RoleMapping; + ACL.userModel = User; + ACL.applicationModel = Application; + Role.roleMappingModel = RoleMapping; + Role.userModel = User; + Role.applicationModel = Application; }); it('should define role/role relations', function() { @@ -205,6 +216,134 @@ describe('role model', function() { }); }); }); + }); + + describe('isMappedToRole', function() { + var user, app, role; + + beforeEach(function(done) { + User.create({ + username: 'john', + email: 'john@gmail.com', + password: 'jpass' + }, function(err, u) { + if (err) return done(err); + user = u; + User.create({ + username: 'mary', + email: 'mary@gmail.com', + password: 'mpass' + }, function(err, u) { + if (err) return done(err); + Application.create({ + name: 'demo' + }, function(err, a) { + if (err) return done(err); + app = a; + Role.create({ + name: 'admin' + }, function(err, r) { + if (err) return done(err); + role = r; + var principals = [ + { + principalType: ACL.USER, + principalId: user.id + }, + { + principalType: ACL.APP, + principalId: app.id + } + ]; + async.each(principals, function(p, done) { + role.principals.create(p, done); + }, done); + }); + }); + }); + }); + }); + + it('should resolve user by id', function(done) { + ACL.resolvePrincipal(ACL.USER, user.id, function(err, u) { + if (err) return done(err); + expect(u.id).to.eql(user.id); + done(); + }); + }); + + it('should resolve user by username', function(done) { + ACL.resolvePrincipal(ACL.USER, user.username, function(err, u) { + if (err) return done(err); + expect(u.username).to.eql(user.username); + done(); + }); + }); + + it('should resolve user by email', function(done) { + ACL.resolvePrincipal(ACL.USER, user.email, function(err, u) { + if (err) return done(err); + expect(u.email).to.eql(user.email); + done(); + }); + }); + + it('should resolve app by id', function(done) { + ACL.resolvePrincipal(ACL.APP, app.id, function(err, a) { + if (err) return done(err); + expect(a.id).to.eql(app.id); + done(); + }); + }); + + it('should resolve app by name', function(done) { + ACL.resolvePrincipal(ACL.APP, app.name, function(err, a) { + if (err) return done(err); + expect(a.name).to.eql(app.name); + done(); + }); + }); + + it('should report isMappedToRole by user.username', function(done) { + ACL.isMappedToRole(ACL.USER, user.username, 'admin', function(err, flag) { + if (err) return done(err); + expect(flag).to.eql(true); + done(); + }); + }); + + it('should report isMappedToRole by user.email', function(done) { + ACL.isMappedToRole(ACL.USER, user.email, 'admin', function(err, flag) { + if (err) return done(err); + expect(flag).to.eql(true); + done(); + }); + }); + + it('should report isMappedToRole by user.username for mismatch', + function(done) { + ACL.isMappedToRole(ACL.USER, 'mary', 'admin', function(err, flag) { + if (err) return done(err); + expect(flag).to.eql(false); + done(); + }); + }); + + it('should report isMappedToRole by app.name', function(done) { + ACL.isMappedToRole(ACL.APP, app.name, 'admin', function(err, flag) { + if (err) return done(err); + expect(flag).to.eql(true); + done(); + }); + }); + + it('should report isMappedToRole by app.name', function(done) { + ACL.isMappedToRole(ACL.APP, app.name, 'admin', function(err, flag) { + if (err) return done(err); + expect(flag).to.eql(true); + done(); + }); + }); });