From 8381b05da14b89d7b48122b76b8b87ae207e5e6d Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 15 Nov 2013 09:41:26 -0800 Subject: [PATCH 1/2] Allows LDL level ACLs --- lib/loopback.js | 8 +++++ lib/models/acl.js | 78 +++++++++++++++++++++++++++++++++++++++-------- test/acl.test.js | 70 ++++++++++++++++++++++++++++++++---------- 3 files changed, 128 insertions(+), 28 deletions(-) diff --git a/lib/loopback.js b/lib/loopback.js index 328f2ffa..468df874 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -165,6 +165,14 @@ loopback.memory = function (name) { return memory; } +/** + * Loop up a model class by name + * @param {String} modelName The model name + */ +loopback.getModel = function(modelName) { + return loopback.Model.dataSource.models[modelName]; +}; + /* * Built in models / services */ diff --git a/lib/models/acl.js b/lib/models/acl.js index 754bb8f5..61fe4453 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -120,6 +120,30 @@ function overridePermission(p1, p2) { return i1 > i2 ? p1 : p2; } +/*! + * Resolve permission from the ACLs + * @param acls + * @param defaultPermission + * @returns {*|Object|Mixed} + */ +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); + } + } + return previousValue; + }, defaultPermission); + return resolvedPermission; +} + /** * Check if the given principal is allowed to access the model/property * @param principalType @@ -132,9 +156,49 @@ function overridePermission(p1, p2) { 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; + accessType = accessType || ACL.ALL; var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]}; + var staticACLs = []; + var modelClass = loopback.getModel(model); { + if(modelClass && modelClass.settings.acls) { + modelClass.settings.acls.forEach(function(acl) { + staticACLs.push({ + model: model, + 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]; + 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 + }); + }); + } + } + + var defaultPermission = {principalType: principalType, principalId: principalId, + model: model, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW}; + + defaultPermission = resolvePermission(staticACLs, defaultPermission); + + if(defaultPermission.permission === ACL.DENY) { + // Fail fast + callback && callback(null, defaultPermission); + return; + } + ACL.find({where: {principalType: principalType, principalId: principalId, model: model, property: propertyQuery, accessType: accessTypeQuery}}, function (err, acls) { @@ -142,17 +206,7 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc callback && callback(err); return; } - 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; - if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === ACL.ALL && currentValue.accessType)) { - previousValue.accessType = currentValue.accessType; - } - previousValue.permission = overridePermission(previousValue.permission, currentValue.permission); - } - return previousValue; - }, {principalType: principalType, principalId: principalId, model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}); + var resolvedPermission = resolvePermission(acls, defaultPermission); callback && callback(null, resolvedPermission); }); }; diff --git a/test/acl.test.js b/test/acl.test.js index 1c5c4d91..0736a406 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -20,15 +20,15 @@ describe('security scopes', function () { // console.log(Scope.relations); - Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { + Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) { // console.log(scope); - ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'user', property: ACL.ALL, + ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'User', property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, resource) { // console.log(resource); - Scope.checkPermission('user', 'user', ACL.ALL, ACL.ALL, checkResult); - Scope.checkPermission('user', 'user', 'name', ACL.ALL, checkResult); - Scope.checkPermission('user', 'user', 'name', ACL.READ, checkResult); + Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, checkResult); + Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, checkResult); + Scope.checkPermission('userScope', 'User', 'name', ACL.READ, checkResult); }); }); @@ -41,25 +41,25 @@ describe('security scopes', function () { // console.log(Scope.relations); - Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { + Scope.create({name: 'userScope', description: 'access user information'}, function (err, scope) { // console.log(scope); ACL.create({principalType: ACL.SCOPE, principalId: scope.id, - model: 'user', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, + model: 'User', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, function (err, resource) { ACL.create({principalType: ACL.SCOPE, principalId: scope.id, - model: 'user', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY}, + model: 'User', property: 'name', accessType: ACL.WRITE, permission: ACL.DENY}, function (err, resource) { // console.log(resource); - Scope.checkPermission('user', 'user', ACL.ALL, ACL.ALL, function (err, perm) { + Scope.checkPermission('userScope', 'User', ACL.ALL, ACL.ALL, function (err, perm) { assert(perm.permission === ACL.DENY); // because name.WRITE == DENY }); - Scope.checkPermission('user', 'user', 'name', ACL.ALL, function (err, perm) { + Scope.checkPermission('userScope', 'User', 'name', ACL.ALL, function (err, perm) { assert(perm.permission === ACL.DENY); // because name.WRITE == DENY }); - Scope.checkPermission('user', 'user', 'name', ACL.READ, function (err, perm) { + Scope.checkPermission('userScope', 'User', 'name', ACL.READ, function (err, perm) { assert(perm.permission === ACL.ALLOW); }); - Scope.checkPermission('user', 'user', 'name', ACL.WRITE, function (err, perm) { + Scope.checkPermission('userScope', 'User', 'name', ACL.WRITE, function (err, perm) { assert(perm.permission === ACL.DENY); }); }); @@ -76,17 +76,17 @@ describe('security ACLs', function () { var ds = loopback.createDataSource({connector: loopback.Memory}); ACL.attachTo(ds); - ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: ACL.ALL, + ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) { - ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: ACL.ALL, + ACL.create({principalType: ACL.USER, principalId: 'u001', model: 'User', property: ACL.ALL, accessType: ACL.READ, permission: ACL.DENY}, function (err, acl) { - ACL.checkPermission('user', 'u001', 'user', 'name', ACL.READ, function (err, perm) { + ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.READ, function (err, perm) { assert(perm.permission === ACL.DENY); }); - ACL.checkPermission('user', 'u001', 'user', 'name', ACL.ALL, function (err, perm) { + ACL.checkPermission(ACL.USER, 'u001', 'User', 'name', ACL.ALL, function (err, perm) { assert(perm.permission === ACL.DENY); }); @@ -96,6 +96,44 @@ describe('security ACLs', function () { }); + it("should honor static ACLs from the model", function () { + var ds = loopback.createDataSource({connector: loopback.Memory}); + 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.acls = [ + {principalType: ACL.USER, principalId: 'u001', accessType: ACL.ALL, permission: ACL.ALLOW} + ]; + */ + ACL.attachTo(ds); + + 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, 'u001', 'Customer', 'name', ACL.ALL, function (err, perm) { + assert(perm.permission === ACL.DENY); + }); + + }); + + }); From 44dfe346475071ee491313ee6b5d5256d00d3b69 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 15 Nov 2013 10:08:49 -0800 Subject: [PATCH 2/2] Allow ACLs for methods/relations --- lib/models/acl.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 61fe4453..028c1ead 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -165,7 +165,7 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc modelClass.settings.acls.forEach(function(acl) { staticACLs.push({ model: model, - property: ACL.ALL, + property: acl.property || ACL.ALL, principalType: acl.principalType, principalId: acl.principalId, // TODO: Should it be a name? accessType: acl.accessType, @@ -173,7 +173,11 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc }); }); } - var prop = modelClass && modelClass.definition.properties[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({