Define the models/relations for ACL
This commit is contained in:
parent
67b934357b
commit
48a0242711
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
;
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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 -
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue