From 7f51c28539d4fbb0d7d8525325d127bad69c4a66 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 9 Dec 2013 15:26:53 -0800 Subject: [PATCH] Fix the ACL resolution against rules by matching score --- lib/models/acl.js | 178 +++++++++++++++++++++++++++++++--------------- test/acl.test.js | 37 +++++++++- 2 files changed, 157 insertions(+), 58 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 918fcfbe..fe9532b6 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -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); }); }); }; diff --git a/test/acl.test.js b/test/acl.test.js index 46035992..b538c227 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -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); }); });