Merge pull request #1584 from strongloop/feature/add-more-acl-utils

Enhance the ACL related models
This commit is contained in:
Raymond Feng 2015-08-13 09:00:32 -07:00
commit 06cece038e
5 changed files with 271 additions and 35 deletions

View File

@ -496,4 +496,76 @@ module.exports = function(ACL) {
if (callback) callback(null, access.permission !== ACL.DENY); 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);
});
});
});
};
}; };

View File

@ -17,6 +17,15 @@ module.exports = function(RoleMapping) {
RoleMapping.APP = RoleMapping.APPLICATION = 'APP'; RoleMapping.APP = RoleMapping.APPLICATION = 'APP';
RoleMapping.ROLE = 'ROLE'; 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 * Get the application principal
* @callback {Function} callback * @callback {Function} callback
@ -24,11 +33,10 @@ module.exports = function(RoleMapping) {
* @param {Application} application * @param {Application} application
*/ */
RoleMapping.prototype.application = function(callback) { RoleMapping.prototype.application = function(callback) {
var registry = this.constructor.registry; this.constructor.resolveRelatedModels();
if (this.principalType === RoleMapping.APPLICATION) { if (this.principalType === RoleMapping.APPLICATION) {
var applicationModel = this.constructor.Application || var applicationModel = this.constructor.applicationModel;
registry.getModelByType('Application');
applicationModel.findById(this.principalId, callback); applicationModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function() { process.nextTick(function() {
@ -44,10 +52,9 @@ module.exports = function(RoleMapping) {
* @param {User} user * @param {User} user
*/ */
RoleMapping.prototype.user = function(callback) { RoleMapping.prototype.user = function(callback) {
var RoleMapping = this.constructor; this.constructor.resolveRelatedModels();
if (this.principalType === RoleMapping.USER) { if (this.principalType === RoleMapping.USER) {
var userModel = RoleMapping.User || var userModel = this.constructor.userModel;
RoleMapping.registry.getModelByType('User');
userModel.findById(this.principalId, callback); userModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function() { process.nextTick(function() {
@ -63,11 +70,10 @@ module.exports = function(RoleMapping) {
* @param {User} childUser * @param {User} childUser
*/ */
RoleMapping.prototype.childRole = function(callback) { RoleMapping.prototype.childRole = function(callback) {
var registry = this.constructor.registry; this.constructor.resolveRelatedModels();
if (this.principalType === RoleMapping.ROLE) { if (this.principalType === RoleMapping.ROLE) {
var roleModel = this.constructor.Role || var roleModel = this.constructor.roleModel;
registry.getModelByType(loopback.Role);
roleModel.findById(this.principalId, callback); roleModel.findById(this.principalId, callback);
} else { } else {
process.nextTick(function() { process.nextTick(function() {

View File

@ -6,9 +6,6 @@ var async = require('async');
var AccessContext = require('../../lib/access-context').AccessContext; var AccessContext = require('../../lib/access-context').AccessContext;
var RoleMapping = loopback.RoleMapping; 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'); assert(RoleMapping, 'RoleMapping model must be defined before Role model');
@ -31,19 +28,19 @@ module.exports = function(Role) {
return new Date(); 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 // Set up the connection to users/applications/roles once the model
Role.once('dataSourceAttached', function() { Role.once('dataSourceAttached', function(roleModel) {
var registry = Role.registry;
var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping);
var principalTypesToModels = {};
principalTypesToModels[RoleMapping.USER] = User; ['users', 'applications', 'roles'].forEach(function(rel) {
principalTypesToModels[RoleMapping.APPLICATION] = Application;
principalTypesToModels[RoleMapping.ROLE] = Role;
Object.keys(principalTypesToModels).forEach(function(principalType) {
var model = principalTypesToModels[principalType];
var pluralName = model.pluralModelName.toLowerCase();
/** /**
* Fetch all users assigned to this role * Fetch all users assigned to this role
* @function Role.prototype#users * @function Role.prototype#users
@ -62,8 +59,23 @@ module.exports = function(Role) {
* @param {object} [query] query object passed to model find call * @param {object} [query] query object passed to model find call
* @param {Function} [callback] * @param {Function} [callback]
*/ */
Role.prototype[pluralName] = function(query, callback) { Role.prototype[rel] = function(query, callback) {
listByPrincipalType(model, principalType, 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 = {}; query = {};
} }
roleMappingModel.find({ roleModel.roleMappingModel.find({
where: {roleId: this.id, principalType: principalType} where: {roleId: this.id, principalType: principalType}
}, function(err, mappings) { }, function(err, mappings) {
var ids; var ids;
@ -272,7 +284,7 @@ module.exports = function(Role) {
context = new AccessContext(context); context = new AccessContext(context);
} }
var registry = this.registry; this.resolveRelatedModels();
debug('isInRole(): %s', role); debug('isInRole(): %s', role);
context.debug(); context.debug();
@ -309,7 +321,7 @@ module.exports = function(Role) {
return; return;
} }
var roleMappingModel = this.RoleMapping || registry.getModelByType(RoleMapping); var roleMappingModel = this.roleMappingModel;
this.findOne({where: {name: role}}, function(err, result) { this.findOne({where: {name: role}}, function(err, result) {
if (err) { if (err) {
if (callback) callback(err); if (callback) callback(err);
@ -364,7 +376,7 @@ module.exports = function(Role) {
context = new AccessContext(context); context = new AccessContext(context);
} }
var roles = []; var roles = [];
var registry = this.registry; this.resolveRelatedModels();
var addRole = function(role) { var addRole = function(role) {
if (role && roles.indexOf(role) === -1) { 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) { context.principals.forEach(function(p) {
// Check against the role mappings // Check against the role mappings
var principalType = p.type || undefined; var principalType = p.type || undefined;

View File

@ -13,6 +13,13 @@ var loopback = require('../../lib/loopback');
*/ */
module.exports = function(Scope) { 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 * Check if the given scope is allowed to access the model/property
* @param {String} scope The scope name * @param {String} scope The scope name
@ -24,17 +31,17 @@ module.exports = function(Scope) {
* @param {AccessRequest} result The access permission * @param {AccessRequest} result The access permission
*/ */
Scope.checkPermission = function(scope, model, property, accessType, callback) { Scope.checkPermission = function(scope, model, property, accessType, callback) {
var ACL = loopback.ACL; this.resolveRelatedModels();
var registry = this.registry; var aclModel = this.aclModel;
assert(ACL, assert(aclModel,
'ACL model must be defined before Scope.checkPermission is called'); 'ACL model must be defined before Scope.checkPermission is called');
this.findOne({where: {name: scope}}, function(err, scope) { this.findOne({where: {name: scope}}, function(err, scope) {
if (err) { if (err) {
if (callback) callback(err); if (callback) callback(err);
} else { } else {
var aclModel = registry.getModelByType(ACL); aclModel.checkPermission(
aclModel.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); aclModel.SCOPE, scope.id, model, property, accessType, callback);
} }
}); });
}; };

View File

@ -6,6 +6,8 @@ var RoleMapping = loopback.RoleMapping;
var User = loopback.User; var User = loopback.User;
var Application = loopback.Application; var Application = loopback.Application;
var ACL = loopback.ACL; var ACL = loopback.ACL;
var async = require('async');
var expect = require('chai').expect;
function checkResult(err, result) { function checkResult(err, result) {
// console.log(err, result); // console.log(err, result);
@ -19,9 +21,18 @@ describe('role model', function() {
ds = loopback.createDataSource({connector: 'memory'}); ds = loopback.createDataSource({connector: 'memory'});
// Re-attach the models so that they can have isolated store to avoid // Re-attach the models so that they can have isolated store to avoid
// pollutions from other tests // pollutions from other tests
ACL.attachTo(ds);
User.attachTo(ds); User.attachTo(ds);
Role.attachTo(ds); Role.attachTo(ds);
RoleMapping.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() { 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();
});
});
}); });