From bee8a3b0229224f46e988b7d2016cbb01988f3db Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 20 Nov 2013 13:31:30 -0800 Subject: [PATCH] Add checkAccess for subject and token --- lib/models/acl.js | 226 +++++++++++++++++++++++++++++++++++--------- lib/models/model.js | 31 +++++- lib/models/role.js | 8 ++ package.json | 3 +- test/acl.test.js | 112 +++++++++++++++++++++- 5 files changed, 333 insertions(+), 47 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 028c1ead..d2b99be4 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -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, diff --git a/lib/models/model.js b/lib/models/model.js index 04f2372c..72a22799 100644 --- a/lib/models/model.js +++ b/lib/models/model.js @@ -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(); + diff --git a/lib/models/role.js b/lib/models/role.js index d70fe008..7306f018 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -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); diff --git a/package.json b/package.json index 413fc9d7..687ee09a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/acl.test.js b/test/acl.test.js index 2df75544..46035992 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -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); + }); + + }); + + }); + }); + }); + }); + }); });