loopback/common/models/acl.js

472 lines
14 KiB
JavaScript
Raw Normal View History

/*!
2013-11-10 06:22:16 +00:00
Schema ACL options
2013-11-10 06:22:16 +00:00
Object level permissions, for example, an album owned by a user
2013-11-10 06:22:16 +00:00
Factors to be authorized against:
2013-11-10 06:22:16 +00:00
* model name: Album
* model instance properties: userId of the album, friends, shared
* methods
* app and/or user ids/roles
** loggedIn
** roles
** userId
** appId
** none
** everyone
** relations: owner/friend/granted
2013-11-10 06:22:16 +00:00
Class level permissions, for example, Album
* model name: Album
* methods
2013-07-15 21:07:17 +00:00
2013-11-10 06:22:16 +00:00
URL/Route level permissions
* url pattern
* application id
* ip addresses
* http headers
2013-07-15 21:07:17 +00:00
2013-11-10 06:22:16 +00:00
Map to oAuth 2.0 scopes
*/
var loopback = require('../../lib/loopback');
2013-11-20 21:31:30 +00:00
var async = require('async');
var assert = require('assert');
var debug = require('debug')('loopback:security:acl');
2013-11-20 21:31:30 +00:00
var ctx = require('../../lib/access-context');
var AccessContext = ctx.AccessContext;
var Principal = ctx.Principal;
var AccessRequest = ctx.AccessRequest;
2013-11-20 21:31:30 +00:00
var role = require('./role');
var Role = role.Role;
2013-11-10 06:22:16 +00:00
2013-11-12 06:16:51 +00:00
/**
* System grants permissions to principals (users/applications, can be grouped
* into roles).
2013-11-12 06:16:51 +00:00
*
* Protected resource: the model data and operations
* (model/property/method/relation/)
2013-11-12 06:16:51 +00:00
*
* For a given principal, such as client application and/or user, is it allowed
* to access (read/write/execute)
2013-11-12 06:16:51 +00:00
* the protected resource?
*/
var ACLSchema = {
2013-11-10 06:22:16 +00:00
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
2013-10-28 17:44:05 +00:00
};
/**
* A Model for access control meta data.
*
* @header ACL
2014-10-02 00:21:22 +00:00
* @property {String} model Name of the model.
* @property {String} property Name of the property, method, scope, or relation.
* @property {String} accessType Type of access being granted: one of READ, WRITE, or EXECUTE.
* @property {String} permission Type of permission granted. One of:
* - 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.
* @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
* @class
* @inherits Model
*/
2014-06-05 07:45:09 +00:00
var ACL = loopback.PersistedModel.extend('ACL', ACLSchema);
2013-11-04 21:19:02 +00:00
ACL.ALL = AccessContext.ALL;
ACL.DEFAULT = AccessContext.DEFAULT; // Not specified
ACL.ALLOW = AccessContext.ALLOW; // Allow
ACL.ALARM = AccessContext.ALARM; // Warn - send an alarm
ACL.AUDIT = AccessContext.AUDIT; // Audit - record the access
ACL.DENY = AccessContext.DENY; // Deny
ACL.READ = AccessContext.READ; // Read operation
ACL.WRITE = AccessContext.WRITE; // Write operation
ACL.EXECUTE = AccessContext.EXECUTE; // Execute operation
ACL.USER = Principal.USER;
ACL.APP = ACL.APPLICATION = Principal.APPLICATION;
ACL.ROLE = Principal.ROLE;
ACL.SCOPE = Principal.SCOPE;
2013-11-10 06:22:16 +00:00
/**
* Calculate the matching score for the given rule and request
* @param {ACL} rule The ACL entry
* @param {AccessRequest} req The request
2014-06-09 22:36:08 +00:00
* @returns {Number}
*/
ACL.getMatchingScore = function getMatchingScore(rule, req) {
var props = ['model', 'property', 'accessType'];
var score = 0;
for (var i = 0; i < props.length; i++) {
// Shift the score by 4 for each of the properties as the weight
score = score * 4;
var val1 = rule[props[i]] || ACL.ALL;
var val2 = req[props[i]] || ACL.ALL;
var isMatchingMethodName = props[i] === 'property' && req.methodNames.indexOf(val1) !== -1;
if (val1 === val2 || isMatchingMethodName) {
// Exact match
score += 3;
} else if (val1 === ACL.ALL) {
// Wildcard match
score += 2;
} else if (val2 === ACL.ALL) {
// Doesn't match at all
score += 1;
} else {
return -1;
}
}
2014-03-19 23:24:50 +00:00
// Weigh against the principal type into 4 levels
// - user level (explicitly allow/deny a given user)
// - app level (explicitly allow/deny a given app)
// - role level (role based authorization)
// - other
// user > app > role > ...
score = score * 4;
switch(rule.principalType) {
case ACL.USER:
score += 4;
break;
case ACL.APP:
score += 3;
break;
case ACL.ROLE:
score += 2;
break;
default:
score +=1;
}
// Weigh against the roles
2014-03-19 23:24:50 +00:00
// everyone < authenticated/unauthenticated < related < owner < ...
score = score * 8;
if(rule.principalType === ACL.ROLE) {
switch(rule.principalId) {
case Role.OWNER:
score += 4;
break;
case Role.RELATED:
score += 3;
break;
case Role.AUTHENTICATED:
case Role.UNAUTHENTICATED:
score += 2;
break;
case Role.EVERYONE:
score += 1;
break;
default:
score += 5;
}
}
score = score * 4;
score += AccessContext.permissionOrder[rule.permission || ACL.ALLOW] - 1;
return score;
};
2013-11-12 18:10:32 +00:00
/**
* Get matching score for the given `AccessRequest`.
* @param {AccessRequest} req The request
* @returns {Number} score
*/
ACL.prototype.score = function(req) {
return this.constructor.getMatchingScore(this, req);
}
2013-11-15 17:41:26 +00:00
/*!
* Resolve permission from the ACLs
* @param {Object[]) acls The list of ACLs
* @param {Object} req The request
* @returns {AccessRequest} result The effective ACL
2013-11-15 17:41:26 +00:00
*/
ACL.resolvePermission = function resolvePermission(acls, req) {
if(!(req instanceof AccessRequest)) {
req = new AccessRequest(req);
}
// Sort by the matching score in descending order
acls = acls.sort(function (rule1, rule2) {
return ACL.getMatchingScore(rule2, req) - ACL.getMatchingScore(rule1, req);
});
var permission = ACL.DEFAULT;
var score = 0;
for (var i = 0; i < acls.length; i++) {
score = ACL.getMatchingScore(acls[i], req);
if (score < 0) {
// the highest scored ACL did not match
break;
}
if (!req.isWildcard()) {
// We should stop from the first match for non-wildcard
permission = acls[i].permission;
break;
} else {
if(req.exactlyMatches(acls[i])) {
permission = acls[i].permission;
break;
}
// For wildcard match, find the strongest permission
if(AccessContext.permissionOrder[acls[i].permission]
> AccessContext.permissionOrder[permission]) {
permission = acls[i].permission;
2013-11-15 17:41:26 +00:00
}
}
}
2013-12-11 05:49:18 +00:00
if(debug.enabled) {
debug('The following ACLs were searched: ');
acls.forEach(function(acl) {
acl.debug();
debug('with score:', acl.score(req));
});
}
var res = new AccessRequest(req.model, req.property, req.accessType,
permission || ACL.DEFAULT);
return res;
};
2013-11-15 17:41:26 +00:00
2013-11-20 21:31:30 +00:00
/*!
* Get the static ACLs from the model definition
2013-11-20 21:31:30 +00:00
* @param {String} model The model name
* @param {String} property The property/method/relation name
*
* @return {Object[]} An array of ACLs
2013-11-12 06:16:51 +00:00
*/
ACL.getStaticACLs = function getStaticACLs(model, property) {
var modelClass = loopback.findModel(model);
2013-11-15 17:41:26 +00:00
var staticACLs = [];
2013-11-20 21:31:30 +00:00
if (modelClass && modelClass.settings.acls) {
modelClass.settings.acls.forEach(function (acl) {
if (!acl.property || acl.property === ACL.ALL
|| property === acl.property) {
staticACLs.push(new ACL({
model: model,
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType || ACL.ALL,
permission: acl.permission
}));
}
2013-11-20 21:31:30 +00:00
});
}
var prop = modelClass &&
(modelClass.definition.properties[property] // regular property
2013-11-15 18:08:49 +00:00
|| (modelClass._scopeMeta && modelClass._scopeMeta[property]) // relation/scope
|| modelClass[property] // static method
|| modelClass.prototype[property]); // prototype method
2013-11-20 21:31:30 +00:00
if (prop && prop.acls) {
prop.acls.forEach(function (acl) {
staticACLs.push(new ACL({
2013-11-20 21:31:30 +00:00
model: modelClass.modelName,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission
}));
2013-11-20 21:31:30 +00:00
});
2013-11-15 17:41:26 +00:00
}
return staticACLs;
};
2013-11-15 17:41:26 +00:00
2013-11-20 21:31:30 +00:00
/**
* Check if the given principal is allowed to access the model/property
2014-06-09 22:36:08 +00:00
* @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.
* @callback {Function} callback Callback function.
2013-11-20 21:31:30 +00:00
* @param {String|Error} err The error object
* @param {AccessRequest} result The access permission
2013-11-20 21:31:30 +00:00
*/
ACL.checkPermission = function checkPermission(principalType, principalId,
model, property, accessType,
callback) {
2014-04-29 01:34:48 +00:00
if(principalId !== null && principalId !== undefined && (typeof principalId !== 'string') ) {
2014-04-29 12:10:44 +00:00
principalId = principalId.toString();
2014-04-29 01:34:48 +00:00
}
2013-11-20 21:31:30 +00:00
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 req = new AccessRequest(model, property, accessType);
2014-01-16 16:50:50 +00:00
var acls = this.getStaticACLs(model, property);
2014-01-16 16:50:50 +00:00
var resolved = this.resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DENY) {
debug('Permission denied by statically resolved permission');
debug(' Resolved Permission: %j', resolved);
2013-11-20 21:31:30 +00:00
process.nextTick(function() {
callback && callback(null, resolved);
2013-11-20 21:31:30 +00:00
});
2013-11-15 17:41:26 +00:00
return;
}
2014-01-16 16:50:50 +00:00
var self = this;
this.find({where: {principalType: principalType, principalId: principalId,
2013-11-14 01:14:13 +00:00
model: model, property: propertyQuery, accessType: accessTypeQuery}},
function (err, dynACLs) {
2013-11-12 06:16:51 +00:00
if (err) {
callback && callback(err);
return;
}
acls = acls.concat(dynACLs);
2014-01-16 16:50:50 +00:00
resolved = self.resolvePermission(acls, req);
if(resolved && resolved.permission === ACL.DEFAULT) {
var modelClass = loopback.findModel(model);
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
2013-11-20 21:31:30 +00:00
}
callback && callback(null, resolved);
2013-11-12 06:16:51 +00:00
});
};
ACL.prototype.debug = function() {
if(debug.enabled) {
2014-01-16 16:50:50 +00:00
debug('---ACL---');
debug('model %s', this.model);
debug('property %s', this.property);
debug('principalType %s', this.principalType);
debug('principalId %s', this.principalId);
debug('accessType %s', this.accessType);
debug('permission %s', this.permission);
}
}
2013-11-20 21:31:30 +00:00
/**
2014-06-09 22:36:08 +00:00
* Check if the request has the permission to access.
* @options {Object} context See below.
* @property {Object[]} principals An array of principals.
* @property {String|Model} model The model name or model class.
* @property {*} id The model instance ID.
* @property {String} property The property/method/relation name.
2014-08-07 17:33:27 +00:00
* @property {String} accessType The access type: READE, WRITE, or EXECUTE.
2014-06-09 22:36:08 +00:00
* @param {Function} callback Callback function
2013-11-20 21:31:30 +00:00
*/
ACL.checkAccessForContext = function (context, callback) {
if(!(context instanceof AccessContext)) {
context = new AccessContext(context);
}
2013-11-15 04:19:46 +00:00
2013-11-20 21:31:30 +00:00
var model = context.model;
var property = context.property;
var accessType = context.accessType;
var modelName = context.modelName;
2013-11-20 21:31:30 +00:00
var methodNames = context.methodNames;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
2013-11-20 21:31:30 +00:00
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
var effectiveACLs = [];
2014-01-16 16:50:50 +00:00
var staticACLs = this.getStaticACLs(model.modelName, property);
2013-11-20 21:31:30 +00:00
2014-01-16 16:50:50 +00:00
var self = this;
var roleModel = loopback.getModelByType(Role);
2014-01-16 16:50:50 +00:00
this.find({where: {model: model.modelName, property: propertyQuery,
accessType: accessTypeQuery}}, function (err, acls) {
2013-11-20 21:31:30 +00:00
if (err) {
callback && callback(err);
return;
}
var inRoleTasks = [];
2013-12-11 05:49:18 +00:00
acls = acls.concat(staticACLs);
2013-11-20 21:31:30 +00:00
acls.forEach(function (acl) {
// Check exact matches
for (var i = 0; i < context.principals.length; i++) {
var p = context.principals[i];
if (p.type === acl.principalType
&& String(p.id) === String(acl.principalId)) {
2013-11-20 21:31:30 +00:00
effectiveACLs.push(acl);
return;
2013-11-20 21:31:30 +00:00
}
}
// Check role matches
if (acl.principalType === ACL.ROLE) {
inRoleTasks.push(function (done) {
roleModel.isInRole(acl.principalId, context,
function (err, inRole) {
if (!err && inRole) {
effectiveACLs.push(acl);
}
done(err, acl);
});
});
}
2013-11-20 21:31:30 +00:00
});
async.parallel(inRoleTasks, function (err, results) {
if(err) {
callback && callback(err, null);
return;
}
2014-01-16 16:50:50 +00:00
var resolved = self.resolvePermission(effectiveACLs, req);
2014-01-16 23:05:10 +00:00
if(resolved && resolved.permission === ACL.DEFAULT) {
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
}
debug('---Resolved---');
resolved.debug();
callback && callback(null, resolved);
2013-11-20 21:31:30 +00:00
});
});
};
/**
* 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
2014-06-09 22:36:08 +00:00
* @callback {Function} callback Callback function
2013-11-20 21:31:30 +00:00
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
ACL.checkAccessForToken = function (token, model, modelId, method, callback) {
2013-11-20 21:31:30 +00:00
assert(token, 'Access token is required');
2013-12-07 01:04:47 +00:00
var context = new AccessContext({
accessToken: token,
2013-11-20 21:31:30 +00:00
model: model,
property: method,
method: method,
modelId: modelId
});
2013-12-11 05:49:18 +00:00
this.checkAccessForContext(context, function (err, access) {
if (err) {
2013-11-20 21:31:30 +00:00
callback && callback(err);
return;
}
2013-11-15 04:19:46 +00:00
callback && callback(null, access.permission !== ACL.DENY);
2013-11-20 21:31:30 +00:00
});
};
module.exports.ACL = ACL;