Merge pull request #1584 from strongloop/feature/add-more-acl-utils
Enhance the ACL related models
This commit is contained in:
commit
06cece038e
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue