From f9849454e9d63c560f0920874a8bb914b3bc3994 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 28 Oct 2013 10:44:05 -0700 Subject: [PATCH 01/13] Update ACL model --- lib/models/acl.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index c304e362..c91ffdcf 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -31,16 +31,44 @@ Map to oAuth 2.0 scopes */ +/* +var ACLEntrySchema = { + principal: String, // Application/User/Role + action: String, // READ/WRITE or method name + allowed: Boolean // Positive or negative +} + +var ACLSchema = { + publicReadAccess: Boolean, + publicWriteAccess: Boolean, + permissions: [ACLEntrySchema], + created: Date, + modified: Date +} + + +var AccessLevel = [ + NotAllowed: 'Not Allowed', // Disabled + // 'Allowed when Logged-in', + Owner: 'Allow to Object Owner', + Role: 'Users defined in a Role', + Related: 'Any User with a relationship to the object', + Authenticated: 'Allow to Any Logged In User', + 'Open' +]; +*/ + var ACLSchema = { model: String, // The model name properties: [String], // A list of property names methods: [String], // A list of methods + users: [String], // A list of users roles: [String], // A list of roles permission: {type: String, enum: ['Allow', 'Deny']}, // Allow/Deny status: String, // Enabled/disabled created: Date, modified: Date -} +}; // readAccess, writeAccess --> public, userId, role @@ -48,4 +76,4 @@ module.exports = function(dataSource) { dataSource = dataSource || new require('loopback-datasource-juggler').ModelBuilder(); var ACL = dataSource.define('ACL', ACLSchema); return ACL; -} \ No newline at end of file +}; \ No newline at end of file From 492aca7724bda232fe76b41c7228cf733ade091c Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 4 Nov 2013 13:19:02 -0800 Subject: [PATCH 02/13] Update acl/role models --- lib/models/acl.js | 78 +++++++++++++++++++++++++--------------------- lib/models/role.js | 20 +++++++----- 2 files changed, 55 insertions(+), 43 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index c91ffdcf..b3edeb83 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -31,49 +31,55 @@ Map to oAuth 2.0 scopes */ -/* -var ACLEntrySchema = { - principal: String, // Application/User/Role - action: String, // READ/WRITE or method name - allowed: Boolean // Positive or negative -} +var loopback = require('loopback'); -var ACLSchema = { +var ACLEntrySchema = { + /** + * Type of the principal - Application/User/Role + */ + principalType: String, + /** + * Id of the principal - such as appId, userId or roleId + */ + principalId: String, + + /** + * Name of the access type - READ/WRITE/EXEC + */ + accessType: String, + + /** + * ALARM - Generate an alarm, in a system dependent way, the access specified in the permissions component of the ACL entry. + * ALLOW - Explicitly grants access to the resource. + * AUDIT - Log, in a system dependent way, the access specified in the permissions component of the ACL entry. + * DENY - Explicitly denies access to the resource. + */ + permission: String +}; + +var AccessSchema = { publicReadAccess: Boolean, publicWriteAccess: Boolean, - permissions: [ACLEntrySchema], - created: Date, - modified: Date -} - - -var AccessLevel = [ - NotAllowed: 'Not Allowed', // Disabled - // 'Allowed when Logged-in', - Owner: 'Allow to Object Owner', - Role: 'Users defined in a Role', - Related: 'Any User with a relationship to the object', - Authenticated: 'Allow to Any Logged In User', - 'Open' -]; -*/ + publicExecAccess: Boolean, + permissions: [ACLEntrySchema] +}; var ACLSchema = { - model: String, // The model name - properties: [String], // A list of property names - methods: [String], // A list of methods - users: [String], // A list of users - roles: [String], // A list of roles - permission: {type: String, enum: ['Allow', 'Deny']}, // Allow/Deny - status: String, // Enabled/disabled + /** + * Resource + */ + model: String, // The name of the model + property: String, // The name of the property + method: String, // The name of the method + + access: AccessSchema, // The access + + status: String, created: Date, modified: Date }; -// readAccess, writeAccess --> public, userId, role -module.exports = function(dataSource) { - dataSource = dataSource || new require('loopback-datasource-juggler').ModelBuilder(); - var ACL = dataSource.define('ACL', ACLSchema); - return ACL; -}; \ No newline at end of file +var ACL = loopback.createModel('ACL', ACLSchema); + +module.exports = ACL; \ No newline at end of file diff --git a/lib/models/role.js b/lib/models/role.js index 9a18e418..e39ef688 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -1,6 +1,8 @@ +var loopback = require('loopback'); + // Role model var RoleSchema = { - id: {type: String, required: true}, // Id + id: {type: String, id: true}, // Id name: {type: String, required: true}, // The name of a role description: String, // Description roles: [String], // A role can be an aggregate of other roles @@ -9,10 +11,14 @@ var RoleSchema = { // Timestamps created: {type: Date, default: Date}, modified: {type: Date, default: Date} -} +}; + +var Role = loopback.createModel('Role', RoleSchema); + +module.exports = Role; + +Role.OWNER ='$owner'; // owner of the object +Role.RELATED = "$related"; // any User with a relationship to the object +Role.AUTHENTICATED = "$authenticated"; // authenticated user +Role.EVERYONE = "$everyone"; // everyone -module.exports = function(dataSource) { - dataSource = dataSource || new require('loopback-datasource-juggler').ModelBuilder(); - var Role = dataSource.define('Role', RoleSchema); - return Role; -} From 67b934357be77167e7f31bc7a1a4a2992e5ff12a Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sat, 9 Nov 2013 22:22:16 -0800 Subject: [PATCH 03/13] Start to build the ACL models --- lib/loopback.js | 2 +- lib/models/acl.js | 160 ++++++++++++++++++++++++++++++--------------- lib/models/role.js | 42 ++++++++---- test/acl.test.js | 28 ++++++++ test/role.test.js | 41 ++++++++++++ 5 files changed, 210 insertions(+), 63 deletions(-) create mode 100644 test/acl.test.js create mode 100644 test/role.test.js diff --git a/lib/loopback.js b/lib/loopback.js index 423a3f99..328f2ffa 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -90,7 +90,7 @@ loopback.errorHandler.title = 'Loopback'; */ loopback.createDataSource = function (name, options) { - var ds = new DataSource(name, options); + var ds = new DataSource(name, options, loopback.Model.dataSource); ds.createModel = function (name, properties, settings) { var ModelCtor = loopback.createModel(name, properties, settings); ModelCtor.attachTo(ds); diff --git a/lib/models/acl.js b/lib/models/acl.js index b3edeb83..b639d31a 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -1,14 +1,14 @@ /** -Schema ACL options + Schema ACL options -Object level permissions, for example, an album owned by a user + Object level permissions, for example, an album owned by a user -Factors to be authorized against: + Factors to be authorized against: -* model name: Album -* model instance properties: userId of the album, friends, shared -* methods -* app and/or user ids/roles + * model name: Album + * model instance properties: userId of the album, friends, shared + * methods + * app and/or user ids/roles ** loggedIn ** roles ** userId @@ -17,69 +17,127 @@ Factors to be authorized against: ** everyone ** relations: owner/friend/granted -Class level permissions, for example, Album + Class level permissions, for example, Album * model name: Album * methods -URL/Route level permissions + URL/Route level permissions * url pattern * application id * ip addresses * http headers -Map to oAuth 2.0 scopes + Map to oAuth 2.0 scopes -*/ + */ -var loopback = require('loopback'); +var loopback = require('../loopback'); -var ACLEntrySchema = { - /** - * Type of the principal - Application/User/Role - */ - principalType: String, - /** - * Id of the principal - such as appId, userId or roleId - */ - principalId: String, - - /** - * Name of the access type - READ/WRITE/EXEC - */ - accessType: String, - - /** - * ALARM - Generate an alarm, in a system dependent way, the access specified in the permissions component of the ACL entry. - * ALLOW - Explicitly grants access to the resource. - * AUDIT - Log, in a system dependent way, the access specified in the permissions component of the ACL entry. - * DENY - Explicitly denies access to the resource. - */ - permission: String +var ScopeSchema = { + name: {type: String, required: true}, + description: String }; -var AccessSchema = { - publicReadAccess: Boolean, - publicWriteAccess: Boolean, - publicExecAccess: Boolean, - permissions: [ACLEntrySchema] +var ScopeResourceAccessSchema = { + model: String, // The name of the model + property: String, // The name of the property, method, scope, or relation + + /** + * Name of the access type - READ/WRITE/EXEC + */ + accessType: String, + + /** + * ALARM - Generate an alarm, in a system dependent way, the access specified in the permissions component of the ACL entry. + * ALLOW - Explicitly grants access to the resource. + * AUDIT - Log, in a system dependent way, the access specified in the permissions component of the ACL entry. + * DENY - Explicitly denies access to the resource. + */ + permission: String, + scopeId: Number }; +var ScopeResourceAccess = loopback.createModel('ScopeResourceAccess', ScopeResourceAccessSchema, { + relations: { + scope: { + type: 'belongsTo', + model: 'Scope', + foreignKey: 'scopeId' + } + } +}); + +/** + * Scope has many resource access entries + * @type {createModel|*} + */ +var Scope = loopback.createModel('Scope', ScopeSchema, { + relations: { + resources: { + type: 'hasMany', + model: 'ScopeResourceAccess', + foreignKey: 'scopeId' + } + } +}); + var ACLSchema = { - /** - * Resource - */ - model: String, // The name of the model - property: String, // The name of the property - method: String, // The name of the method + model: String, // The name of the model + property: String, // The name of the property, method, scope, or relation - access: AccessSchema, // The access + /** + * Name of the access type - READ/WRITE/EXEC + */ + accessType: String, - status: String, - created: Date, - modified: Date + /** + * ALARM - Generate an alarm, in a system dependent way, the access specified in the permissions component of the ACL entry. + * ALLOW - Explicitly grants access to the resource. + * AUDIT - Log, in a system dependent way, the access specified in the permissions component of the ACL entry. + * DENY - Explicitly denies access to the resource. + */ + permission: String, + /** + * Type of the principal - Application/User/Role + */ + principalType: String, + /** + * Id of the principal - such as appId, userId or roleId + */ + principalId: String }; - var ACL = loopback.createModel('ACL', ACLSchema); -module.exports = ACL; \ No newline at end of file +module.exports = { + ACL: ACL, + Scope: Scope, + ScopeResourceAccess: ScopeResourceAccess +}; + +Scope.isAllowed = function (scope, model, property, accessType, callback) { + Scope.findOne({where: {name: scope}}, function (err, scope) { + if (err) { + callback && callback(err); + } else { + scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) + { + if (err) { + callback && callback(err); + } else { + console.log('Resources: ', resources); + for (var r = 0; r < resources.length; r++) { + if (resources[r].permission === 'Allow') { + callback && callback(null, true); + return; + } + } + callback && callback(null, false); + } + + } + ) + ; + } + }); +}; \ No newline at end of file diff --git a/lib/models/role.js b/lib/models/role.js index e39ef688..9c1170d9 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -1,23 +1,43 @@ -var loopback = require('loopback'); +var loopback = require('../loopback'); // Role model var RoleSchema = { - id: {type: String, id: true}, // Id - name: {type: String, required: true}, // The name of a role - description: String, // Description - roles: [String], // A role can be an aggregate of other roles - users: [String], // A role contains a list of user ids + id: {type: String, id: true}, // Id + name: {type: String, required: true}, // The name of a role + description: String, // Description + // roles: [String], // A role can be an aggregate of other roles + // users: [String], // A role contains a list of user ids - // Timestamps - created: {type: Date, default: Date}, - modified: {type: Date, default: Date} + parent: String, + // Timestamps + created: {type: Date, default: Date}, + modified: {type: Date, default: Date} }; -var Role = loopback.createModel('Role', RoleSchema); +var Role = loopback.createModel('Role', RoleSchema, { + relations: { + roles: { + type: 'hasMany', + model: 'Role', + foreignKey: 'parent' + }, + users: { + type: 'hasAndBelongsToMany', + model: 'user', + foreignKey: 'userId' + }, + applications: { + type: 'hasAndBelongsToMany', + model: 'Application', + foreignKey: 'appId' + } + } +}); module.exports = Role; -Role.OWNER ='$owner'; // owner of the object +// Special roles +Role.OWNER = '$owner'; // owner of the object Role.RELATED = "$related"; // any User with a relationship to the object Role.AUTHENTICATED = "$authenticated"; // authenticated user Role.EVERYONE = "$everyone"; // everyone diff --git a/test/acl.test.js b/test/acl.test.js new file mode 100644 index 00000000..15ca5720 --- /dev/null +++ b/test/acl.test.js @@ -0,0 +1,28 @@ +var assert = require('assert'); +var loopback = require('../index'); +var acl = require('../lib/models/acl'); +var User = loopback.User; + +describe('security scopes', function () { + + it("should allow access to models", function () { + var ds = loopback.createDataSource({connector: loopback.Memory}); + acl.Scope.attachTo(ds); + acl.ScopeResourceAccess.attachTo(ds); + + // console.log(acl.Scope.relations); + + acl.Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { + console.log(scope); + scope.resources.create({model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, resource) { + console.log(resource); + acl.Scope.isAllowed('user', 'user', '*', '*', console.log); + }); + }); + + }); + +}); + + + diff --git a/test/role.test.js b/test/role.test.js new file mode 100644 index 00000000..81cdeffd --- /dev/null +++ b/test/role.test.js @@ -0,0 +1,41 @@ +var assert = require('assert'); +var loopback = require('../index'); +var Role = require('../lib/models/role'); +var User = loopback.User; + +describe('security models', function () { + + describe('roles', function () { + + it("Defines role/role relations", function () { + var ds = loopback.createDataSource({connector: loopback.Memory}); + Role.attachTo(ds); + + Role.create({name: 'user'}, function (err, role) { + role.roles.create({name: 'admin'}, function (err, role2) { + Role.find(console.log); + role.roles(console.log); + }); + }); + + }); + + it("Defines role/user relations", function () { + var ds = loopback.createDataSource({connector: loopback.Memory}); + User.attachTo(ds); + Role.attachTo(ds); + + Role.create({name: 'user'}, function (err, role) { + role.users.create({name: 'Raymond'}, function (err, user) { + console.log('User: ', user); + Role.find(console.log); + role.users(console.log); + }); + }); + + }); + }); +}); + + + From 48a02427113efcd0b48b019c39b00cee64f5ad82 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 11 Nov 2013 22:16:51 -0800 Subject: [PATCH 04/13] Define the models/relations for ACL --- lib/models/acl.js | 98 ++++++++++++++++++++++------ lib/models/role.js | 156 +++++++++++++++++++++++++++++++++++++++------ test/acl.test.js | 58 ++++++++++++++--- test/model.test.js | 33 ++++++++++ test/role.test.js | 43 +++++++++---- 5 files changed, 329 insertions(+), 59 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index b639d31a..58282c4b 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -33,12 +33,15 @@ var loopback = require('../loopback'); +/** + * Schema for Scope which represents the permissions that are granted to client applications by the resource owner + */ var ScopeSchema = { name: {type: String, required: true}, description: String }; -var ScopeResourceAccessSchema = { +var ScopeACLSchema = { model: String, // The name of the model property: String, // The name of the property, method, scope, or relation @@ -57,7 +60,7 @@ var ScopeResourceAccessSchema = { scopeId: Number }; -var ScopeResourceAccess = loopback.createModel('ScopeResourceAccess', ScopeResourceAccessSchema, { +var ScopeACL = loopback.createModel('ScopeACL', ScopeACLSchema, { relations: { scope: { type: 'belongsTo', @@ -68,6 +71,10 @@ var ScopeResourceAccess = loopback.createModel('ScopeResourceAccess', ScopeResou }); /** + * Resource owner grants/delegates permissions to client applications + * + * For a protected resource, does the client application have the authorization from the resource owner (user or system)? + * * Scope has many resource access entries * @type {createModel|*} */ @@ -75,12 +82,21 @@ var Scope = loopback.createModel('Scope', ScopeSchema, { relations: { resources: { type: 'hasMany', - model: 'ScopeResourceAccess', + model: 'ScopeACL', foreignKey: 'scopeId' } } }); +/** + * System grants permissions to principals (users/applications, can be grouped into roles). + * + * Protected resource: the model data and operations (model/property/method/relation/…) + * + * For a given principal, such as client application and/or user, is it allowed to access (read/write/execute) + * the protected resource? + * + */ var ACLSchema = { model: String, // The name of the model property: String, // The name of the property, method, scope, or relation @@ -112,32 +128,72 @@ var ACL = loopback.createModel('ACL', ACLSchema); module.exports = { ACL: ACL, Scope: Scope, - ScopeResourceAccess: ScopeResourceAccess + ScopeACL: ScopeACL }; -Scope.isAllowed = function (scope, model, property, accessType, callback) { +/** + * Check if the given principal is allowed to access the model/property + * @param principalType + * @param principalId + * @param model + * @param property + * @param accessType + * @param callback + */ +ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) { + ACL.find({where: {principalType: principalType, principalId: principalId, + model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, + function (err, acls) { + if (err) { + callback && callback(err); + return; + } + var resolvedPermission = acls.reduce(function (previousValue, currentValue, index, array) { + // If the property is the same or the previous one is '*' (ALL) + if (previousValue.property === currentValue.property || (previousValue.property === '*' && currentValue.property)) { + previousValue.property = currentValue.property; + if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === '*' && currentValue.accessType)) { + previousValue.accessType = currentValue.accessType; + } + } + return previousValue; + }, {principalType: principalType, principalId: principalId, model: model, property: '*', accessType: '*', permission: 'Allow'}); + callback && callback(resolvedPermission); + }); +}; + +/** + * Check if the given scope is allowed to access the model/property + * @param scope + * @param model + * @param property + * @param accessType + * @param callback + */ +Scope.checkPermission = function (scope, model, property, accessType, callback) { Scope.findOne({where: {name: scope}}, function (err, scope) { if (err) { callback && callback(err); } else { - scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) - { - if (err) { - callback && callback(err); - } else { - console.log('Resources: ', resources); - for (var r = 0; r < resources.length; r++) { - if (resources[r].permission === 'Allow') { - callback && callback(null, true); - return; - } + scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) { + if (err) { + callback && callback(err); + return; } - callback && callback(null, false); + // Try to resolve the permission + var resolvedPermission = resources.reduce(function (previousValue, currentValue, index, array) { + // If the property is the same or the previous one is '*' (ALL) + if (previousValue.property === currentValue.property || (previousValue.property === '*' && currentValue.property)) { + previousValue.property = currentValue.property; + if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === '*' && currentValue.accessType)) { + previousValue.accessType = currentValue.accessType; + } + } + return previousValue; + }, {model: model, property: '*', accessType: '*', permission: 'Allow'}); + callback && callback(resolvedPermission); } - - } - ) - ; + ); } }); }; \ No newline at end of file diff --git a/lib/models/role.js b/lib/models/role.js index 9c1170d9..b0aaf58f 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -5,36 +5,112 @@ var RoleSchema = { id: {type: String, id: true}, // Id name: {type: String, required: true}, // The name of a role description: String, // Description - // roles: [String], // A role can be an aggregate of other roles - // users: [String], // A role contains a list of user ids - parent: String, // Timestamps created: {type: Date, default: Date}, modified: {type: Date, default: Date} }; -var Role = loopback.createModel('Role', RoleSchema, { +/** + * Map principals to roles + */ +var RoleMappingSchema = { + id: {type: String, id: true}, // Id + roleId: String, // The role id + principalType: String, // The principal type, such as user, application, or role + principalId: String // The principal id +}; + +var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, { relations: { - roles: { - type: 'hasMany', + role: { + type: 'belongsTo', model: 'Role', - foreignKey: 'parent' - }, - users: { - type: 'hasAndBelongsToMany', - model: 'user', - foreignKey: 'userId' - }, - applications: { - type: 'hasAndBelongsToMany', - model: 'Application', - foreignKey: 'appId' + foreignKey: 'roleId' } } }); -module.exports = Role; +RoleMapping.prototype.application = function(callback) { + if(this.principalType === 'application') { + loopback.Application.findById(this.principalId, callback); + } else { + process.nextTick(function() { + callback && callback(null, null); + }); + } +}; + +RoleMapping.prototype.user = function(callback) { + if(this.principalType === 'user') { + loopback.User.findById(this.principalId, callback); + } else { + process.nextTick(function() { + callback && callback(null, null); + }); + } +}; + +RoleMapping.prototype.childRole = function(callback) { + if(this.principalType === 'role') { + loopback.User.findById(this.principalId, callback); + } else { + process.nextTick(function() { + callback && callback(null, null); + }); + } +}; + +var Role = loopback.createModel('Role', RoleSchema, { + relations: { + principals: { + type: 'hasMany', + model: 'RoleMapping', + foreignKey: 'roleId' + } + } +}); + + +Role.once('dataSourceAttached', function() { + Role.prototype.users = function(callback) { + RoleMapping.find({where: {roleId: this.id, principalType: 'user'}}, function(err, mappings) { + if(err) { + callback && callback(err); + return; + } + return mappings.map(function(m) { + return m.principalId; + }); + }); + }; + + Role.prototype.applications = function(callback) { + RoleMapping.find({where: {roleId: this.id, principalType: 'application'}}, function(err, mappings) { + if(err) { + callback && callback(err); + return; + } + return mappings.map(function(m) { + return m.principalId; + }); + }); + }; + + Role.prototype.roles = function(callback) { + RoleMapping.find({where: {roleId: this.id, principalType: 'role'}}, function(err, mappings) { + if(err) { + callback && callback(err); + return; + } + return mappings.map(function(m) { + return m.principalId; + }); + }); + }; + +}); + // Special roles Role.OWNER = '$owner'; // owner of the object @@ -42,3 +118,47 @@ Role.RELATED = "$related"; // any User with a relationship to the object Role.AUTHENTICATED = "$authenticated"; // authenticated user Role.EVERYONE = "$everyone"; // everyone +/** + * Check if a given principal is in the role + * + * @param principalType + * @param principalId + * @param role + * @param callback + */ +Role.isInRole = function(principalType, principalId, role, callback) { + Role.findOne({where: {name: role}}, function(err, role) { + if(err) { + callback && callback(err); + return; + } + RoleMapping.exists({where: {roleId: role.id, principalType: principalType, principalId: principalId}}, callback); + }); +}; + +/** + * List roles for a given principal + * @param principalType + * @param principalId + * @param role + * @param callback + */ +Role.getRoles = function(principalType, principalId, role, callback) { + RoleMapping.find({where: {principalType: principalType, principalId: principalId}},function(err, mappings) { + if(err) { + callback && callback(err); + return; + } + var roles = []; + mappings.forEach(function(m) { + roles.push(m.roleId); + }); + callback && callback(null, roles); + }); +}; + +module.exports = { + Role: Role, + RoleMapping: RoleMapping +}; + diff --git a/test/acl.test.js b/test/acl.test.js index 15ca5720..e5d0731a 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -1,27 +1,69 @@ var assert = require('assert'); var loopback = require('../index'); var acl = require('../lib/models/acl'); +var Scope = acl.Scope; +var ACL = acl.ACL; +var ScopeACL = acl.ScopeACL; var User = loopback.User; describe('security scopes', function () { - it("should allow access to models", function () { + it("should allow access to models for the given scope by wildcard", function () { var ds = loopback.createDataSource({connector: loopback.Memory}); - acl.Scope.attachTo(ds); - acl.ScopeResourceAccess.attachTo(ds); + Scope.attachTo(ds); + ScopeACL.attachTo(ds); - // console.log(acl.Scope.relations); + // console.log(Scope.relations); - acl.Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { - console.log(scope); + Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { + // console.log(scope); scope.resources.create({model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, resource) { - console.log(resource); - acl.Scope.isAllowed('user', 'user', '*', '*', console.log); + // console.log(resource); + Scope.checkPermission('user', 'user', '*', '*', console.log); + Scope.checkPermission('user', 'user', 'name', '*', console.log); + Scope.checkPermission('user', 'user', 'name', 'Read', console.log); }); }); }); + it("should allow access to models for the given scope", function () { + var ds = loopback.createDataSource({connector: loopback.Memory}); + Scope.attachTo(ds); + ScopeACL.attachTo(ds); + + // console.log(Scope.relations); + + Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { + // console.log(scope); + scope.resources.create({model: 'user', property: 'name', accessType: 'Read', permission: 'Allow'}, function (err, resource) { + // console.log(resource); + Scope.checkPermission('user', 'user', '*', '*', console.log); + Scope.checkPermission('user', 'user', 'name', '*', console.log); + Scope.checkPermission('user', 'user', 'name', 'Read', console.log); + }); + }); + + }); + +}); + +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); + + // console.log(Scope.relations); + + ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, acl) { + + ACL.checkPermission('user', 'u001', 'user', 'u001', 'Read', console.log); + + }); + + }); + }); diff --git a/test/model.test.js b/test/model.test.js index 1ac72441..d0686fee 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -512,6 +512,39 @@ describe('Model', function() { }); }); + describe('Model.extend() events', function() { + it('create isolated emitters for subclasses', function() { + var User1 = loopback.createModel('User1', { + 'first': String, + 'last': String + }); + + var User2 = loopback.createModel('User2', { + 'name': String + }); + + var user1Triggered = false; + User1.once('x', function(event) { + user1Triggered = true; + }); + + + var user2Triggered = false; + User2.once('x', function(event) { + user2Triggered = true; + }); + + assert(User1.once !== User2.once); + assert(User1.once !== loopback.Model.once); + + User1.emit('x', User1); + + assert(user1Triggered); + assert(!user2Triggered); + }); + + }); + // describe('Model.hasAndBelongsToMany()', function() { // it("TODO: implement / document", function(done) { // /* example - diff --git a/test/role.test.js b/test/role.test.js index 81cdeffd..93797e2f 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -1,39 +1,58 @@ var assert = require('assert'); var loopback = require('../index'); -var Role = require('../lib/models/role'); +var role = require('../lib/models/role'); +var Role = role.Role; +var RoleMapping = role.RoleMapping; var User = loopback.User; describe('security models', function () { describe('roles', function () { - it("Defines role/role relations", function () { + it("should define role/role relations", function () { var ds = loopback.createDataSource({connector: loopback.Memory}); Role.attachTo(ds); + RoleMapping.attachTo(ds); - Role.create({name: 'user'}, function (err, role) { - role.roles.create({name: 'admin'}, function (err, role2) { - Role.find(console.log); - role.roles(console.log); + Role.create({name: 'user'}, function (err, userRole) { + Role.create({name: 'admin'}, function (err, adminRole) { + userRole.principals.create({principalType: 'role', principalId: adminRole.id}, function (err, mapping) { + Role.find(function(err, roles) { + assert.equal(roles.length, 2); + }); + RoleMapping.find(function(err, mappings) { + assert.equal(mappings.length, 1); + }); + userRole.principals(function(err, principals) { + assert.equal(principals.length, 1); + }); + userRole.roles(function(err, roles) { + assert.equal(roles.length, 1); + }); + }); }); }); }); - it("Defines role/user relations", function () { + it("should define role/user relations", function () { var ds = loopback.createDataSource({connector: loopback.Memory}); User.attachTo(ds); Role.attachTo(ds); - Role.create({name: 'user'}, function (err, role) { - role.users.create({name: 'Raymond'}, function (err, user) { - console.log('User: ', user); - Role.find(console.log); - role.users(console.log); + User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { + console.log('User: ', user.id); + Role.create({name: 'userRole'}, function (err, role) { + role.principals.create({principalType: 'user', principalId: user.id}, function (err, p) { + Role.find(console.log); + role.principals(console.log); + role.users(console.log); + }); }); }); }); + }); }); From c3a1a8515980533d3490e864eac77e2a25bb3990 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 12 Nov 2013 10:10:32 -0800 Subject: [PATCH 05/13] Add constants and more tests --- lib/models/acl.js | 67 ++++++++++++++++++++++-------- lib/models/role.js | 92 +++++++++++++++++++++++++---------------- test/acl.test.js | 29 ++++++++----- test/role.test.js | 101 +++++++++++++++++++++++++++------------------ 4 files changed, 186 insertions(+), 103 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 58282c4b..819d1985 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -125,12 +125,36 @@ var ACLSchema = { var ACL = loopback.createModel('ACL', ACLSchema); -module.exports = { - ACL: ACL, - Scope: Scope, - ScopeACL: ScopeACL +ACL.ALL = '*'; + +ACL.ALLOW = 'ALLOW'; +ACL.ALARM = 'ALARM'; +ACL.AUDIT = 'AUDIT'; +ACL.DENY = 'DENY'; + +ACL.READ = 'READ'; +ACL.WRITE = 'WRITE'; +ACL.EXECUTE = 'EXECUTE'; + +ACL.USER = 'USER'; +ACL.APP = ACL.APPLICATION = 'APP'; +ACL.ROLE = 'ROLE'; + +var permissionOrder = { + ALLOW: 1, + ALARM: 2, + AUDIT: 3, + 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; +} + /** * Check if the given principal is allowed to access the model/property * @param principalType @@ -142,23 +166,24 @@ module.exports = { */ ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) { ACL.find({where: {principalType: principalType, principalId: principalId, - model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, + model: model, property: {inq: [property, ACL.ALL]}, accessType: {inq: [accessType, ACL.ALL]}}}, function (err, acls) { if (err) { callback && callback(err); return; } var resolvedPermission = acls.reduce(function (previousValue, currentValue, index, array) { - // If the property is the same or the previous one is '*' (ALL) - if (previousValue.property === currentValue.property || (previousValue.property === '*' && currentValue.property)) { + // 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 === '*' && currentValue.accessType)) { + if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === ACL.ALL && currentValue.accessType)) { previousValue.accessType = currentValue.accessType; } + currentValue.permission = overridePermission(previousValue.permission, currentValue.permission); } return previousValue; - }, {principalType: principalType, principalId: principalId, model: model, property: '*', accessType: '*', permission: 'Allow'}); - callback && callback(resolvedPermission); + }, {principalType: principalType, principalId: principalId, model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}); + callback && callback(null, resolvedPermission); }); }; @@ -175,25 +200,33 @@ Scope.checkPermission = function (scope, model, property, accessType, callback) if (err) { callback && callback(err); } else { - scope.resources({where: {model: model, property: {inq: [property, '*']}, accessType: {inq: [accessType, '*']}}}, function (err, resources) { + scope.resources({where: {model: model, property: {inq: [property, ACL.ALL]}, accessType: {inq: [accessType, ACL.ALL]}}}, function (err, resources) { if (err) { callback && callback(err); return; } // Try to resolve the permission var resolvedPermission = resources.reduce(function (previousValue, currentValue, index, array) { - // If the property is the same or the previous one is '*' (ALL) - if (previousValue.property === currentValue.property || (previousValue.property === '*' && currentValue.property)) { + // 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 === '*' && currentValue.accessType)) { + if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === ACL.ALL && currentValue.accessType)) { previousValue.accessType = currentValue.accessType; } + currentValue.permission = overridePermission(previousValue.permission, currentValue.permission); } return previousValue; - }, {model: model, property: '*', accessType: '*', permission: 'Allow'}); - callback && callback(resolvedPermission); + }, {model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}); + callback && callback(null, resolvedPermission); } ); } }); -}; \ No newline at end of file +}; + + +module.exports = { + ACL: ACL, + Scope: Scope, + ScopeACL: ScopeACL +}; diff --git a/lib/models/role.js b/lib/models/role.js index b0aaf58f..4e1d7aa7 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -31,36 +31,56 @@ var RoleMapping = loopback.createModel('RoleMapping', RoleMappingSchema, { } }); -RoleMapping.prototype.application = function(callback) { - if(this.principalType === 'application') { +// Principal types +RoleMapping.USER = 'USER'; +RoleMapping.APP = RoleMapping.APPLICATION = 'APP'; +RoleMapping.ROLE = 'ROLE'; + +/** + * Get the application principal + * @param callback + */ +RoleMapping.prototype.application = function (callback) { + if (this.principalType === RoleMapping.APPLICATION) { loopback.Application.findById(this.principalId, callback); } else { - process.nextTick(function() { + process.nextTick(function () { callback && callback(null, null); }); } }; -RoleMapping.prototype.user = function(callback) { - if(this.principalType === 'user') { +/** + * Get the user principal + * @param callback + */ +RoleMapping.prototype.user = function (callback) { + if (this.principalType === RoleMapping.USER) { loopback.User.findById(this.principalId, callback); } else { - process.nextTick(function() { + process.nextTick(function () { callback && callback(null, null); }); } }; -RoleMapping.prototype.childRole = function(callback) { - if(this.principalType === 'role') { +/** + * Get the child role principal + * @param callback + */ +RoleMapping.prototype.childRole = function (callback) { + if (this.principalType === RoleMapping.ROLE) { loopback.User.findById(this.principalId, callback); } else { - process.nextTick(function() { + process.nextTick(function () { callback && callback(null, null); }); } }; +/** + * Define the Role model with `hasMany` relation to RoleMapping + */ var Role = loopback.createModel('Role', RoleSchema, { relations: { principals: { @@ -71,39 +91,39 @@ var Role = loopback.createModel('Role', RoleSchema, { } }); - -Role.once('dataSourceAttached', function() { - Role.prototype.users = function(callback) { - RoleMapping.find({where: {roleId: this.id, principalType: 'user'}}, function(err, mappings) { - if(err) { +// Set up the connection to users/applications/roles once the model +Role.once('dataSourceAttached', function () { + Role.prototype.users = function (callback) { + RoleMapping.find({where: {roleId: this.id, principalType: RoleMapping.USER}}, function (err, mappings) { + if (err) { callback && callback(err); return; } - return mappings.map(function(m) { + return mappings.map(function (m) { return m.principalId; }); }); }; - Role.prototype.applications = function(callback) { - RoleMapping.find({where: {roleId: this.id, principalType: 'application'}}, function(err, mappings) { - if(err) { + Role.prototype.applications = function (callback) { + RoleMapping.find({where: {roleId: this.id, principalType: RoleMapping.APPLICATION}}, function (err, mappings) { + if (err) { callback && callback(err); return; } - return mappings.map(function(m) { + return mappings.map(function (m) { return m.principalId; }); }); }; - Role.prototype.roles = function(callback) { - RoleMapping.find({where: {roleId: this.id, principalType: 'role'}}, function(err, mappings) { - if(err) { + Role.prototype.roles = function (callback) { + RoleMapping.find({where: {roleId: this.id, principalType: RoleMapping.ROLE}}, function (err, mappings) { + if (err) { callback && callback(err); return; } - return mappings.map(function(m) { + return mappings.map(function (m) { return m.principalId; }); }); @@ -111,7 +131,6 @@ Role.once('dataSourceAttached', function() { }); - // Special roles Role.OWNER = '$owner'; // owner of the object Role.RELATED = "$related"; // any User with a relationship to the object @@ -126,9 +145,9 @@ Role.EVERYONE = "$everyone"; // everyone * @param role * @param callback */ -Role.isInRole = function(principalType, principalId, role, callback) { - Role.findOne({where: {name: role}}, function(err, role) { - if(err) { +Role.isInRole = function (principalType, principalId, role, callback) { + Role.findOne({where: {name: role}}, function (err, role) { + if (err) { callback && callback(err); return; } @@ -138,19 +157,22 @@ Role.isInRole = function(principalType, principalId, role, callback) { /** * List roles for a given principal - * @param principalType - * @param principalId - * @param role - * @param callback + * @param {String} principalType + * @param {String|Number} principalId + * @param {Function} callback + * + * @callback callback + * @param err + * @param {String[]} An array of role ids */ -Role.getRoles = function(principalType, principalId, role, callback) { - RoleMapping.find({where: {principalType: principalType, principalId: principalId}},function(err, mappings) { - if(err) { +Role.getRoles = function (principalType, principalId, callback) { + RoleMapping.find({where: {principalType: principalType, principalId: principalId}}, function (err, mappings) { + if (err) { callback && callback(err); return; } var roles = []; - mappings.forEach(function(m) { + mappings.forEach(function (m) { roles.push(m.roleId); }); callback && callback(null, roles); diff --git a/test/acl.test.js b/test/acl.test.js index e5d0731a..7a9b44fe 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -6,6 +6,11 @@ var ACL = acl.ACL; var ScopeACL = acl.ScopeACL; var User = loopback.User; +function checkResult(err, result) { + // console.log(err, result); + assert(!err); +} + describe('security scopes', function () { it("should allow access to models for the given scope by wildcard", function () { @@ -17,11 +22,12 @@ describe('security scopes', function () { Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { // console.log(scope); - scope.resources.create({model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, resource) { + scope.resources.create({model: 'user', property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}, + function (err, resource) { // console.log(resource); - Scope.checkPermission('user', 'user', '*', '*', console.log); - Scope.checkPermission('user', 'user', 'name', '*', console.log); - Scope.checkPermission('user', 'user', 'name', 'Read', console.log); + 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); }); }); @@ -36,11 +42,12 @@ describe('security scopes', function () { Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { // console.log(scope); - scope.resources.create({model: 'user', property: 'name', accessType: 'Read', permission: 'Allow'}, function (err, resource) { + scope.resources.create({model: 'user', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, + function (err, resource) { // console.log(resource); - Scope.checkPermission('user', 'user', '*', '*', console.log); - Scope.checkPermission('user', 'user', 'name', '*', console.log); - Scope.checkPermission('user', 'user', 'name', 'Read', console.log); + 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); }); }); @@ -54,11 +61,11 @@ describe('security ACLs', function () { var ds = loopback.createDataSource({connector: loopback.Memory}); ACL.attachTo(ds); - // console.log(Scope.relations); - ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: '*', accessType: '*', permission: 'Allow'}, function (err, acl) { + ACL.create({principalType: 'user', principalId: 'u001', model: 'user', property: ACL.ALL, + accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) { - ACL.checkPermission('user', 'u001', 'user', 'u001', 'Read', console.log); + ACL.checkPermission('user', 'u001', 'user', 'u001', ACL.READ, checkResult); }); diff --git a/test/role.test.js b/test/role.test.js index 93797e2f..f94c0cce 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -5,56 +5,77 @@ var Role = role.Role; var RoleMapping = role.RoleMapping; var User = loopback.User; -describe('security models', function () { +function checkResult(err, result) { + // console.log(err, result); + assert(!err); +} - describe('roles', function () { +describe('role model', function () { - it("should define role/role relations", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); - Role.attachTo(ds); - RoleMapping.attachTo(ds); + it("should define role/role relations", function () { + var ds = loopback.createDataSource({connector: 'memory'}); + Role.attachTo(ds); + RoleMapping.attachTo(ds); - Role.create({name: 'user'}, function (err, userRole) { - Role.create({name: 'admin'}, function (err, adminRole) { - userRole.principals.create({principalType: 'role', principalId: adminRole.id}, function (err, mapping) { - Role.find(function(err, roles) { - assert.equal(roles.length, 2); - }); - RoleMapping.find(function(err, mappings) { - assert.equal(mappings.length, 1); - }); - userRole.principals(function(err, principals) { - assert.equal(principals.length, 1); - }); - userRole.roles(function(err, roles) { - assert.equal(roles.length, 1); - }); + Role.create({name: 'user'}, function (err, userRole) { + Role.create({name: 'admin'}, function (err, adminRole) { + userRole.principals.create({principalType: RoleMapping.ROLE, principalId: adminRole.id}, function (err, mapping) { + Role.find(function (err, roles) { + assert.equal(roles.length, 2); + }); + RoleMapping.find(function (err, mappings) { + assert.equal(mappings.length, 1); + assert.equal(mappings[0].principalType, RoleMapping.ROLE); + assert.equal(mappings[0].principalId, adminRole.id); + }); + userRole.principals(function (err, principals) { + assert.equal(principals.length, 1); + }); + userRole.roles(function (err, roles) { + assert.equal(roles.length, 1); }); }); }); - - }); - - it("should define role/user relations", function () { - var ds = loopback.createDataSource({connector: loopback.Memory}); - User.attachTo(ds); - Role.attachTo(ds); - - User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { - console.log('User: ', user.id); - Role.create({name: 'userRole'}, function (err, role) { - role.principals.create({principalType: 'user', principalId: user.id}, function (err, p) { - Role.find(console.log); - role.principals(console.log); - role.users(console.log); - }); - }); - }); - }); }); + + it("should define role/user relations", function () { + var ds = loopback.createDataSource({connector: 'memory'}); + User.attachTo(ds); + Role.attachTo(ds); + RoleMapping.attachTo(ds); + + User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { + // console.log('User: ', user.id); + Role.create({name: 'userRole'}, function (err, role) { + role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) { + Role.find(function(err, roles) { + assert(!err); + assert.equal(roles.length, 1); + assert.equal(roles[0].name, 'userRole'); + }); + role.principals(function(err, principals) { + assert(!err); + // console.log(principals); + assert.equal(principals.length, 1); + assert.equal(principals[0].principalType, RoleMapping.USER); + assert.equal(principals[0].principalId, user.id); + }); + role.users(function(err, users) { + assert(!err); + assert.equal(users.length, 1); + assert.equal(users[0].principalType, RoleMapping.USER); + assert.equal(users[0].principalId, user.id); + }); + }); + }); + }); + + }); + }); + From 0430cd2ae311452364f707674c7fab6649450033 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 12 Nov 2013 10:47:59 -0800 Subject: [PATCH 06/13] Add tests for isInRole and getRoles --- lib/models/role.js | 19 +++++++++++++++---- test/role.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/lib/models/role.js b/lib/models/role.js index 4e1d7aa7..c9a7be37 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -140,18 +140,29 @@ Role.EVERYONE = "$everyone"; // everyone /** * Check if a given principal is in the role * + * @param role * @param principalType * @param principalId - * @param role * @param callback */ -Role.isInRole = function (principalType, principalId, role, callback) { - Role.findOne({where: {name: role}}, function (err, role) { +Role.isInRole = function (role, principalType, principalId, callback) { + Role.findOne({where: {name: role}}, function (err, result) { if (err) { callback && callback(err); return; } - RoleMapping.exists({where: {roleId: role.id, principalType: principalType, principalId: principalId}}, callback); + if(!result) { + callback && callback(null, false); + return; + } + RoleMapping.findOne({where: {roleId: result.id, principalType: principalType, principalId: principalId}}, + function (err, result) { + if (err) { + callback && callback(err); + return; + } + callback && callback(null, !!result); + }); }); }; diff --git a/test/role.test.js b/test/role.test.js index f94c0cce..acd561d9 100644 --- a/test/role.test.js +++ b/test/role.test.js @@ -74,6 +74,47 @@ describe('role model', function () { }); + it("should support getRoles() and isInRole()", function () { + var ds = loopback.createDataSource({connector: 'memory'}); + User.attachTo(ds); + Role.attachTo(ds); + RoleMapping.attachTo(ds); + + User.create({name: 'Raymond', email: 'x@y.com', password: 'foobar'}, function (err, user) { + // console.log('User: ', user.id); + Role.create({name: 'userRole'}, function (err, role) { + role.principals.create({principalType: RoleMapping.USER, principalId: user.id}, function (err, p) { + // Role.find(console.log); + // role.principals(console.log); + Role.isInRole('userRole', RoleMapping.USER, user.id, function(err, exists) { + assert(!err && exists === true); + }); + + Role.isInRole('userRole', RoleMapping.APP, user.id, function(err, exists) { + assert(!err && exists === false); + }); + + Role.isInRole('userRole', RoleMapping.USER, 100, function(err, exists) { + assert(!err && exists === false); + }); + + Role.getRoles(RoleMapping.USER, user.id, function(err, roles) { + assert.equal(roles.length, 1); + assert.equal(roles[0], role.id); + }); + Role.getRoles(RoleMapping.APP, user.id, function(err, roles) { + assert.equal(roles.length, 0); + }); + Role.getRoles(RoleMapping.USER, 100, function(err, roles) { + assert.equal(roles.length, 0); + }); + + }); + }); + }); + + }); + }); From 660ef8975521a65c42ac876467036f679af10035 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 10:02:59 -0800 Subject: [PATCH 07/13] Merge ScopeACL into ACL --- lib/models/acl.js | 46 ++++++---------------------------------------- test/acl.test.js | 10 ++++++---- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 819d1985..91a37297 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -41,34 +41,6 @@ var ScopeSchema = { description: String }; -var ScopeACLSchema = { - model: String, // The name of the model - property: String, // The name of the property, method, scope, or relation - - /** - * Name of the access type - READ/WRITE/EXEC - */ - accessType: String, - - /** - * ALARM - Generate an alarm, in a system dependent way, the access specified in the permissions component of the ACL entry. - * ALLOW - Explicitly grants access to the resource. - * AUDIT - Log, in a system dependent way, the access specified in the permissions component of the ACL entry. - * DENY - Explicitly denies access to the resource. - */ - permission: String, - scopeId: Number -}; - -var ScopeACL = loopback.createModel('ScopeACL', ScopeACLSchema, { - relations: { - scope: { - type: 'belongsTo', - model: 'Scope', - foreignKey: 'scopeId' - } - } -}); /** * Resource owner grants/delegates permissions to client applications @@ -78,15 +50,7 @@ var ScopeACL = loopback.createModel('ScopeACL', ScopeACLSchema, { * Scope has many resource access entries * @type {createModel|*} */ -var Scope = loopback.createModel('Scope', ScopeSchema, { - relations: { - resources: { - type: 'hasMany', - model: 'ScopeACL', - foreignKey: 'scopeId' - } - } -}); +var Scope = loopback.createModel('Scope', ScopeSchema); /** * System grants permissions to principals (users/applications, can be grouped into roles). @@ -139,6 +103,7 @@ ACL.EXECUTE = 'EXECUTE'; ACL.USER = 'USER'; ACL.APP = ACL.APPLICATION = 'APP'; ACL.ROLE = 'ROLE'; +ACL.SCOPE = 'SCOPE'; var permissionOrder = { ALLOW: 1, @@ -200,7 +165,9 @@ Scope.checkPermission = function (scope, model, property, accessType, callback) if (err) { callback && callback(err); } else { - scope.resources({where: {model: model, property: {inq: [property, ACL.ALL]}, accessType: {inq: [accessType, ACL.ALL]}}}, function (err, resources) { + ACL.find({where: {principalType: ACL.SCOPE, principalId: scope.id, + model: model, property: {inq: [property, ACL.ALL]}, + accessType: {inq: [accessType, ACL.ALL]}}}, function (err, resources) { if (err) { callback && callback(err); return; @@ -227,6 +194,5 @@ Scope.checkPermission = function (scope, model, property, accessType, callback) module.exports = { ACL: ACL, - Scope: Scope, - ScopeACL: ScopeACL + Scope: Scope }; diff --git a/test/acl.test.js b/test/acl.test.js index 7a9b44fe..705f6367 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -16,13 +16,14 @@ describe('security scopes', function () { it("should allow access to models for the given scope by wildcard", function () { var ds = loopback.createDataSource({connector: loopback.Memory}); Scope.attachTo(ds); - ScopeACL.attachTo(ds); + ACL.attachTo(ds); // console.log(Scope.relations); Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { // console.log(scope); - scope.resources.create({model: 'user', property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}, + 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); @@ -36,13 +37,14 @@ 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); - ScopeACL.attachTo(ds); + ACL.attachTo(ds); // console.log(Scope.relations); Scope.create({name: 'user', description: 'access user information'}, function (err, scope) { // console.log(scope); - scope.resources.create({model: 'user', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, + ACL.create({principalType: ACL.SCOPE, principalId: scope.id, + model: 'user', property: 'name', accessType: ACL.READ, permission: ACL.ALLOW}, function (err, resource) { // console.log(resource); Scope.checkPermission('user', 'user', ACL.ALL, ACL.ALL, checkResult); From be3234146756016a023889f4f928a8f5e08866f7 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 10:29:33 -0800 Subject: [PATCH 08/13] Add a stub to register role resolvers --- lib/models/role.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/models/role.js b/lib/models/role.js index c9a7be37..5522340b 100644 --- a/lib/models/role.js +++ b/lib/models/role.js @@ -137,6 +137,17 @@ Role.RELATED = "$related"; // any User with a relationship to the object Role.AUTHENTICATED = "$authenticated"; // authenticated user Role.EVERYONE = "$everyone"; // everyone +/** + * Add custom handler for roles + * @param role + * @param resolver The resolver function decides if a principal is in the role dynamically + * + * isInRole(role, context, callback) + */ +Role.registerResolver = function(role, resolver) { + Role.resolvers[role] = resolver; +}; + /** * Check if a given principal is in the role * From be3c40c3d3e8ce90316b45978b647e3ec2bd5a22 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 11:12:33 -0800 Subject: [PATCH 09/13] Add oauth2 related models --- lib/models/oauth2.js | 222 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 lib/models/oauth2.js diff --git a/lib/models/oauth2.js b/lib/models/oauth2.js new file mode 100644 index 00000000..ba82a6e3 --- /dev/null +++ b/lib/models/oauth2.js @@ -0,0 +1,222 @@ +var loopback = require('../loopback'); + +// "OAuth token" +var OAuthToken = loopback.createModel({ + // "access token" + accessToken: { + type: String, + index: { + unique: true + } + }, // key, The string token + clientId: { + type: String, + index: true + }, // The client id + resourceOwner: { + type: String, + index: true + }, // The resource owner (user) id + realm: { + type: String, + index: true + }, // The resource owner realm + issuedAt: { + type: Date, + index: true + }, // The timestamp when the token is issued + expiresIn: Number, // Expiration time in seconds + expiredAt: { + type: Date, + index: { + expires: "1d" + } + }, // The timestamp when the token is expired + scopes: [ String ], // oAuth scopes + parameters: [ + { + name: String, + value: String + } + ], + + authorizationCode: { + type: String, + index: true + }, // The corresponding authorization code that is used to request the + // access token + refreshToken: { + type: String, + index: true + }, // The corresponding refresh token if issued + + tokenType: { + type: String, + enum: [ "Bearer", "MAC" ] + }, // The token type, such as Bearer: + // http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16 + // or MAC: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 + authenticationScheme: String, // HTTP authenticationScheme + hash: String // The SHA-1 hash for +// client-secret/resource-owner-secret-key +}); + +// "OAuth authorization code" +var OAuthAuthorizationCode = loopback.createModel({ + code: { + type: String, + index: { + unique: true + } + }, // key // The string code + clientId: { + type: String, + index: true + }, // The client id + resourceOwner: { + type: String, + index: true + }, // The resource owner (user) id + realm: { + type: String, + index: true + }, // The resource owner realm + + issuedAt: { + type: Date, + index: true + }, // The timestamp when the token is issued + expiresIn: Number, // Expiration time in seconds + expiredAt: { + type: Date, + index: { + expires: "1d" + } + }, // The timestamp when the token is expired + + scopes: [ String ], // oAuth scopes + parameters: [ + { + name: String, + value: String + } + ], + + used: Boolean, // Is it ever used + redirectURI: String, // The redirectURI from the request, we need to + // check if it's identical to the one used for + // access token + hash: String // The SHA-1 hash for +// client-secret/resource-owner-secret-key +}); + +// "OAuth client registration record" +var ClientRegistration = loopback.createModel({ + id: { + type: String, + index: { + unique: true + } + }, + clientId: { + type: String, + index: { + unique: true + } + }, // key; // The client id + clientSecret: String, // The generated client secret + + defaultTokenType: String, + accessLevel: Number, // The access level to scopes, -1: disabled, 0: + // basic, 1..N + disabled: Boolean, + + name: { + type: String, + index: true + }, + email: String, + description: String, + url: String, + iconURL: String, + redirectURIs: [ String ], + type: { + type: String, + enum: [ "CONFIDENTIAL", "PUBLIC" ] + }, + + userId: { + type: String, + index: true + } // The registered developer + +}); + +// "OAuth permission" +var OAuthPermission = loopback.createModel({ + clientId: { + type: String, + index: true + }, // The client id + resourceOwner: { + type: String, + index: true + }, // The resource owner (user) id + realm: { + type: String, + index: true + }, // The resource owner realm + + issuedAt: { + type: Date, + index: true + }, // The timestamp when the permission is issued + expiresIn: Number, // Expiration time in seconds + expiredAt: { + type: Date, + index: { + expires: "1d" + } + }, // The timestamp when the permission is expired + + scopes: [ String ] +}); + +// "OAuth scope" +var OAuthScope = loopback.createModel({ + scope: { + type: String, + index: { + unique: true + } + }, // key; // The scope name + description: String, // Description of the scope + iconURL: String, // The icon to be displayed on the "Request Permission" + // dialog + expiresIn: Number, // The default maximum lifetime of access token that + // carries the scope + requiredAccessLevel: Number, // The minimum access level required + resourceOwnerAuthorizationRequired: Boolean +// The scope requires authorization from the resource owner +}); + +// "OAuth protected resource" +var OAuthResource = loopback.createModel({ + operations: [ + { + type: String, + enum: [ "ALL", "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH" ] + } + ], // A list of operations, by default ALL + path: String, // The resource URI path + scopes: [ String ] +// Allowd scopes +}); + +// Use the schema to register a model +exports.OAuthToken = OAuthToken; +exports.OAuthAuthorizationCode = OAuthAuthorizationCode; +exports.ClientRegistration = ClientRegistration; +exports.OAuthPermission = OAuthPermission; +exports.OAuthScope = OAuthScope; +exports.OAuthResource = OAuthResource; From 94f12d0fcef6576462d0e40f7c92100aca9a8f68 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 17:07:43 -0800 Subject: [PATCH 10/13] Fix the permission check --- lib/models/acl.js | 4 ++-- test/acl.test.js | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 91a37297..6adfaa9d 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -144,7 +144,7 @@ ACL.checkPermission = function (principalType, principalId, model, property, acc if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === ACL.ALL && currentValue.accessType)) { previousValue.accessType = currentValue.accessType; } - currentValue.permission = overridePermission(previousValue.permission, currentValue.permission); + previousValue.permission = overridePermission(previousValue.permission, currentValue.permission); } return previousValue; }, {principalType: principalType, principalId: principalId, model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}); @@ -180,7 +180,7 @@ Scope.checkPermission = function (scope, model, property, accessType, callback) if (previousValue.accessType === currentValue.accessType || (previousValue.accessType === ACL.ALL && currentValue.accessType)) { previousValue.accessType = currentValue.accessType; } - currentValue.permission = overridePermission(previousValue.permission, currentValue.permission); + previousValue.permission = overridePermission(previousValue.permission, currentValue.permission); } return previousValue; }, {model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}); diff --git a/test/acl.test.js b/test/acl.test.js index 705f6367..2695f8d4 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -46,11 +46,24 @@ describe('security scopes', function () { ACL.create({principalType: ACL.SCOPE, principalId: scope.id, model: 'user', property: 'name', accessType: ACL.READ, 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); - }); + ACL.create({principalType: ACL.SCOPE, principalId: scope.id, + 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) { + assert(perm.permission === ACL.ALLOW); + }); + Scope.checkPermission('user', 'user', 'name', ACL.ALL, function (err, perm) { + assert(perm.permission === ACL.ALLOW); + }); + Scope.checkPermission('user', 'user', 'name', ACL.READ, function (err, perm) { + assert(perm.permission === ACL.ALLOW); + }); + Scope.checkPermission('user', 'user', 'name', ACL.WRITE, function (err, perm) { + assert(perm.permission === ACL.DENY); + }); + }); + }); }); }); From cc7560b25849b277b3d565350ba6e7080a9798dd Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 17:14:13 -0800 Subject: [PATCH 11/13] Simplify check permission --- lib/models/acl.js | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index 6adfaa9d..f4953969 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -130,8 +130,13 @@ function overridePermission(p1, p2) { * @param callback */ ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) { + property = property || ACL.ALL; + var propertyQuery = (property === ACL.ALL) ? ACL.ALL : {inq: [property, ACL.ALL]}; + accessType = accessType || ACL.aLL; + var accessTypeQuery = (accessType === ACL.ALL) ? ACL.ALL : {inq: [accessType, ACL.ALL]}; + ACL.find({where: {principalType: principalType, principalId: principalId, - model: model, property: {inq: [property, ACL.ALL]}, accessType: {inq: [accessType, ACL.ALL]}}}, + model: model, property: propertyQuery, accessType: accessTypeQuery}}, function (err, acls) { if (err) { callback && callback(err); @@ -165,28 +170,7 @@ Scope.checkPermission = function (scope, model, property, accessType, callback) if (err) { callback && callback(err); } else { - ACL.find({where: {principalType: ACL.SCOPE, principalId: scope.id, - model: model, property: {inq: [property, ACL.ALL]}, - accessType: {inq: [accessType, ACL.ALL]}}}, function (err, resources) { - if (err) { - callback && callback(err); - return; - } - // Try to resolve the permission - var resolvedPermission = resources.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; - }, {model: model, property: ACL.ALL, accessType: ACL.ALL, permission: ACL.ALLOW}); - callback && callback(null, resolvedPermission); - } - ); + ACL.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback); } }); }; From 8e679d092752a09d7fe4e4ad865678d470261fd9 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 17:24:42 -0800 Subject: [PATCH 12/13] Fix the permission resolution --- lib/models/acl.js | 4 ++-- test/acl.test.js | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/models/acl.js b/lib/models/acl.js index f4953969..754bb8f5 100644 --- a/lib/models/acl.js +++ b/lib/models/acl.js @@ -131,9 +131,9 @@ function overridePermission(p1, p2) { */ ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) { property = property || ACL.ALL; - var propertyQuery = (property === ACL.ALL) ? ACL.ALL : {inq: [property, ACL.ALL]}; + var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]}; accessType = accessType || ACL.aLL; - var accessTypeQuery = (accessType === ACL.ALL) ? ACL.ALL : {inq: [accessType, ACL.ALL]}; + var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]}; ACL.find({where: {principalType: principalType, principalId: principalId, model: model, property: propertyQuery, accessType: accessTypeQuery}}, diff --git a/test/acl.test.js b/test/acl.test.js index 2695f8d4..1c5c4d91 100644 --- a/test/acl.test.js +++ b/test/acl.test.js @@ -51,10 +51,10 @@ describe('security scopes', function () { function (err, resource) { // console.log(resource); Scope.checkPermission('user', 'user', ACL.ALL, ACL.ALL, function (err, perm) { - assert(perm.permission === ACL.ALLOW); + assert(perm.permission === ACL.DENY); // because name.WRITE == DENY }); Scope.checkPermission('user', 'user', 'name', ACL.ALL, function (err, perm) { - assert(perm.permission === ACL.ALLOW); + assert(perm.permission === ACL.DENY); // because name.WRITE == DENY }); Scope.checkPermission('user', 'user', 'name', ACL.READ, function (err, perm) { assert(perm.permission === ACL.ALLOW); @@ -76,11 +76,21 @@ 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, accessType: ACL.ALL, permission: ACL.ALLOW}, function (err, acl) { - ACL.checkPermission('user', 'u001', 'user', 'u001', ACL.READ, checkResult); + ACL.create({principalType: '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) { + assert(perm.permission === ACL.DENY); + }); + + ACL.checkPermission('user', 'u001', 'user', 'name', ACL.ALL, function (err, perm) { + assert(perm.permission === ACL.DENY); + }); + + }); }); From 9bc762c09c0229404c37fba25b016e8bff6959cc Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 13 Nov 2013 22:19:41 -0800 Subject: [PATCH 13/13] Update dependencies --- package.json | 8 ++++---- test/user.test.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 7883e8de..4ed8d6b0 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,11 @@ "debug": "~0.7.2", "express": "~3.4.0", "loopback-datasource-juggler": "~1.2.0", - "strong-remoting": "~1.0.0", + "strong-remoting": "~1.1.0", "inflection": "~1.2.5", "passport": "~0.1.17", "passport-local": "~0.1.6", - "nodemailer": "~0.4.4", + "nodemailer": "~0.5.5", "ejs": "~0.8.4", "bcryptjs": "~0.7.10", "underscore.string": "~2.3.3", @@ -34,9 +34,9 @@ }, "devDependencies": { "blanket": "~1.1.5", - "mocha": "~1.12.1", + "mocha": "~1.14.0", "strong-task-emitter": "0.0.x", - "supertest": "~0.7.1" + "supertest": "~0.8.1" }, "repository": { "type": "git", diff --git a/test/user.test.js b/test/user.test.js index 74ddab1a..bc2ea76f 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -264,7 +264,7 @@ describe('User', function(){ var lines = result.email.message.split('\n'); - assert(lines[4].indexOf('To: bar@bat.com') === 0); + assert(lines[3].indexOf('To: bar@bat.com') === 0); done(); }); });