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');
/**
* Schema for Scope which represents the permissions that are granted to client applications by the resource owner
*/
var ScopeSchema = {
name: {type: String, required: true},
description: String
};
var ScopeResourceAccessSchema = {
var ScopeACLSchema = {
model: String, // The name of the model
property: String, // The name of the property, method, scope, or relation
@ -57,7 +60,7 @@ var ScopeResourceAccessSchema = {
scopeId: Number
};
var ScopeResourceAccess = loopback.createModel('ScopeResourceAccess', ScopeResourceAccessSchema, {
var ScopeACL = loopback.createModel('ScopeACL', ScopeACLSchema, {
relations: {
scope: {
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
* @type {createModel|*}
*/
@ -75,12 +82,21 @@ var Scope = loopback.createModel('Scope', ScopeSchema, {
relations: {
resources: {
type: 'hasMany',
model: 'ScopeResourceAccess',
model: 'ScopeACL',
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 = {
model: String, // The name of the model
property: String, // The name of the property, method, scope, or relation
@ -112,32 +128,72 @@ var ACL = loopback.createModel('ACL', ACLSchema);
module.exports = {
ACL: ACL,
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) {
if (err) {
callback && callback(err);
} else {
scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources)
{
if (err) {
callback && callback(err);
} else {
console.log('Resources: ', resources);
for (var r = 0; r < resources.length; r++) {
if (resources[r].permission === 'Allow') {
callback && callback(null, true);
return;
}
scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) {
if (err) {
callback && callback(err);
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
name: {type: String, required: true}, // The name of a role
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
created: {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: {
roles: {
type: 'hasMany',
role: {
type: 'belongsTo',
model: 'Role',
foreignKey: 'parent'
},
users: {
type: 'hasAndBelongsToMany',
model: 'user',
foreignKey: 'userId'
},
applications: {
type: 'hasAndBelongsToMany',
model: 'Application',
foreignKey: 'appId'
foreignKey: 'roleId'
}
}
});
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
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.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 loopback = require('../index');
var acl = require('../lib/models/acl');
var Scope = acl.Scope;
var ACL = acl.ACL;
var ScopeACL = acl.ScopeACL;
var User = loopback.User;
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});
acl.Scope.attachTo(ds);
acl.ScopeResourceAccess.attachTo(ds);
Scope.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) {
console.log(scope);
Scope.create({name: 'user', description: 'access user information'}, function (err, scope) {
// console.log(scope);
scope.resources.create({model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, resource) {
console.log(resource);
acl.Scope.isAllowed('user', 'user', '*', '*', console.log);
// console.log(resource);
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() {
// it("TODO: implement / document", function(done) {
// /* example -

View File

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