models: move ACL LDL def into a json file

This commit is contained in:
Miroslav Bajtoš 2014-10-13 10:55:08 +02:00
parent ef890d5f26
commit 7c01d59d80
5 changed files with 360 additions and 351 deletions

View File

@ -5,9 +5,7 @@
var loopback = require('../../lib/loopback') var loopback = require('../../lib/loopback')
, assert = require('assert') , assert = require('assert')
, uid = require('uid2') , uid = require('uid2')
, DEFAULT_TOKEN_LEN = 64 , DEFAULT_TOKEN_LEN = 64;
, Role = require('./role').Role
, ACL = require('./acl').ACL;
/** /**
* Token based authentication and access control. * Token based authentication and access control.

View File

@ -45,6 +45,8 @@ var role = require('./role');
var Role = role.Role; var Role = role.Role;
/** /**
* A Model for access control meta data.
*
* System grants permissions to principals (users/applications, can be grouped * System grants permissions to principals (users/applications, can be grouped
* into roles). * into roles).
* *
@ -54,18 +56,6 @@ var Role = role.Role;
* For a given principal, such as client application and/or user, is it allowed * For a given principal, such as client application and/or user, is it allowed
* to access (read/write/execute) * to access (read/write/execute)
* the protected resource? * the protected resource?
*/
var ACLSchema = {
model: String, // The name of the model
property: String, // The name of the property, method, scope, or relation
accessType: String,
permission: String,
principalType: String,
principalId: String
};
/**
* A Model for access control meta data.
* *
* @header ACL * @header ACL
* @property {String} model Name of the model. * @property {String} model Name of the model.
@ -78,36 +68,37 @@ var ACLSchema = {
* - DENY: Explicitly denies access to the resource. * - DENY: Explicitly denies access to the resource.
* @property {String} principalType Type of the principal; one of: Application, Use, Role. * @property {String} principalType Type of the principal; one of: Application, Use, Role.
* @property {String} principalId ID of the principal - such as appId, userId or roleId * @property {String} principalId ID of the principal - such as appId, userId or roleId
* @class *
* @inherits Model * @class ACL
* @inherits PersistedModel
*/ */
var ACL = loopback.PersistedModel.extend('ACL', ACLSchema); module.exports = function(ACL) {
ACL.ALL = AccessContext.ALL; ACL.ALL = AccessContext.ALL;
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
ACL.ALLOW = AccessContext.ALLOW; // Allow ACL.ALLOW = AccessContext.ALLOW; // Allow
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
ACL.DENY = AccessContext.DENY; // Deny ACL.DENY = AccessContext.DENY; // Deny
ACL.READ = AccessContext.READ; // Read operation ACL.READ = AccessContext.READ; // Read operation
ACL.WRITE = AccessContext.WRITE; // Write operation ACL.WRITE = AccessContext.WRITE; // Write operation
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
ACL.USER = Principal.USER; ACL.USER = Principal.USER;
ACL.APP = ACL.APPLICATION = Principal.APPLICATION; ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
ACL.ROLE = Principal.ROLE; ACL.ROLE = Principal.ROLE;
ACL.SCOPE = Principal.SCOPE; ACL.SCOPE = Principal.SCOPE;
/** /**
* Calculate the matching score for the given rule and request * Calculate the matching score for the given rule and request
* @param {ACL} rule The ACL entry * @param {ACL} rule The ACL entry
* @param {AccessRequest} req The request * @param {AccessRequest} req The request
* @returns {Number} * @returns {Number}
*/ */
ACL.getMatchingScore = function getMatchingScore(rule, req) { ACL.getMatchingScore = function getMatchingScore(rule, req) {
var props = ['model', 'property', 'accessType']; var props = ['model', 'property', 'accessType'];
var score = 0; var score = 0;
@ -139,7 +130,7 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
// - other // - other
// user > app > role > ... // user > app > role > ...
score = score * 4; score = score * 4;
switch(rule.principalType) { switch (rule.principalType) {
case ACL.USER: case ACL.USER:
score += 4; score += 4;
break; break;
@ -150,14 +141,14 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
score += 2; score += 2;
break; break;
default: default:
score +=1; score += 1;
} }
// Weigh against the roles // Weigh against the roles
// everyone < authenticated/unauthenticated < related < owner < ... // everyone < authenticated/unauthenticated < related < owner < ...
score = score * 8; score = score * 8;
if(rule.principalType === ACL.ROLE) { if (rule.principalType === ACL.ROLE) {
switch(rule.principalId) { switch (rule.principalId) {
case Role.OWNER: case Role.OWNER:
score += 4; score += 4;
break; break;
@ -178,30 +169,30 @@ ACL.getMatchingScore = function getMatchingScore(rule, req) {
score = score * 4; score = score * 4;
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1; score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
return score; return score;
}; };
/** /**
* Get matching score for the given `AccessRequest`. * Get matching score for the given `AccessRequest`.
* @param {AccessRequest} req The request * @param {AccessRequest} req The request
* @returns {Number} score * @returns {Number} score
*/ */
ACL.prototype.score = function(req) { ACL.prototype.score = function(req) {
return this.constructor.getMatchingScore(this, req); return this.constructor.getMatchingScore(this, req);
} }
/*! /*!
* Resolve permission from the ACLs * Resolve permission from the ACLs
* @param {Object[]) acls The list of ACLs * @param {Object[]) acls The list of ACLs
* @param {Object} req The request * @param {Object} req The request
* @returns {AccessRequest} result The effective ACL * @returns {AccessRequest} result The effective ACL
*/ */
ACL.resolvePermission = function resolvePermission(acls, req) { ACL.resolvePermission = function resolvePermission(acls, req) {
if(!(req instanceof AccessRequest)) { if (!(req instanceof AccessRequest)) {
req = new AccessRequest(req); req = new AccessRequest(req);
} }
// Sort by the matching score in descending order // Sort by the matching score in descending order
acls = acls.sort(function (rule1, rule2) { acls = acls.sort(function(rule1, rule2) {
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req); return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
}); });
var permission = ACL.DEFAULT; var permission = ACL.DEFAULT;
@ -218,19 +209,19 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
permission = acls[i].permission; permission = acls[i].permission;
break; break;
} else { } else {
if(req.exactlyMatches(acls[i])) { if (req.exactlyMatches(acls[i])) {
permission = acls[i].permission; permission = acls[i].permission;
break; break;
} }
// For wildcard match, find the strongest permission // For wildcard match, find the strongest permission
if(AccessContext.permissionOrder[acls[i].permission] if (AccessContext.permissionOrder[acls[i].permission]
> AccessContext.permissionOrder[permission]) { > AccessContext.permissionOrder[permission]) {
permission = acls[i].permission; permission = acls[i].permission;
} }
} }
} }
if(debug.enabled) { if (debug.enabled) {
debug('The following ACLs were searched: '); debug('The following ACLs were searched: ');
acls.forEach(function(acl) { acls.forEach(function(acl) {
acl.debug(); acl.debug();
@ -241,20 +232,20 @@ ACL.resolvePermission = function resolvePermission(acls, req) {
var res = new AccessRequest(req.model, req.property, req.accessType, var res = new AccessRequest(req.model, req.property, req.accessType,
permission || ACL.DEFAULT); permission || ACL.DEFAULT);
return res; return res;
}; };
/*! /*!
* Get the static ACLs from the model definition * Get the static ACLs from the model definition
* @param {String} model The model name * @param {String} model The model name
* @param {String} property The property/method/relation name * @param {String} property The property/method/relation name
* *
* @return {Object[]} An array of ACLs * @return {Object[]} An array of ACLs
*/ */
ACL.getStaticACLs = function getStaticACLs(model, property) { ACL.getStaticACLs = function getStaticACLs(model, property) {
var modelClass = loopback.findModel(model); var modelClass = loopback.findModel(model);
var staticACLs = []; var staticACLs = [];
if (modelClass && modelClass.settings.acls) { if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function (acl) { modelClass.settings.acls.forEach(function(acl) {
if (!acl.property || acl.property === ACL.ALL if (!acl.property || acl.property === ACL.ALL
|| property === acl.property) { || property === acl.property) {
staticACLs.push(new ACL({ staticACLs.push(new ACL({
@ -274,7 +265,7 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
|| modelClass[property] // static method || modelClass[property] // static method
|| modelClass.prototype[property]); // prototype method || modelClass.prototype[property]); // prototype method
if (prop && prop.acls) { if (prop && prop.acls) {
prop.acls.forEach(function (acl) { prop.acls.forEach(function(acl) {
staticACLs.push(new ACL({ staticACLs.push(new ACL({
model: modelClass.modelName, model: modelClass.modelName,
property: property, property: property,
@ -286,9 +277,9 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
}); });
} }
return staticACLs; return staticACLs;
}; };
/** /**
* Check if the given principal is allowed to access the model/property * Check if the given principal is allowed to access the model/property
* @param {String} principalType The principal type. * @param {String} principalType The principal type.
* @param {String} principalId The principal ID. * @param {String} principalId The principal ID.
@ -299,10 +290,10 @@ ACL.getStaticACLs = function getStaticACLs(model, property) {
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {AccessRequest} result The access permission * @param {AccessRequest} result The access permission
*/ */
ACL.checkPermission = function checkPermission(principalType, principalId, ACL.checkPermission = function checkPermission(principalType, principalId,
model, property, accessType, model, property, accessType,
callback) { callback) {
if(principalId !== null && principalId !== undefined && (typeof principalId !== 'string') ) { if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
principalId = principalId.toString(); principalId = principalId.toString();
} }
property = property || ACL.ALL; property = property || ACL.ALL;
@ -316,7 +307,7 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
var resolved = this.resolvePermission(acls, req); var resolved = this.resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DENY) { if (resolved && resolved.permission === ACL.DENY) {
debug('Permission denied by statically resolved permission'); debug('Permission denied by statically resolved permission');
debug(' Resolved Permission: %j', resolved); debug(' Resolved Permission: %j', resolved);
process.nextTick(function() { process.nextTick(function() {
@ -328,23 +319,23 @@ ACL.checkPermission = function checkPermission(principalType, principalId,
var self = this; var self = this;
this.find({where: {principalType: principalType, principalId: principalId, this.find({where: {principalType: principalType, principalId: principalId,
model: model, property: propertyQuery, accessType: accessTypeQuery}}, model: model, property: propertyQuery, accessType: accessTypeQuery}},
function (err, dynACLs) { function(err, dynACLs) {
if (err) { if (err) {
callback && callback(err); callback && callback(err);
return; return;
} }
acls = acls.concat(dynACLs); acls = acls.concat(dynACLs);
resolved = self.resolvePermission(acls, req); resolved = self.resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DEFAULT) { if (resolved && resolved.permission === ACL.DEFAULT) {
var modelClass = loopback.findModel(model); var modelClass = loopback.findModel(model);
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
} }
callback && callback(null, resolved); callback && callback(null, resolved);
}); });
}; };
ACL.prototype.debug = function() { ACL.prototype.debug = function() {
if(debug.enabled) { if (debug.enabled) {
debug('---ACL---'); debug('---ACL---');
debug('model %s', this.model); debug('model %s', this.model);
debug('property %s', this.property); debug('property %s', this.property);
@ -353,9 +344,9 @@ ACL.prototype.debug = function() {
debug('accessType %s', this.accessType); debug('accessType %s', this.accessType);
debug('permission %s', this.permission); debug('permission %s', this.permission);
} }
} }
/** /**
* Check if the request has the permission to access. * Check if the request has the permission to access.
* @options {Object} context See below. * @options {Object} context See below.
* @property {Object[]} principals An array of principals. * @property {Object[]} principals An array of principals.
@ -366,8 +357,8 @@ ACL.prototype.debug = function() {
* @param {Function} callback Callback function * @param {Function} callback Callback function
*/ */
ACL.checkAccessForContext = function (context, callback) { ACL.checkAccessForContext = function(context, callback) {
if(!(context instanceof AccessContext)) { if (!(context instanceof AccessContext)) {
context = new AccessContext(context); context = new AccessContext(context);
} }
@ -388,7 +379,7 @@ ACL.checkAccessForContext = function (context, callback) {
var self = this; var self = this;
var roleModel = loopback.getModelByType(Role); var roleModel = loopback.getModelByType(Role);
this.find({where: {model: model.modelName, property: propertyQuery, this.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function (err, acls) { accessType: accessTypeQuery}}, function(err, acls) {
if (err) { if (err) {
callback && callback(err); callback && callback(err);
return; return;
@ -397,7 +388,7 @@ ACL.checkAccessForContext = function (context, callback) {
acls = acls.concat(staticACLs); acls = acls.concat(staticACLs);
acls.forEach(function (acl) { acls.forEach(function(acl) {
// Check exact matches // Check exact matches
for (var i = 0; i < context.principals.length; i++) { for (var i = 0; i < context.principals.length; i++) {
var p = context.principals[i]; var p = context.principals[i];
@ -410,9 +401,9 @@ ACL.checkAccessForContext = function (context, callback) {
// Check role matches // Check role matches
if (acl.principalType === ACL.ROLE) { if (acl.principalType === ACL.ROLE) {
inRoleTasks.push(function (done) { inRoleTasks.push(function(done) {
roleModel.isInRole(acl.principalId, context, roleModel.isInRole(acl.principalId, context,
function (err, inRole) { function(err, inRole) {
if (!err && inRole) { if (!err && inRole) {
effectiveACLs.push(acl); effectiveACLs.push(acl);
} }
@ -422,13 +413,13 @@ ACL.checkAccessForContext = function (context, callback) {
} }
}); });
async.parallel(inRoleTasks, function (err, results) { async.parallel(inRoleTasks, function(err, results) {
if(err) { if (err) {
callback && callback(err, null); callback && callback(err, null);
return; return;
} }
var resolved = self.resolvePermission(effectiveACLs, req); var resolved = self.resolvePermission(effectiveACLs, req);
if(resolved && resolved.permission === ACL.DEFAULT) { if (resolved && resolved.permission === ACL.DEFAULT) {
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW; resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
} }
debug('---Resolved---'); debug('---Resolved---');
@ -436,9 +427,9 @@ ACL.checkAccessForContext = function (context, callback) {
callback && callback(null, resolved); callback && callback(null, resolved);
}); });
}); });
}; };
/** /**
* Check if the given access token can invoke the method * Check if the given access token can invoke the method
* @param {AccessToken} token The access token * @param {AccessToken} token The access token
* @param {String} model The model name * @param {String} model The model name
@ -448,7 +439,7 @@ ACL.checkAccessForContext = function (context, callback) {
* @param {String|Error} err The error object * @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed * @param {Boolean} allowed is the request allowed
*/ */
ACL.checkAccessForToken = function (token, model, modelId, method, callback) { ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
assert(token, 'Access token is required'); assert(token, 'Access token is required');
var context = new AccessContext({ var context = new AccessContext({
@ -459,13 +450,13 @@ ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
modelId: modelId modelId: modelId
}); });
this.checkAccessForContext(context, function (err, access) { this.checkAccessForContext(context, function(err, access) {
if (err) { if (err) {
callback && callback(err); callback && callback(err);
return; return;
} }
callback && callback(null, access.permission !== ACL.DENY); callback && callback(null, access.permission !== ACL.DENY);
}); });
}; };
module.exports.ACL = ACL; }

17
common/models/acl.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "ACL",
"properties": {
"model": {
"type": "string",
"description": "The name of the model"
},
"property": {
"type": "string",
"description": "The name of the property, method, scope, or relation"
},
"accessType": "string",
"permission": "string",
"principalType": "string",
"principalId": "string"
}
}

View File

@ -281,7 +281,7 @@ AccessRequest.prototype.exactlyMatches = function(acl) {
*/ */
AccessRequest.prototype.isAllowed = function() { AccessRequest.prototype.isAllowed = function() {
return this.permission !== require('../common/models/acl').ACL.DENY; return this.permission !== loopback.ACL.DENY;
} }
AccessRequest.prototype.debug = function() { AccessRequest.prototype.debug = function() {

View File

@ -15,7 +15,10 @@ module.exports = function(loopback) {
loopback.Role = require('../common/models/role').Role; loopback.Role = require('../common/models/role').Role;
loopback.RoleMapping = require('../common/models/role').RoleMapping; loopback.RoleMapping = require('../common/models/role').RoleMapping;
loopback.ACL = require('../common/models/acl').ACL;
loopback.ACL = createModel(
require('../common/models/acl.json'),
require('../common/models/acl.js'));
loopback.Scope = createModel( loopback.Scope = createModel(
require('../common/models/scope.json'), require('../common/models/scope.json'),