Add checkAccess for subject and token

This commit is contained in:
Raymond Feng 2013-11-20 13:31:30 -08:00
parent 2c7c5fc7ec
commit bee8a3b022
5 changed files with 333 additions and 47 deletions

View File

@ -32,6 +32,11 @@
*/
var loopback = require('../loopback');
var async = require('async');
var assert = require('assert');
var role = require('./role');
var Role = role.Role;
/**
* Schema for Scope which represents the permissions that are granted to client applications by the resource owner
@ -91,6 +96,7 @@ var ACL = loopback.createModel('ACL', ACLSchema);
ACL.ALL = '*';
ACL.DEFAULT = 'DEFAULT';
ACL.ALLOW = 'ALLOW';
ACL.ALARM = 'ALARM';
ACL.AUDIT = 'AUDIT';
@ -106,6 +112,7 @@ ACL.ROLE = 'ROLE';
ACL.SCOPE = 'SCOPE';
var permissionOrder = {
DEFAULT: 0,
ALLOW: 1,
ALARM: 2,
AUDIT: 3,
@ -144,62 +151,80 @@ function resolvePermission(acls, defaultPermission) {
return resolvedPermission;
}
/**
* Check if the given principal is allowed to access the model/property
/*!
* Check the LDL ACLs
* @param principalType
* @param principalId
* @param model
* @param property
* @param accessType
* @param callback
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
*
* @returns {{principalType: *, principalId: *, model: *, property: string, accessType: *, permission: string}}
*/
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
function getStaticPermission(principalType, principalId, model, property, accessType) {
var modelClass = loopback.getModel(model);
var staticACLs = [];
var modelClass = loopback.getModel(model); {
if(modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function(acl) {
staticACLs.push({
model: model,
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType,
permission: acl.permission
});
if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function (acl) {
staticACLs.push({
model: model,
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType,
permission: acl.permission
});
}
var prop = modelClass &&
(modelClass.definition.properties[property] // regular property
});
}
var prop = modelClass &&
(modelClass.definition.properties[property] // regular property
|| (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope
|| modelClass[property] // static method
|| modelClass.prototype[property]); // prototype method
if(prop && prop.acls) {
prop.acls.forEach(function(acl) {
staticACLs.push({
model: model,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission
});
if (prop && prop.acls) {
prop.acls.forEach(function (acl) {
staticACLs.push({
model: modelClass.modelName,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission
});
}
});
}
var defaultPermission = {principalType: principalType, principalId: principalId,
model: model, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
defaultPermission = resolvePermission(staticACLs, defaultPermission);
return defaultPermission;
}
/**
* Check if the given principal is allowed to access the model/property
* @param {String} principalType The principal type
* @param {String} principalId The principal id
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
* @param {Function} callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Object} the access permission
*/
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var defaultPermission = getStaticPermission(principalType, principalId, model, property, accessType);
if(defaultPermission.permission === ACL.DENY) {
// Fail fast
callback && callback(null, defaultPermission);
process.nextTick(function() {
callback && callback(null, defaultPermission);
});
return;
}
@ -211,17 +236,25 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc
return;
}
var resolvedPermission = resolvePermission(acls, defaultPermission);
if(resolvedPermission.permission === ACL.DEFAULT) {
var modelClass = loopback.getModel(model);
resolvedPermission.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
}
callback && callback(null, resolvedPermission);
});
};
/**
* Check if the given scope is allowed to access the model/property
* @param scope
* @param model
* @param property
* @param accessType
* @param callback
* @param {String} scope The scope name
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
* @param {Function} callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Object} the access permission
*/
Scope.checkPermission = function (scope, model, property, accessType, callback) {
Scope.findOne({where: {name: scope}}, function (err, scope) {
@ -233,6 +266,113 @@ Scope.checkPermission = function (scope, model, property, accessType, callback)
});
};
/**
* Check if the request has the permission to access
* @param {Object} context
* @param {Function} callback
*/
ACL.checkAccess = function (context, callback) {
context = context || {};
var principals = context.principals || [];
var model = context.model;
model = ('string' === typeof model) ? loopback.getModel(model) : model;
var id = context.id;
var property = context.property;
var accessType = context.accessType;
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var defaultPermission = {principalType: null, principalId: null,
model: model.modelName, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
// Check the LDL ACLs
principals.forEach(function(p) {
var perm = getStaticPermission(p.principalType, p.principalId, model.modelName, property, accessType);
defaultPermission = resolvePermission([perm], defaultPermission);
});
if(defaultPermission.permission === ACL.DENY) {
// Fail fast
process.nextTick(function() {
callback && callback(null, defaultPermission);
});
return;
}
ACL.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function (err, acls) {
if (err) {
callback && callback(err);
return;
}
var effectiveACLs = [];
var inRoleTasks = [];
acls.forEach(function (acl) {
principals.forEach(function (principal) {
if (principal.principalType === acl.pricipalType && principal.principalId === acl.principalId) {
effectiveACLs.push(acl);
} else if (acl.principalType === ACL.ROLE) {
inRoleTasks.push(function (done) {
Role.isInRole(acl.principalId,
{principalType: principal.principalType, principalId: acl.principalId, model: model, id: id, property: property},
function (err, inRole) {
if(!err) {
effectiveACLs.push(acl);
}
done(err, acl);
});
});
}
});
});
async.parallel(inRoleTasks, function(err, results) {
defaultPermission = resolvePermission(effectiveACLs, defaultPermission);
callback && callback(null, defaultPermission);
});
});
};
/**
* Check if the given access token can invoke the method
* @param {AccessToken} token The access token
* @param {String} model The model name
* @param {*} modelId The model id
* @param {String} method The method name
* @param callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
assert(token, 'Access token is required');
var principals = [];
if(token.userId) {
principals.push({principalType: ACL.USER, principalId: token.userId});
}
if(token.appId) {
principals.push({principalType: ACL.APPLICATION, principalId: token.appId});
}
var context = {
principals: principals,
model: model,
property: method,
accessType: ACL.EXECUTE,
id: modelId
};
ACL.checkAccess(context, function(err, access) {
if(err) {
callback && callback(err);
return;
}
callback && callback(access.permission !== ACL.DENY);
});
};
module.exports = {
ACL: ACL,

View File

@ -79,7 +79,7 @@ Model.setup = function () {
self.beforeRemote.apply(self, args);
});
}
}
};
// after remote hook
ModelCtor.afterRemote = function (name, fn) {
@ -95,7 +95,7 @@ Model.setup = function () {
self.afterRemote.apply(self, args);
});
}
}
};
// Map the prototype method to /:id with data in the body
ModelCtor.sharedCtor.accepts = [
@ -110,7 +110,34 @@ Model.setup = function () {
ModelCtor.sharedCtor.returns = {root: true};
return ModelCtor;
};
/*!
* Get the reference to ACL in a lazy fashion to avoid race condition in require
*/
var ACL = null;
function getACL() {
return ACL || (ACL = require('./acl').ACL);
}
/**
* Check if the given access token can invoke the method
*
* @param {AccessToken} token The access token
* @param {*} modelId The model id
* @param {String} method The method name
* @param callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
Model.checkAccess = function(token, modelId, method, callback) {
var ACL = getACL();
var methodName = 'string' === typeof method? method: method && method.name;
ACL.checkAccessForToken(token, this.modelName, modelId, methodName, callback);
};
// setup the initial model
Model.setup();

View File

@ -226,6 +226,14 @@ Role.isInRole = function (role, context, callback) {
var principalType = context.principalType;
var principalId = context.principalId;
// Check if it's the same role
if(principalType === RoleMapping.ROLE && principalId === role) {
process.nextTick(function() {
callback && callback(null, true);
});
return;
}
Role.findOne({where: {name: role}}, function (err, result) {
if (err) {
callback && callback(err);

View File

@ -26,7 +26,8 @@
"bcryptjs": "~0.7.10",
"underscore.string": "~2.3.3",
"underscore": "~1.5.2",
"uid2": "0.0.3"
"uid2": "0.0.3",
"async": "~0.2.9"
},
"devDependencies": {
"mocha": "~1.14.0",

View File

@ -3,7 +3,9 @@ var loopback = require('../index');
var acl = require('../lib/models/acl');
var Scope = acl.Scope;
var ACL = acl.ACL;
var ScopeACL = acl.ScopeACL;
var role = require('../lib/models/role');
var Role = role.Role;
var RoleMapping = role.RoleMapping;
var User = loopback.User;
function checkResult(err, result) {
@ -81,6 +83,39 @@ describe('security ACLs', function () {
});
it("should honor defaultPermission from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
var Customer = ds.createModel('Customer', {
name: {
type: String,
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
}
}, {
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
});
Customer.settings.defaultPermission = ACL.DENY;
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY);
});
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW);
});
ACL.checkPermission(ACL.USER, 'u002', 'Customer', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY);
});
});
it("should honor static ACLs from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
var Customer = ds.createModel('Customer', {
@ -117,6 +152,81 @@ describe('security ACLs', function () {
});
it("should check access against LDL, ACL, and Role", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
Role.attachTo(ds);
RoleMapping.attachTo(ds);
User.attachTo(ds);
// var log = console.log;
var log = function() {};
// Create
User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) {
log('User: ', user.toObject());
// Define a model with static ACLs
var Customer = ds.createModel('Customer', {
name: {
type: String,
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.WRITE, permission: ACL.DENY},
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
}
}, {
acls: [
{principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW}
]
});
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'Customer', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) {
log('ACL 1: ', acl.toObject());
Role.create({name: 'MyRole'}, function (err, myRole) {
log('Role: ', myRole.toObject());
myRole.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) {
log('Principal added to role: ', p.toObject());
ACL.create({principalType: ACL.ROLE, principalId: myRole.id, model: 'Customer', property: ACL.ALL,
accessType: ACL.READ, permission: ACL.DENY}, function (err, acl) {
log('ACL 2: ', acl.toObject());
ACL.checkAccess({
principals: [
{principalType: ACL.USER, principalId: 'u001'}
],
model: 'Customer',
property: 'name',
accessType: ACL.READ
}, function(err, access) {
assert(!err && access.permission === ACL.ALLOW);
});
ACL.checkAccess({
principals: [
{principalType: ACL.USER, principalId: 'u001'}
],
model: 'Customer',
accessType: ACL.READ
}, function(err, access) {
assert(!err && access.permission === ACL.DENY);
});
});
});
});
});
});
});
});