Fix the ACL resolution against rules by matching score

This commit is contained in:
Raymond Feng 2013-12-09 15:26:53 -08:00
parent af2b8dd4ff
commit 7f51c28539
2 changed files with 157 additions and 58 deletions

View File

@ -96,15 +96,15 @@ var ACL = loopback.createModel('ACL', ACLSchema);
ACL.ALL = '*';
ACL.DEFAULT = 'DEFAULT';
ACL.ALLOW = 'ALLOW';
ACL.ALARM = 'ALARM';
ACL.AUDIT = 'AUDIT';
ACL.DENY = 'DENY';
ACL.DEFAULT = 'DEFAULT'; // Not specified
ACL.ALLOW = 'ALLOW'; // Allow
ACL.ALARM = 'ALARM'; // Warn - send an alarm
ACL.AUDIT = 'AUDIT'; // Audit - record the access
ACL.DENY = 'DENY'; // Deny
ACL.READ = 'READ';
ACL.WRITE = 'WRITE';
ACL.EXECUTE = 'EXECUTE';
ACL.READ = 'READ'; // Read operation
ACL.WRITE = 'WRITE'; // Write operation
ACL.EXECUTE = 'EXECUTE'; // Execute operation
ACL.USER = 'USER';
ACL.APP = ACL.APPLICATION = 'APP';
@ -119,49 +119,96 @@ var permissionOrder = {
DENY: 4
};
function overridePermission(p1, p2) {
p1 = permissionOrder[p1] ? p1 : ACL.ALLOW;
p2 = permissionOrder[p2] ? p2 : ACL.ALLOW;
var i1 = permissionOrder[p1];
var i2 = permissionOrder[p2];
return i1 > i2 ? p1 : p2;
/**
* Calculate the matching score for the given rule and request
* @param {Object} rule The ACL entry
* @param {Object} req The request
* @returns {number}
*/
function getMatchingScore(rule, req) {
var props = ['model', 'property', 'accessType'];
var score = 0;
for (var i = 0; i < props.length; i++) {
// Shift the score by 4 for each of the properties as the weight
score = score * 4;
var val1 = rule[props[i]] || ACL.ALL;
var val2 = req[props[i]] || ACL.ALL;
if (val1 === val2) {
// Exact match
score += 3;
} else if (val1 === ACL.ALL) {
// Wildcard match
score += 2;
} else if (val2 === ACL.ALL) {
// Doesn't match at all
score += 1;
} else {
return -1;
}
}
score = score * 4;
score += permissionOrder[rule.permission || ACL.ALLOW] - 1;
return score;
}
/*!
* Resolve permission from the ACLs
* @param acls
* @param defaultPermission
* @returns {*|Object|Mixed}
* @param {Object[]) acls The list of ACLs
* @param {Object} req The request
* @returns {Object} The effective ACL
*/
function resolvePermission(acls, defaultPermission) {
var resolvedPermission = acls.reduce(function (previousValue, currentValue, index, array) {
// If the property is the same or the previous one is ACL.ALL (ALL)
if (previousValue.property === currentValue.property || (previousValue.property === ACL.ALL && currentValue.property)) {
previousValue.property = currentValue.property;
// Check if the accessType applies
if (previousValue.accessType === currentValue.accessType
|| previousValue.accessType === ACL.ALL
|| currentValue.accessType === ACL.ALL
|| !currentValue.accessType) {
previousValue.permission = overridePermission(previousValue.permission, currentValue.permission);
function resolvePermission(acls, req) {
// Sort by the matching score in descending order
acls = acls.sort(function (rule1, rule2) {
return getMatchingScore(rule2, req) - getMatchingScore(rule1, req);
});
var permission = ACL.DEFAULT;
var score = 0;
for (var i = 0; i < acls.length; i++) {
score = getMatchingScore(acls[i], req);
if (score < 0) {
break;
}
if (req.model !== ACL.ALL &&
req.property !== ACL.ALL &&
req.accessType !== ACL.ALL) {
// We should stop from the first match for non-wildcard
permission = acls[i].permission;
break;
} else {
if(acls[i].model === req.model &&
acls[i].property === req.property &&
acls[i].accessType === req.accessType
) {
// We should stop at the exact match
permission = acls[i].permission;
break;
}
// For wildcard match, find the strongest permission
if(permissionOrder[acls[i].permission] > permissionOrder[permission]) {
permission = acls[i].permission;
}
}
return previousValue;
}, defaultPermission);
return resolvedPermission;
}
return {
model: req.model,
property: req.property,
accessType: req.accessType,
permission: permission || ACL.DEFAULT
};
}
/*!
* Check the LDL ACLs
* @param principalType
* @param principalId
* @param {String} principalType
* @param {*} principalId
* @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}}
* @return {Object[]} An array of ACLs
*/
function getStaticPermission(principalType, principalId, model, property, accessType) {
function getStaticACLs(principalType, principalId, model, property, accessType) {
var modelClass = loopback.getModel(model);
var staticACLs = [];
if (modelClass && modelClass.settings.acls) {
@ -194,11 +241,7 @@ function getStaticPermission(principalType, principalId, model, property, access
});
}
var defaultPermission = {principalType: principalType, principalId: principalId,
model: model, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
defaultPermission = resolvePermission(staticACLs, defaultPermission);
return defaultPermission;
return staticACLs;
}
/**
@ -219,28 +262,39 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc
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) {
var req = {
model: model,
property: property,
accessType: accessType
};
var acls = getStaticACLs(principalType, principalId, model, property, accessType);
var resolved = resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DENY) {
// Fail fast
process.nextTick(function() {
callback && callback(null, defaultPermission);
callback && callback(null, resolved);
});
return;
}
ACL.find({where: {principalType: principalType, principalId: principalId,
model: model, property: propertyQuery, accessType: accessTypeQuery}},
function (err, acls) {
function (err, dynACLs) {
if (err) {
callback && callback(err);
return;
}
var resolvedPermission = resolvePermission(acls, defaultPermission);
if(resolvedPermission.permission === ACL.DEFAULT) {
acls = acls.concat(dynACLs);
resolved = resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DEFAULT) {
var modelClass = loopback.getModel(model);
resolvedPermission.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
}
callback && callback(null, resolvedPermission);
callback && callback(null, resolved);
});
};
@ -269,6 +323,11 @@ Scope.checkPermission = function (scope, model, property, accessType, callback)
/**
* Check if the request has the permission to access
* @param {Object} context
* @property {Object[]} principals An array of principals
* @property {String|Model} model The model name or model class
* @property {*} id The model instance id
* @property {String} property The property/method/relation name
* @property {String} accessType The access type
* @param {Function} callback
*/
ACL.checkAccess = function (context, callback) {
@ -289,19 +348,25 @@ ACL.checkAccess = function (context, callback) {
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};
var req = {
model: model.modelName,
property: property,
accessType: accessType
};
var effectiveACLs = [];
// Check the LDL ACLs
principals.forEach(function(p) {
var perm = getStaticPermission(p.principalType, p.principalId, model.modelName, property, accessType);
defaultPermission = resolvePermission([perm], defaultPermission);
effectiveACLs = effectiveACLs.concat(getStaticACLs(p.principalType, p.principalId, model.modelName, property, accessType));
});
if(defaultPermission.permission === ACL.DENY) {
var resolved = resolvePermission(effectiveACLs, req);
if(resolved && resolved.permission === ACL.DENY) {
// Fail fast
process.nextTick(function() {
callback && callback(null, defaultPermission);
callback && callback(null, resolved);
});
return;
}
@ -311,7 +376,6 @@ ACL.checkAccess = function (context, callback) {
callback && callback(err);
return;
}
var effectiveACLs = [];
var inRoleTasks = [];
acls.forEach(function (acl) {
principals.forEach(function (principal) {
@ -333,8 +397,8 @@ ACL.checkAccess = function (context, callback) {
});
async.parallel(inRoleTasks, function(err, results) {
defaultPermission = resolvePermission(effectiveACLs, defaultPermission);
callback && callback(null, defaultPermission);
resolved = resolvePermission(effectiveACLs, req);
callback && callback(null, resolved);
});
});
};

View File

@ -30,6 +30,8 @@ describe('security scopes', function () {
it("should allow access to models for the given scope", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
Scope.attachTo(ds);
ACL.attachTo(ds);
Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) {
ACL.create({principalType: ACL.SCOPE, principalId: scope.id,
model: 'User', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW},
@ -62,6 +64,7 @@ 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);
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) {
@ -83,6 +86,38 @@ describe('security ACLs', function () {
});
it("should allow access to models by exception", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
accessType: ACL.ALL, permission: ACL.DENY}, function (err, acl) {
ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL,
accessType: ACL.READ, permission: ACL.ALLOW}, function (err, acl) {
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW);
});
ACL.checkPermission(ACL.USER, 'u001', 'User', ACL.ALL, ACL.READ, function (err, perm) {
assert(perm.permission === ACL.ALLOW);
});
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.WRITE, function (err, perm) {
assert(perm.permission === ACL.DENY);
});
ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.ALL, function (err, perm) {
assert(perm.permission === ACL.DENY);
});
});
});
});
it("should honor defaultPermission from the model", function () {
var ds = loopback.createDataSource({connector: loopback.Memory});
ACL.attachTo(ds);
@ -147,7 +182,7 @@ describe('security ACLs', function () {
});
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.ALL, function (err, perm) {
assert(perm.permission === ACL.DENY);
assert(perm.permission === ACL.ALLOW);
});
});