loopback/lib/models/acl.js

385 lines
12 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('../loopback');
2013-11-20 21:31:30 +00:00
var async = require('async');
var assert = require('assert');
var role = require('./role');
var Role = role.Role;
2013-11-10 06:22:16 +00:00
2013-11-12 06:16:51 +00:00
/**
* Schema for Scope which represents the permissions that are granted to client applications by the resource owner
*/
2013-11-10 06:22:16 +00:00
var ScopeSchema = {
name: {type: String, required: true},
description: String
2013-11-04 21:19:02 +00:00
};
2013-11-10 06:22:16 +00:00
/**
2013-11-12 06:16:51 +00:00
* 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)?
*
2013-11-10 06:22:16 +00:00
* Scope has many resource access entries
* @type {createModel|*}
*/
2013-11-13 18:02:59 +00:00
var Scope = loopback.createModel('Scope', ScopeSchema);
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).
*
* 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 = {
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
/**
* 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,
/**
* Type of the principal - Application/User/Role
*/
principalType: String,
/**
* Id of the principal - such as appId, userId or roleId
*/
principalId: String
2013-10-28 17:44:05 +00:00
};
2013-11-04 21:19:02 +00:00
var ACL = loopback.createModel('ACL', ACLSchema);
2013-11-12 18:10:32 +00:00
ACL.ALL = '*';
2013-11-20 21:31:30 +00:00
ACL.DEFAULT = 'DEFAULT';
2013-11-12 18:10:32 +00:00
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';
2013-11-13 18:02:59 +00:00
ACL.SCOPE = 'SCOPE';
2013-11-12 18:10:32 +00:00
var permissionOrder = {
2013-11-20 21:31:30 +00:00
DEFAULT: 0,
2013-11-12 18:10:32 +00:00
ALLOW: 1,
ALARM: 2,
AUDIT: 3,
DENY: 4
2013-11-10 06:22:16 +00:00
};
2013-11-12 18:10:32 +00:00
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;
}
2013-11-15 17:41:26 +00:00
/*!
* Resolve permission from the ACLs
* @param acls
* @param defaultPermission
* @returns {*|Object|Mixed}
*/
function resolvePermission(acls, defaultPermission) {
var resolvedPermission = acls.reduce(function (previousValue, currentValue, index, array) {
// If the property is the same or the previous one is ACL.ALL (ALL)
if (previousValue.property === currentValue.property || (previousValue.property === ACL.ALL && currentValue.property)) {
previousValue.property = currentValue.property;
// Check if the accessType applies
if (previousValue.accessType === currentValue.accessType
|| previousValue.accessType === ACL.ALL
|| currentValue.accessType === ACL.ALL
|| !currentValue.accessType) {
previousValue.permission = overridePermission(previousValue.permission, currentValue.permission);
}
}
return previousValue;
}, defaultPermission);
return resolvedPermission;
}
2013-11-20 21:31:30 +00:00
/*!
* Check the LDL ACLs
2013-11-12 06:16:51 +00:00
* @param principalType
* @param principalId
2013-11-20 21:31:30 +00:00
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
*
* @returns {{principalType: *, principalId: *, model: *, property: string, accessType: *, permission: string}}
2013-11-12 06:16:51 +00:00
*/
2013-11-20 21:31:30 +00:00
function getStaticPermission(principalType, principalId, model, property, accessType) {
var modelClass = loopback.getModel(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) {
staticACLs.push({
model: model,
property: acl.property || ACL.ALL,
principalType: acl.principalType,
principalId: acl.principalId, // TODO: Should it be a name?
accessType: acl.accessType,
permission: acl.permission
2013-11-15 17:41:26 +00:00
});
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({
model: modelClass.modelName,
property: property,
principalType: acl.principalType,
principalId: acl.principalId,
accessType: acl.accessType,
permission: acl.permission
2013-11-15 17:41:26 +00:00
});
2013-11-20 21:31:30 +00:00
});
2013-11-15 17:41:26 +00:00
}
var defaultPermission = {principalType: principalType, principalId: principalId,
model: model, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
defaultPermission = resolvePermission(staticACLs, defaultPermission);
2013-11-20 21:31:30 +00:00
return defaultPermission;
}
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
* @param {String} principalType The principal type
* @param {String} principalId The principal id
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
* @param {Function} callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Object} the access permission
*/
ACL.checkPermission = function (principalType, principalId, model, property, accessType, callback) {
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var defaultPermission = getStaticPermission(principalType, principalId, model, property, accessType);
2013-11-15 17:41:26 +00:00
if(defaultPermission.permission === ACL.DENY) {
// Fail fast
2013-11-20 21:31:30 +00:00
process.nextTick(function() {
callback && callback(null, defaultPermission);
});
2013-11-15 17:41:26 +00:00
return;
}
2013-11-12 06:16:51 +00:00
ACL.find({where: {principalType: principalType, principalId: principalId,
2013-11-14 01:14:13 +00:00
model: model, property: propertyQuery, accessType: accessTypeQuery}},
2013-11-12 06:16:51 +00:00
function (err, acls) {
if (err) {
callback && callback(err);
return;
}
2013-11-15 17:41:26 +00:00
var resolvedPermission = resolvePermission(acls, defaultPermission);
2013-11-20 21:31:30 +00:00
if(resolvedPermission.permission === ACL.DEFAULT) {
var modelClass = loopback.getModel(model);
resolvedPermission.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
}
2013-11-12 18:10:32 +00:00
callback && callback(null, resolvedPermission);
2013-11-12 06:16:51 +00:00
});
};
/**
* Check if the given scope is allowed to access the model/property
2013-11-20 21:31:30 +00:00
* @param {String} scope The scope name
* @param {String} model The model name
* @param {String} property The property/method/relation name
* @param {String} accessType The access type
* @param {Function} callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Object} the access permission
2013-11-12 06:16:51 +00:00
*/
Scope.checkPermission = function (scope, model, property, accessType, callback) {
2013-11-10 06:22:16 +00:00
Scope.findOne({where: {name: scope}}, function (err, scope) {
if (err) {
callback && callback(err);
} else {
2013-11-14 01:14:13 +00:00
ACL.checkPermission(ACL.SCOPE, scope.id, model, property, accessType, callback);
2013-11-10 06:22:16 +00:00
}
});
2013-11-12 18:10:32 +00:00
};
2013-11-20 21:31:30 +00:00
/**
* Check if the request has the permission to access
* @param {Object} context
* @param {Function} callback
*/
ACL.checkAccess = function (context, callback) {
context = context || {};
var principals = context.principals || [];
2013-11-15 04:19:46 +00:00
// add ROLE.EVERYONE
principals.unshift({principalType: ACL.ROLE, principalId: Role.EVERYONE});
2013-11-20 21:31:30 +00:00
var model = context.model;
model = ('string' === typeof model) ? loopback.getModel(model) : model;
var id = context.id;
var property = context.property;
var accessType = context.accessType;
property = property || ACL.ALL;
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
accessType = accessType || ACL.ALL;
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : {inq: [accessType, ACL.ALL]};
var defaultPermission = {principalType: null, principalId: null,
model: model.modelName, property: ACL.ALL, accessType: accessType, permission: ACL.ALLOW};
// Check the LDL ACLs
principals.forEach(function(p) {
var perm = getStaticPermission(p.principalType, p.principalId, model.modelName, property, accessType);
defaultPermission = resolvePermission([perm], defaultPermission);
});
if(defaultPermission.permission === ACL.DENY) {
// Fail fast
process.nextTick(function() {
callback && callback(null, defaultPermission);
});
return;
}
ACL.find({where: {model: model.modelName, property: propertyQuery, accessType: accessTypeQuery}}, function (err, acls) {
if (err) {
callback && callback(err);
return;
}
var effectiveACLs = [];
var inRoleTasks = [];
acls.forEach(function (acl) {
principals.forEach(function (principal) {
if (principal.principalType === acl.pricipalType && principal.principalId === acl.principalId) {
effectiveACLs.push(acl);
} else if (acl.principalType === ACL.ROLE) {
inRoleTasks.push(function (done) {
Role.isInRole(acl.principalId,
{principalType: principal.principalType, principalId: acl.principalId, model: model, id: id, property: property},
function (err, inRole) {
if(!err) {
effectiveACLs.push(acl);
}
done(err, acl);
});
});
}
});
});
async.parallel(inRoleTasks, function(err, results) {
defaultPermission = resolvePermission(effectiveACLs, defaultPermission);
callback && callback(null, defaultPermission);
});
});
};
/**
* Check if the given access token can invoke the method
* @param {AccessToken} token The access token
* @param {String} model The model name
* @param {*} modelId The model id
* @param {String} method The method name
* @param callback The callback function
*
* @callback callback
* @param {String|Error} err The error object
* @param {Boolean} allowed is the request allowed
*/
ACL.checkAccessForToken = function(token, model, modelId, method, callback) {
assert(token, 'Access token is required');
var principals = [];
if(token.userId) {
principals.push({principalType: ACL.USER, principalId: token.userId});
}
if(token.appId) {
principals.push({principalType: ACL.APPLICATION, principalId: token.appId});
}
var context = {
principals: principals,
model: model,
property: method,
accessType: ACL.EXECUTE,
id: modelId
};
ACL.checkAccess(context, function(err, access) {
if(err) {
callback && callback(err);
return;
}
2013-11-15 04:19:46 +00:00
callback && callback(null, access.permission !== ACL.DENY);
2013-11-20 21:31:30 +00:00
});
};
2013-11-12 18:10:32 +00:00
module.exports = {
ACL: ACL,
2013-11-13 18:02:59 +00:00
Scope: Scope
2013-11-12 18:10:32 +00:00
};