Define the models/relations for ACL

This commit is contained in:
Raymond Feng 2013-11-11 22:16:51 -08:00
parent 67b934357b
commit 48a0242711
5 changed files with 329 additions and 59 deletions

View File

@ -33,12 +33,15 @@
var loopback = require('../loopback'); var loopback = require('../loopback');
/**
* Schema for Scope which represents the permissions that are granted to client applications by the resource owner
*/
var ScopeSchema = { var ScopeSchema = {
name: {type: String, required: true}, name: {type: String, required: true},
description: String description: String
}; };
var ScopeResourceAccessSchema = { var ScopeACLSchema = {
model: String, // The name of the model model: String, // The name of the model
property: String, // The name of the property, method, scope, or relation property: String, // The name of the property, method, scope, or relation
@ -57,7 +60,7 @@ var ScopeResourceAccessSchema = {
scopeId: Number scopeId: Number
}; };
var ScopeResourceAccess = loopback.createModel('ScopeResourceAccess', ScopeResourceAccessSchema, { var ScopeACL = loopback.createModel('ScopeACL', ScopeACLSchema, {
relations: { relations: {
scope: { scope: {
type: 'belongsTo', type: 'belongsTo',
@ -68,6 +71,10 @@ var ScopeResourceAccess = loopback.createModel('ScopeResourceAccess', ScopeResou
}); });
/** /**
* Resource owner grants/delegates permissions to client applications
*
* For a protected resource, does the client application have the authorization from the resource owner (user or system)?
*
* Scope has many resource access entries * Scope has many resource access entries
* @type {createModel|*} * @type {createModel|*}
*/ */
@ -75,12 +82,21 @@ var Scope = loopback.createModel('Scope', ScopeSchema, {
relations: { relations: {
resources: { resources: {
type: 'hasMany', type: 'hasMany',
model: 'ScopeResourceAccess', model: 'ScopeACL',
foreignKey: 'scopeId' foreignKey: 'scopeId'
} }
} }
}); });
/**
* System grants permissions to principals (users/applications, can be grouped into roles).
*
* Protected resource: the model data and operations (model/property/method/relation/)
*
* For a given principal, such as client application and/or user, is it allowed to access (read/write/execute)
* the protected resource?
*
*/
var ACLSchema = { var ACLSchema = {
model: String, // The name of the model model: String, // The name of the model
property: String, // The name of the property, method, scope, or relation property: String, // The name of the property, method, scope, or relation
@ -112,32 +128,72 @@ var ACL = loopback.createModel('ACL', ACLSchema);
module.exports = { module.exports = {
ACL: ACL, ACL: ACL,
Scope: Scope, Scope: Scope,
ScopeResourceAccess: ScopeResourceAccess ScopeACL: ScopeACL
}; };
Scope.isAllowed = function (scope, model, property, accessType, callback) { /**
* Check if the given principal is allowed to access the model/property
* @param principalType
* @param principalId
* @param model
* @param property
* @param accessType
* @param callback
*/
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
ACL.find({where: {principalType: principalType, principalId: principalId,
model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}},
function (err, acls) {
if (err) {
callback && callback(err);
return;
}
var resolvedPermission = acls.reduce(function (previousValue, currentValue, index, array) {
// If the property is the same or the previous one is '*' (ALL)
if (previousValue.property === currentValue.property || (previousValue.property === '*' && currentValue.property)) {
previousValue.property = currentValue.property;
if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === '*' && currentValue.accessType)) {
previousValue.accessType = currentValue.accessType;
}
}
return previousValue;
}, {principalType: principalType, principalId: principalId, model: model, property: '*', accessType: '*', permission: 'Allow'});
callback && callback(resolvedPermission);
});
};
/**
* Check if the given scope is allowed to access the model/property
* @param scope
* @param model
* @param property
* @param accessType
* @param callback
*/
Scope.checkPermission = function (scope, model, property, accessType, callback) {
Scope.findOne({where: {name: scope}}, function (err, scope) { Scope.findOne({where: {name: scope}}, function (err, scope) {
if (err) { if (err) {
callback && callback(err); callback && callback(err);
} else { } else {
scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) {
{ if (err) {
if (err) { callback && callback(err);
callback && callback(err); return;
} else {
console.log('Resources: ', resources);
for (var r = 0; r < resources.length; r++) {
if (resources[r].permission === 'Allow') {
callback && callback(null, true);
return;
}
} }
callback && callback(null, false); // Try to resolve the permission
var resolvedPermission = resources.reduce(function (previousValue, currentValue, index, array) {
// If the property is the same or the previous one is '*' (ALL)
if (previousValue.property === currentValue.property || (previousValue.property === '*' && currentValue.property)) {
previousValue.property = currentValue.property;
if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === '*' && currentValue.accessType)) {
previousValue.accessType = currentValue.accessType;
}
}
return previousValue;
}, {model: model, property: '*', accessType: '*', permission: 'Allow'});
callback && callback(resolvedPermission);
} }
);
}
)
;
} }
}); });
}; };

View File

@ -5,36 +5,112 @@ var RoleSchema = {
id: {type: String, id: true}, // Id id: {type: String, id: true}, // Id
name: {type: String, required: true}, // The name of a role name: {type: String, required: true}, // The name of a role
description: String, // Description description: String, // Description
// roles: [String], // A role can be an aggregate of other roles
// users: [String], // A role contains a list of user ids
parent: String,
// Timestamps // Timestamps
created: {type: Date, default: Date}, created: {type: Date, default: Date},
modified: {type: Date, default: Date} modified: {type: Date, default: Date}
}; };
var Role = loopback.createModel('Role', RoleSchema, { /**
* Map principals to roles
*/
var RoleMappingSchema = {
id: {type: String, id: true}, // Id
roleId: String, // The role id
principalType: String, // The principal type, such as user, application, or role
principalId: String // The principal id
};
var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, {
relations: { relations: {
roles: { role: {
type: 'hasMany', type: 'belongsTo',
model: 'Role', model: 'Role',
foreignKey: 'parent' foreignKey: 'roleId'
},
users: {
type: 'hasAndBelongsToMany',
model: 'user',
foreignKey: 'userId'
},
applications: {
type: 'hasAndBelongsToMany',
model: 'Application',
foreignKey: 'appId'
} }
} }
}); });
module.exports = Role; RoleMapping.prototype.application = function(callback) {
if(this.principalType === 'application') {
loopback.Application.findById(this.principalId, callback);
} else {
process.nextTick(function() {
callback && callback(null, null);
});
}
};
RoleMapping.prototype.user = function(callback) {
if(this.principalType === 'user') {
loopback.User.findById(this.principalId, callback);
} else {
process.nextTick(function() {
callback && callback(null, null);
});
}
};
RoleMapping.prototype.childRole = function(callback) {
if(this.principalType === 'role') {
loopback.User.findById(this.principalId, callback);
} else {
process.nextTick(function() {
callback && callback(null, null);
});
}
};
var Role = loopback.createModel('Role', RoleSchema, {
relations: {
principals: {
type: 'hasMany',
model: 'RoleMapping',
foreignKey: 'roleId'
}
}
});
Role.once('dataSourceAttached', function() {
Role.prototype.users = function(callback) {
RoleMapping.find({where: {roleId: this.id, principalType: 'user'}}, function(err, mappings) {
if(err) {
callback && callback(err);
return;
}
return mappings.map(function(m) {
return m.principalId;
});
});
};
Role.prototype.applications = function(callback) {
RoleMapping.find({where: {roleId: this.id, principalType: 'application'}}, function(err, mappings) {
if(err) {
callback && callback(err);
return;
}
return mappings.map(function(m) {
return m.principalId;
});
});
};
Role.prototype.roles = function(callback) {
RoleMapping.find({where: {roleId: this.id, principalType: 'role'}}, function(err, mappings) {
if(err) {
callback && callback(err);
return;
}
return mappings.map(function(m) {
return m.principalId;
});
});
};
});
// Special roles // Special roles
Role.OWNER = '$owner'; // owner of the object Role.OWNER = '$owner'; // owner of the object
@ -42,3 +118,47 @@ Role.RELATED = "$related"; // any User with a relationship to the object
Role.AUTHENTICATED = "$authenticated"; // authenticated user Role.AUTHENTICATED = "$authenticated"; // authenticated user
Role.EVERYONE = "$everyone"; // everyone Role.EVERYONE = "$everyone"; // everyone
/**
* Check if a given principal is in the role
*
* @param principalType
* @param principalId
* @param role
* @param callback
*/
Role.isInRole = function(principalType, principalId, role, callback) {
Role.findOne({where: {name: role}}, function(err, role) {
if(err) {
callback && callback(err);
return;
}
RoleMapping.exists({where: {roleId: role.id, principalType: principalType, principalId: principalId}}, callback);
});
};
/**
* List roles for a given principal
* @param principalType
* @param principalId
* @param role
* @param callback
*/
Role.getRoles = function(principalType, principalId, role, callback) {
RoleMapping.find({where: {principalType: principalType, principalId: principalId}},function(err, mappings) {
if(err) {
callback && callback(err);
return;
}
var roles = [];
mappings.forEach(function(m) {
roles.push(m.roleId);
});
callback && callback(null, roles);
});
};
module.exports = {
Role: Role,
RoleMapping: RoleMapping
};

View File

@ -1,27 +1,69 @@
var assert = require('assert'); var assert = require('assert');
var loopback = require('../index'); var loopback = require('../index');
var acl = require('../lib/models/acl'); var acl = require('../lib/models/acl');
var Scope = acl.Scope;
var ACL = acl.ACL;
var ScopeACL = acl.ScopeACL;
var User = loopback.User; var User = loopback.User;
describe('security scopes', function () { describe('security scopes', function () {
it("should allow access to models", function () { it("should allow access to models for the given scope by wildcard", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); var ds = loopback.createDataSource({connector: loopback.Memory});
acl.Scope.attachTo(ds); Scope.attachTo(ds);
acl.ScopeResourceAccess.attachTo(ds); ScopeACL.attachTo(ds);
// console.log(acl.Scope.relations); // console.log(Scope.relations);
acl.Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { Scope.create({name: 'user', description: 'access user information'}, function (err, scope) {
console.log(scope); // console.log(scope);
scope.resources.create({model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, resource) { scope.resources.create({model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, resource) {
console.log(resource); // console.log(resource);
acl.Scope.isAllowed('user', 'user', '*', '*', console.log); Scope.checkPermission('user', 'user', '*', '*', console.log);
Scope.checkPermission('user', 'user', 'name', '*', console.log);
Scope.checkPermission('user', 'user', 'name', 'Read', console.log);
}); });
}); });
}); });
it("should allow access to models for the given scope", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
Scope.attachTo(ds);
ScopeACL.attachTo(ds);
// console.log(Scope.relations);
Scope.create({name: 'user', description: 'access user information'}, function (err, scope) {
// console.log(scope);
scope.resources.create({model: 'user', property: 'name', accessType: 'Read', permission: 'Allow'}, function (err, resource) {
// console.log(resource);
Scope.checkPermission('user', 'user', '*', '*', console.log);
Scope.checkPermission('user', 'user', 'name', '*', console.log);
Scope.checkPermission('user', 'user', 'name', 'Read', console.log);
});
});
});
});
describe('security ACLs', function () {
it("should allow access to models for the given principal by wildcard", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
// console.log(Scope.relations);
ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, acl) {
ACL.checkPermission('user', 'u001', 'user', 'u001', 'Read', console.log);
});
});
}); });

View File

@ -512,6 +512,39 @@ describe('Model', function() {
}); });
}); });
describe('Model.extend() events', function() {
it('create isolated emitters for subclasses', function() {
var User1 = loopback.createModel('User1', {
'first': String,
'last': String
});
var User2 = loopback.createModel('User2', {
'name': String
});
var user1Triggered = false;
User1.once('x', function(event) {
user1Triggered = true;
});
var user2Triggered = false;
User2.once('x', function(event) {
user2Triggered = true;
});
assert(User1.once !== User2.once);
assert(User1.once !== loopback.Model.once);
User1.emit('x', User1);
assert(user1Triggered);
assert(!user2Triggered);
});
});
// describe('Model.hasAndBelongsToMany()', function() { // describe('Model.hasAndBelongsToMany()', function() {
// it("TODO: implement / document", function(done) { // it("TODO: implement / document", function(done) {
// /* example - // /* example -

View File

@ -1,39 +1,58 @@
var assert = require('assert'); var assert = require('assert');
var loopback = require('../index'); var loopback = require('../index');
var Role = require('../lib/models/role'); var role = require('../lib/models/role');
var Role = role.Role;
var RoleMapping = role.RoleMapping;
var User = loopback.User; var User = loopback.User;
describe('security models', function () { describe('security models', function () {
describe('roles', function () { describe('roles', function () {
it("Defines role/role relations", function () { it("should define role/role relations", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); var ds = loopback.createDataSource({connector: loopback.Memory});
Role.attachTo(ds); Role.attachTo(ds);
RoleMapping.attachTo(ds);
Role.create({name: 'user'}, function (err, role) { Role.create({name: 'user'}, function (err, userRole) {
role.roles.create({name: 'admin'}, function (err, role2) { Role.create({name: 'admin'}, function (err, adminRole) {
Role.find(console.log); userRole.principals.create({principalType: 'role', principalId: adminRole.id}, function (err, mapping) {
role.roles(console.log); Role.find(function(err, roles) {
assert.equal(roles.length, 2);
});
RoleMapping.find(function(err, mappings) {
assert.equal(mappings.length, 1);
});
userRole.principals(function(err, principals) {
assert.equal(principals.length, 1);
});
userRole.roles(function(err, roles) {
assert.equal(roles.length, 1);
});
});
}); });
}); });
}); });
it("Defines role/user relations", function () { it("should define role/user relations", function () {
var ds = loopback.createDataSource({connector: loopback.Memory}); var ds = loopback.createDataSource({connector: loopback.Memory});
User.attachTo(ds); User.attachTo(ds);
Role.attachTo(ds); Role.attachTo(ds);
Role.create({name: 'user'}, function (err, role) { User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
role.users.create({name: 'Raymond'}, function (err, user) { console.log('User: ', user.id);
console.log('User: ', user); Role.create({name: 'userRole'}, function (err, role) {
Role.find(console.log); role.principals.create({principalType: 'user', principalId: user.id}, function (err, p) {
role.users(console.log); Role.find(console.log);
role.principals(console.log);
role.users(console.log);
});
}); });
}); });
}); });
}); });
}); });