Fix the ACL resolution against rules by matching score
This commit is contained in:
parent
af2b8dd4ff
commit
7f51c28539
|
@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue