2018-01-03 04:05:53 +00:00
|
|
|
// Copyright IBM Corp. 2014,2018. All Rights Reserved.
|
2016-05-03 22:50:21 +00:00
|
|
|
// Node module: loopback
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
|
2016-11-15 21:46:23 +00:00
|
|
|
'use strict';
|
2013-12-20 01:49:47 +00:00
|
|
|
/*!
|
2013-11-10 06:22:16 +00:00
|
|
|
Schema ACL options
|
|
|
|
Object level permissions, for example, an album owned by a user
|
|
|
|
Factors to be authorized against:
|
|
|
|
* model name: Album
|
|
|
|
* model instance properties: userId of the album, friends, shared
|
|
|
|
* methods
|
|
|
|
* app and/or user ids/roles
|
2013-07-01 18:51:28 +00:00
|
|
|
** loggedIn
|
|
|
|
** roles
|
|
|
|
** userId
|
|
|
|
** appId
|
|
|
|
** none
|
|
|
|
** everyone
|
|
|
|
** relations: owner/friend/granted
|
2013-11-10 06:22:16 +00:00
|
|
|
Class level permissions, for example, Album
|
2013-07-01 18:51:28 +00:00
|
|
|
* model name: Album
|
|
|
|
* methods
|
2013-11-10 06:22:16 +00:00
|
|
|
URL/Route level permissions
|
2013-07-18 18:44:25 +00:00
|
|
|
* url pattern
|
|
|
|
* application id
|
|
|
|
* ip addresses
|
|
|
|
* http headers
|
2013-11-10 06:22:16 +00:00
|
|
|
Map to oAuth 2.0 scopes
|
|
|
|
*/
|
|
|
|
|
2016-09-16 19:31:48 +00:00
|
|
|
var g = require('../../lib/globalize');
|
2014-10-09 15:32:03 +00:00
|
|
|
var loopback = require('../../lib/loopback');
|
2017-01-30 13:56:57 +00:00
|
|
|
var utils = require('../../lib/utils');
|
2013-11-20 21:31:30 +00:00
|
|
|
var async = require('async');
|
2016-12-06 14:24:49 +00:00
|
|
|
var extend = require('util')._extend;
|
2013-11-20 21:31:30 +00:00
|
|
|
var assert = require('assert');
|
2013-12-12 03:15:19 +00:00
|
|
|
var debug = require('debug')('loopback:security:acl');
|
2013-11-20 21:31:30 +00:00
|
|
|
|
2014-10-09 15:32:03 +00:00
|
|
|
var ctx = require('../../lib/access-context');
|
2013-12-12 00:03:48 +00:00
|
|
|
var AccessContext = ctx.AccessContext;
|
|
|
|
var Principal = ctx.Principal;
|
|
|
|
var AccessRequest = ctx.AccessRequest;
|
|
|
|
|
2014-10-13 09:31:27 +00:00
|
|
|
var Role = loopback.Role;
|
|
|
|
assert(Role, 'Role model must be defined before ACL model');
|
2013-11-10 06:22:16 +00:00
|
|
|
|
2013-11-12 06:16:51 +00:00
|
|
|
/**
|
2014-10-13 08:55:08 +00:00
|
|
|
* A Model for access control meta data.
|
|
|
|
*
|
2013-12-11 07:33:57 +00:00
|
|
|
* System grants permissions to principals (users/applications, can be grouped
|
|
|
|
* into roles).
|
2013-11-12 06:16:51 +00:00
|
|
|
*
|
2013-12-11 07:33:57 +00:00
|
|
|
* Protected resource: the model data and operations
|
|
|
|
* (model/property/method/relation/…)
|
2013-11-12 06:16:51 +00:00
|
|
|
*
|
2013-12-11 07:33:57 +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?
|
2013-12-20 01:49:47 +00:00
|
|
|
*
|
|
|
|
* @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:
|
2015-02-23 21:13:52 +00:00
|
|
|
*
|
2014-10-02 00:21:22 +00:00
|
|
|
* - 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.
|
2017-08-11 18:10:51 +00:00
|
|
|
* @property {String} principalType Type of the principal; one of: APPLICATION, USER, ROLE.
|
2015-02-23 21:13:52 +00:00
|
|
|
* @property {String} principalId ID of the principal - such as appId, userId or roleId.
|
|
|
|
* @property {Object} settings Extends the `Model.settings` object.
|
|
|
|
* @property {String} settings.defaultPermission Default permission setting: ALLOW, DENY, ALARM, or AUDIT. Default is ALLOW.
|
|
|
|
* Set to DENY to prohibit all API access by default.
|
2014-10-13 08:55:08 +00:00
|
|
|
*
|
|
|
|
* @class ACL
|
|
|
|
* @inherits PersistedModel
|
2013-12-09 23:26:53 +00:00
|
|
|
*/
|
2014-05-31 02:29:30 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
module.exports = function(ACL) {
|
|
|
|
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
|
2015-04-03 14:41:32 +00:00
|
|
|
ACL.REPLICATE = AccessContext.REPLICATE; // Replicate (pull) changes
|
2014-10-13 08:55:08 +00:00
|
|
|
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;
|
|
|
|
|
2017-03-27 12:58:01 +00:00
|
|
|
ACL.DEFAULT_SCOPE = ctx.DEFAULT_SCOPES[0];
|
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
/**
|
|
|
|
* Calculate the matching score for the given rule and request
|
|
|
|
* @param {ACL} rule The ACL entry
|
|
|
|
* @param {AccessRequest} req The request
|
|
|
|
* @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;
|
2015-04-03 14:41:32 +00:00
|
|
|
var ruleValue = rule[props[i]] || ACL.ALL;
|
|
|
|
var requestedValue = req[props[i]] || ACL.ALL;
|
2016-04-01 09:14:26 +00:00
|
|
|
var isMatchingMethodName = props[i] === 'property' &&
|
|
|
|
req.methodNames.indexOf(ruleValue) !== -1;
|
2015-04-03 14:41:32 +00:00
|
|
|
|
|
|
|
var isMatchingAccessType = ruleValue === requestedValue;
|
|
|
|
if (props[i] === 'accessType' && !isMatchingAccessType) {
|
|
|
|
switch (ruleValue) {
|
|
|
|
case ACL.EXECUTE:
|
|
|
|
// EXECUTE should match READ, REPLICATE and WRITE
|
|
|
|
isMatchingAccessType = true;
|
|
|
|
break;
|
|
|
|
case ACL.WRITE:
|
|
|
|
// WRITE should match REPLICATE too
|
|
|
|
isMatchingAccessType = requestedValue === ACL.REPLICATE;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2015-01-14 21:38:44 +00:00
|
|
|
|
2015-04-03 14:41:32 +00:00
|
|
|
if (isMatchingMethodName || isMatchingAccessType) {
|
2014-10-13 08:55:08 +00:00
|
|
|
// Exact match
|
|
|
|
score += 3;
|
2015-04-03 14:41:32 +00:00
|
|
|
} else if (ruleValue === ACL.ALL) {
|
2014-10-13 08:55:08 +00:00
|
|
|
// Wildcard match
|
|
|
|
score += 2;
|
2015-04-03 14:41:32 +00:00
|
|
|
} else if (requestedValue === ACL.ALL) {
|
2014-10-13 08:55:08 +00:00
|
|
|
score += 1;
|
|
|
|
} else {
|
2015-01-14 21:38:44 +00:00
|
|
|
// Doesn't match at all
|
2014-10-13 08:55:08 +00:00
|
|
|
return -1;
|
|
|
|
}
|
2013-12-09 23:26:53 +00:00
|
|
|
}
|
2014-03-19 22:09:20 +00:00
|
|
|
|
2014-10-13 08:55:08 +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:
|
2014-03-19 22:09:20 +00:00
|
|
|
score += 4;
|
|
|
|
break;
|
2014-10-13 08:55:08 +00:00
|
|
|
case ACL.APP:
|
2014-03-19 22:09:20 +00:00
|
|
|
score += 3;
|
|
|
|
break;
|
2014-10-13 08:55:08 +00:00
|
|
|
case ACL.ROLE:
|
2014-03-19 22:09:20 +00:00
|
|
|
score += 2;
|
|
|
|
break;
|
|
|
|
default:
|
2014-10-13 08:55:08 +00:00
|
|
|
score += 1;
|
2014-03-19 22:09:20 +00:00
|
|
|
}
|
2014-06-02 20:41:14 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
// Weigh against the roles
|
|
|
|
// 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;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
2014-11-04 12:52:49 +00:00
|
|
|
};
|
2014-10-13 08:55:08 +00:00
|
|
|
|
|
|
|
/*!
|
|
|
|
* Resolve permission from the ACLs
|
|
|
|
* @param {Object[]) acls The list of ACLs
|
2016-12-06 14:24:49 +00:00
|
|
|
* @param {AccessRequest} req The access request
|
|
|
|
* @returns {AccessRequest} result The resolved access request
|
2014-10-13 08:55:08 +00:00
|
|
|
*/
|
|
|
|
ACL.resolvePermission = function resolvePermission(acls, req) {
|
|
|
|
if (!(req instanceof AccessRequest)) {
|
2016-12-06 14:24:49 +00:00
|
|
|
req.registry = this.registry;
|
2014-10-13 08:55:08 +00:00
|
|
|
req = new AccessRequest(req);
|
2013-12-09 23:26:53 +00:00
|
|
|
}
|
2014-10-13 08:55:08 +00:00
|
|
|
// 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++) {
|
2014-11-04 12:52:49 +00:00
|
|
|
var candidate = acls[i];
|
|
|
|
score = ACL.getMatchingScore(candidate, req);
|
2014-10-13 08:55:08 +00:00
|
|
|
if (score < 0) {
|
|
|
|
// the highest scored ACL did not match
|
2013-12-09 23:26:53 +00:00
|
|
|
break;
|
|
|
|
}
|
2014-10-13 08:55:08 +00:00
|
|
|
if (!req.isWildcard()) {
|
|
|
|
// We should stop from the first match for non-wildcard
|
2014-11-04 12:52:49 +00:00
|
|
|
permission = candidate.permission;
|
2014-10-13 08:55:08 +00:00
|
|
|
break;
|
|
|
|
} else {
|
2014-11-04 12:52:49 +00:00
|
|
|
if (req.exactlyMatches(candidate)) {
|
|
|
|
permission = candidate.permission;
|
2014-10-13 08:55:08 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
// For wildcard match, find the strongest permission
|
2014-11-04 12:52:49 +00:00
|
|
|
var candidateOrder = AccessContext.permissionOrder[candidate.permission];
|
|
|
|
var permissionOrder = AccessContext.permissionOrder[permission];
|
|
|
|
if (candidateOrder > permissionOrder) {
|
|
|
|
permission = candidate.permission;
|
2017-03-18 08:10:15 +00:00
|
|
|
break;
|
2014-10-13 08:55:08 +00:00
|
|
|
}
|
2013-11-15 17:41:26 +00:00
|
|
|
}
|
|
|
|
}
|
2013-12-11 05:49:18 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
if (debug.enabled) {
|
|
|
|
debug('The following ACLs were searched: ');
|
|
|
|
acls.forEach(function(acl) {
|
|
|
|
acl.debug();
|
|
|
|
debug('with score:', acl.score(req));
|
|
|
|
});
|
|
|
|
}
|
2016-12-06 14:24:49 +00:00
|
|
|
var res = new AccessRequest({
|
|
|
|
model: req.model,
|
|
|
|
property: req.property,
|
|
|
|
accessType: req.accessType,
|
|
|
|
permission: permission || ACL.DEFAULT,
|
|
|
|
registry: this.registry});
|
|
|
|
|
|
|
|
// Elucidate permission status if DEFAULT
|
|
|
|
res.settleDefaultPermission();
|
2013-11-15 17:41:26 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
return res;
|
|
|
|
};
|
|
|
|
|
|
|
|
/*!
|
|
|
|
* Get the static ACLs from the model definition
|
|
|
|
* @param {String} model The model name
|
|
|
|
* @param {String} property The property/method/relation name
|
|
|
|
*
|
|
|
|
* @return {Object[]} An array of ACLs
|
|
|
|
*/
|
|
|
|
ACL.getStaticACLs = function getStaticACLs(model, property) {
|
2016-07-26 12:10:13 +00:00
|
|
|
var modelClass = this.registry.findModel(model);
|
2014-10-13 08:55:08 +00:00
|
|
|
var staticACLs = [];
|
|
|
|
if (modelClass && modelClass.settings.acls) {
|
|
|
|
modelClass.settings.acls.forEach(function(acl) {
|
2015-03-05 10:35:18 +00:00
|
|
|
var prop = acl.property;
|
|
|
|
// We support static ACL property with array of string values.
|
|
|
|
if (Array.isArray(prop) && prop.indexOf(property) >= 0)
|
|
|
|
prop = property;
|
|
|
|
if (!prop || prop === ACL.ALL || property === prop) {
|
2014-10-13 08:55:08 +00:00
|
|
|
staticACLs.push(new ACL({
|
|
|
|
model: model,
|
2015-03-05 10:35:18 +00:00
|
|
|
property: prop || ACL.ALL,
|
2014-10-13 08:55:08 +00:00
|
|
|
principalType: acl.principalType,
|
|
|
|
principalId: acl.principalId, // TODO: Should it be a name?
|
|
|
|
accessType: acl.accessType || ACL.ALL,
|
2016-04-01 09:14:26 +00:00
|
|
|
permission: acl.permission,
|
2014-10-13 08:55:08 +00:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2014-11-04 12:52:49 +00:00
|
|
|
var prop = modelClass && (
|
|
|
|
// regular property
|
|
|
|
modelClass.definition.properties[property] ||
|
|
|
|
// relation/scope
|
|
|
|
(modelClass._scopeMeta && modelClass._scopeMeta[property]) ||
|
|
|
|
// static method
|
|
|
|
modelClass[property] ||
|
|
|
|
// prototype method
|
|
|
|
modelClass.prototype[property]);
|
2014-10-13 08:55:08 +00:00
|
|
|
if (prop && prop.acls) {
|
|
|
|
prop.acls.forEach(function(acl) {
|
2014-10-08 23:30:34 +00:00
|
|
|
staticACLs.push(new ACL({
|
2014-10-13 08:55:08 +00:00
|
|
|
model: modelClass.modelName,
|
|
|
|
property: property,
|
2014-10-08 23:30:34 +00:00
|
|
|
principalType: acl.principalType,
|
2014-10-13 08:55:08 +00:00
|
|
|
principalId: acl.principalId,
|
|
|
|
accessType: acl.accessType,
|
2016-04-01 09:14:26 +00:00
|
|
|
permission: acl.permission,
|
2014-10-08 23:30:34 +00:00
|
|
|
}));
|
2014-10-13 08:55:08 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return staticACLs;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
* @callback {Function} callback Callback function.
|
2016-12-06 14:24:49 +00:00
|
|
|
* @param {String|Error} err The error object.
|
|
|
|
* @param {AccessRequest} result The resolved access request.
|
2014-10-13 08:55:08 +00:00
|
|
|
*/
|
|
|
|
ACL.checkPermission = function checkPermission(principalType, principalId,
|
2017-12-12 08:33:15 +00:00
|
|
|
model, property, accessType,
|
|
|
|
callback) {
|
2017-01-30 13:56:57 +00:00
|
|
|
if (!callback) callback = utils.createPromiseCallback();
|
2014-10-13 08:55:08 +00:00
|
|
|
if (principalId !== null && principalId !== undefined && (typeof principalId !== 'string')) {
|
|
|
|
principalId = principalId.toString();
|
|
|
|
}
|
|
|
|
property = property || ACL.ALL;
|
2016-11-15 21:46:23 +00:00
|
|
|
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: [property, ACL.ALL]};
|
2014-10-13 08:55:08 +00:00
|
|
|
accessType = accessType || ACL.ALL;
|
2016-04-01 09:14:26 +00:00
|
|
|
var accessTypeQuery = (accessType === ACL.ALL) ? undefined :
|
2016-11-15 21:46:23 +00:00
|
|
|
{inq: [accessType, ACL.ALL, ACL.EXECUTE]};
|
2013-12-09 23:26:53 +00:00
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
var req = new AccessRequest({model, property, accessType, registry: this.registry});
|
2013-12-09 23:26:53 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
var acls = this.getStaticACLs(model, property);
|
2013-12-09 23:26:53 +00:00
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
// resolved is an instance of AccessRequest
|
2014-10-13 08:55:08 +00:00
|
|
|
var resolved = this.resolvePermission(acls, req);
|
2013-12-09 23:26:53 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
if (resolved && resolved.permission === ACL.DENY) {
|
|
|
|
debug('Permission denied by statically resolved permission');
|
|
|
|
debug(' Resolved Permission: %j', resolved);
|
|
|
|
process.nextTick(function() {
|
2017-01-30 13:56:57 +00:00
|
|
|
callback(null, resolved);
|
2014-10-13 08:55:08 +00:00
|
|
|
});
|
2017-01-30 13:56:57 +00:00
|
|
|
return callback.promise;
|
2014-10-13 08:55:08 +00:00
|
|
|
}
|
2013-11-15 17:41:26 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
var self = this;
|
2016-11-15 21:46:23 +00:00
|
|
|
this.find({where: {principalType: principalType, principalId: principalId,
|
2016-12-06 14:40:42 +00:00
|
|
|
model: model, property: propertyQuery, accessType: accessTypeQuery}},
|
2017-12-12 08:33:15 +00:00
|
|
|
function(err, dynACLs) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
acls = acls.concat(dynACLs);
|
|
|
|
// resolved is an instance of AccessRequest
|
|
|
|
resolved = self.resolvePermission(acls, req);
|
|
|
|
return callback(null, resolved);
|
|
|
|
});
|
2017-01-30 13:56:57 +00:00
|
|
|
return callback.promise;
|
2014-10-13 08:55:08 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
ACL.prototype.debug = function() {
|
|
|
|
if (debug.enabled) {
|
|
|
|
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);
|
|
|
|
}
|
2014-11-04 12:52:49 +00:00
|
|
|
};
|
2013-12-17 02:12:13 +00:00
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
// NOTE Regarding ACL.isAllowed() and ACL.prototype.isAllowed()
|
|
|
|
// Extending existing logic, including from ACL.checkAccessForContext() method,
|
|
|
|
// ACL instance with missing property `permission` are not promoted to
|
|
|
|
// permission = ACL.DEFAULT config. Such ACL instances will hence always be
|
|
|
|
// inefective
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test if ACL's permission is ALLOW
|
|
|
|
* @param {String} permission The permission to test, expects one of 'ALLOW', 'DENY', 'DEFAULT'
|
|
|
|
* @param {String} defaultPermission The default permission to apply if not providing a finite one in the permission parameter
|
|
|
|
* @returns {Boolean} true if ACL permission is ALLOW
|
|
|
|
*/
|
|
|
|
ACL.isAllowed = function(permission, defaultPermission) {
|
|
|
|
if (permission === ACL.DEFAULT) {
|
|
|
|
permission = defaultPermission || ACL.ALLOW;
|
|
|
|
}
|
|
|
|
return permission !== loopback.ACL.DENY;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test if ACL's permission is ALLOW
|
|
|
|
* @param {String} defaultPermission The default permission to apply if missing in ACL instance
|
|
|
|
* @returns {Boolean} true if ACL permission is ALLOW
|
|
|
|
*/
|
|
|
|
ACL.prototype.isAllowed = function(defaultPermission) {
|
|
|
|
return this.constructor.isAllowed(this.permission, defaultPermission);
|
|
|
|
};
|
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
/**
|
|
|
|
* Check if the request has the permission to access.
|
2016-12-06 14:24:49 +00:00
|
|
|
* @options {AccessContext|Object} context
|
|
|
|
* An AccessContext instance or a plain object with the following properties.
|
2014-10-13 08:55:08 +00:00
|
|
|
* @property {Object[]} principals An array of principals.
|
|
|
|
* @property {String|Model} model The model name or model class.
|
2016-12-06 14:24:49 +00:00
|
|
|
* @property {*} modelId The model instance ID.
|
2014-10-13 08:55:08 +00:00
|
|
|
* @property {String} property The property/method/relation name.
|
2015-04-03 14:41:32 +00:00
|
|
|
* @property {String} accessType The access type:
|
2016-12-06 14:24:49 +00:00
|
|
|
* READ, REPLICATE, WRITE, or EXECUTE.
|
|
|
|
* @callback {Function} callback Callback function
|
|
|
|
* @param {String|Error} err The error object.
|
|
|
|
* @param {AccessRequest} result The resolved access request.
|
2014-10-13 08:55:08 +00:00
|
|
|
*/
|
|
|
|
ACL.checkAccessForContext = function(context, callback) {
|
2017-01-30 13:56:57 +00:00
|
|
|
if (!callback) callback = utils.createPromiseCallback();
|
2016-08-16 15:03:21 +00:00
|
|
|
var self = this;
|
|
|
|
self.resolveRelatedModels();
|
|
|
|
var roleModel = self.roleModel;
|
2015-04-01 21:50:36 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
if (!(context instanceof AccessContext)) {
|
2016-12-06 14:24:49 +00:00
|
|
|
context.registry = this.registry;
|
2014-10-13 08:55:08 +00:00
|
|
|
context = new AccessContext(context);
|
|
|
|
}
|
2013-11-15 04:19:46 +00:00
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
var authorizedRoles = {};
|
|
|
|
var remotingContext = context.remotingContext;
|
2014-10-13 08:55:08 +00:00
|
|
|
var model = context.model;
|
2016-12-06 14:24:49 +00:00
|
|
|
var modelDefaultPermission = model && model.settings.defaultPermission;
|
2014-10-13 08:55:08 +00:00
|
|
|
var property = context.property;
|
|
|
|
var accessType = context.accessType;
|
|
|
|
var modelName = context.modelName;
|
2013-11-20 21:31:30 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
var methodNames = context.methodNames;
|
2016-11-15 21:46:23 +00:00
|
|
|
var propertyQuery = (property === ACL.ALL) ? undefined : {inq: methodNames.concat([ACL.ALL])};
|
2015-04-03 14:41:32 +00:00
|
|
|
|
|
|
|
var accessTypeQuery = (accessType === ACL.ALL) ?
|
|
|
|
undefined :
|
|
|
|
(accessType === ACL.REPLICATE) ?
|
2016-11-15 21:46:23 +00:00
|
|
|
{inq: [ACL.REPLICATE, ACL.WRITE, ACL.ALL]} :
|
|
|
|
{inq: [accessType, ACL.ALL]};
|
2013-11-20 21:31:30 +00:00
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
var req = new AccessRequest({
|
|
|
|
model: modelName,
|
|
|
|
property,
|
|
|
|
accessType,
|
|
|
|
permission: ACL.DEFAULT,
|
|
|
|
methodNames,
|
|
|
|
registry: this.registry});
|
2013-12-09 23:26:53 +00:00
|
|
|
|
2017-03-27 12:58:01 +00:00
|
|
|
if (!context.isScopeAllowed()) {
|
|
|
|
req.permission = ACL.DENY;
|
|
|
|
debug('--Denied by scope config--');
|
|
|
|
debug('Scopes allowed:', context.accessToken.scopes || ctx.DEFAULT_SCOPES);
|
|
|
|
debug('Scope required:', context.getScopes());
|
|
|
|
context.debug();
|
|
|
|
callback(null, req);
|
|
|
|
return callback.promise;
|
|
|
|
}
|
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
var effectiveACLs = [];
|
2016-08-16 15:03:21 +00:00
|
|
|
var staticACLs = self.getStaticACLs(model.modelName, property);
|
2013-11-20 21:31:30 +00:00
|
|
|
|
2016-11-15 21:46:23 +00:00
|
|
|
this.find({where: {model: model.modelName, property: propertyQuery,
|
|
|
|
accessType: accessTypeQuery}}, function(err, acls) {
|
2017-01-30 13:56:57 +00:00
|
|
|
if (err) return callback(err);
|
2014-10-13 08:55:08 +00:00
|
|
|
var inRoleTasks = [];
|
|
|
|
|
|
|
|
acls = acls.concat(staticACLs);
|
|
|
|
|
|
|
|
acls.forEach(function(acl) {
|
|
|
|
// Check exact matches
|
|
|
|
for (var i = 0; i < context.principals.length; i++) {
|
|
|
|
var p = context.principals[i];
|
2014-11-04 12:52:49 +00:00
|
|
|
var typeMatch = p.type === acl.principalType;
|
|
|
|
var idMatch = String(p.id) === String(acl.principalId);
|
|
|
|
if (typeMatch && idMatch) {
|
2014-10-13 08:55:08 +00:00
|
|
|
effectiveACLs.push(acl);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2013-12-11 05:49:18 +00:00
|
|
|
|
2014-10-13 08:55:08 +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);
|
2016-12-06 14:24:49 +00:00
|
|
|
// add the role to authorizedRoles if allowed
|
|
|
|
if (acl.isAllowed(modelDefaultPermission))
|
|
|
|
authorizedRoles[acl.principalId] = true;
|
2014-10-13 08:55:08 +00:00
|
|
|
}
|
|
|
|
done(err, acl);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2013-12-11 05:49:18 +00:00
|
|
|
|
2014-10-13 08:55:08 +00:00
|
|
|
async.parallel(inRoleTasks, function(err, results) {
|
2017-01-30 13:56:57 +00:00
|
|
|
if (err) return callback(err, null);
|
2015-04-03 14:41:32 +00:00
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
// resolved is an instance of AccessRequest
|
2014-10-13 08:55:08 +00:00
|
|
|
var resolved = self.resolvePermission(effectiveACLs, req);
|
|
|
|
debug('---Resolved---');
|
|
|
|
resolved.debug();
|
2016-12-06 14:24:49 +00:00
|
|
|
|
|
|
|
// set authorizedRoles in remotingContext options argument if
|
|
|
|
// resolved AccessRequest permission is ALLOW, else set it to empty object
|
|
|
|
authorizedRoles = resolved.isAllowed() ? authorizedRoles : {};
|
|
|
|
saveAuthorizedRolesToRemotingContext(remotingContext, authorizedRoles);
|
2017-01-30 13:56:57 +00:00
|
|
|
return callback(null, resolved);
|
2014-10-13 08:55:08 +00:00
|
|
|
});
|
|
|
|
});
|
2017-01-30 13:56:57 +00:00
|
|
|
return callback.promise;
|
2014-10-13 08:55:08 +00:00
|
|
|
};
|
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
function saveAuthorizedRolesToRemotingContext(remotingContext, authorizedRoles) {
|
|
|
|
const options = remotingContext && remotingContext.args && remotingContext.args.options;
|
|
|
|
// authorizedRoles key/value map is added to the options argument only if
|
|
|
|
// the latter exists and is an object. This means that the feature's availability
|
|
|
|
// will depend on the app configuration
|
|
|
|
if (options && typeof options === 'object') { // null is object too
|
|
|
|
options.authorizedRoles = authorizedRoles;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-10-13 08:55:08 +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
|
|
|
|
* @callback {Function} callback Callback function
|
|
|
|
* @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');
|
2017-01-30 13:56:57 +00:00
|
|
|
if (!callback) callback = utils.createPromiseCallback();
|
2014-10-13 08:55:08 +00:00
|
|
|
var context = new AccessContext({
|
2016-11-21 20:51:43 +00:00
|
|
|
registry: this.registry,
|
2014-10-13 08:55:08 +00:00
|
|
|
accessToken: token,
|
|
|
|
model: model,
|
|
|
|
property: method,
|
|
|
|
method: method,
|
2016-04-01 09:14:26 +00:00
|
|
|
modelId: modelId,
|
2013-11-20 21:31:30 +00:00
|
|
|
});
|
|
|
|
|
2016-12-06 14:24:49 +00:00
|
|
|
this.checkAccessForContext(context, function(err, accessRequest) {
|
2017-01-30 13:56:57 +00:00
|
|
|
if (err) callback(err);
|
2016-12-06 14:24:49 +00:00
|
|
|
else callback(null, accessRequest.isAllowed());
|
2013-11-20 21:31:30 +00:00
|
|
|
});
|
2017-01-30 13:56:57 +00:00
|
|
|
return callback.promise;
|
2014-10-13 08:55:08 +00:00
|
|
|
};
|
2015-08-13 15:58:41 +00:00
|
|
|
|
|
|
|
ACL.resolveRelatedModels = function() {
|
|
|
|
if (!this.roleModel) {
|
|
|
|
var reg = this.registry;
|
2016-08-16 15:02:34 +00:00
|
|
|
this.roleModel = reg.getModelByType('Role');
|
|
|
|
this.roleMappingModel = reg.getModelByType('RoleMapping');
|
|
|
|
this.userModel = reg.getModelByType('User');
|
|
|
|
this.applicationModel = reg.getModelByType('Application');
|
2015-08-13 15:58:41 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve a principal by type/id
|
|
|
|
* @param {String} type Principal type - ROLE/APP/USER
|
|
|
|
* @param {String|Number} id Principal id or name
|
2016-12-06 14:24:49 +00:00
|
|
|
* @callback {Function} callback Callback function
|
|
|
|
* @param {String|Error} err The error object
|
|
|
|
* @param {Object} result An instance of principal (Role, Application or User)
|
2015-08-13 15:58:41 +00:00
|
|
|
*/
|
|
|
|
ACL.resolvePrincipal = function(type, id, cb) {
|
2017-01-30 13:56:57 +00:00
|
|
|
cb = cb || utils.createPromiseCallback();
|
2015-08-13 15:58:41 +00:00
|
|
|
type = type || ACL.ROLE;
|
|
|
|
this.resolveRelatedModels();
|
2016-11-21 20:51:43 +00:00
|
|
|
|
2015-08-13 15:58:41 +00:00
|
|
|
switch (type) {
|
|
|
|
case ACL.ROLE:
|
2016-11-15 21:46:23 +00:00
|
|
|
this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb);
|
2015-08-13 15:58:41 +00:00
|
|
|
break;
|
|
|
|
case ACL.USER:
|
|
|
|
this.userModel.findOne(
|
2018-08-08 15:22:20 +00:00
|
|
|
{where: {or: [{username: id}, {email: id}, {id: id}]}}, cb
|
|
|
|
);
|
2015-08-13 15:58:41 +00:00
|
|
|
break;
|
|
|
|
case ACL.APP:
|
|
|
|
this.applicationModel.findOne(
|
2018-08-08 15:22:20 +00:00
|
|
|
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb
|
|
|
|
);
|
2015-08-13 15:58:41 +00:00
|
|
|
break;
|
|
|
|
default:
|
2016-12-06 14:24:49 +00:00
|
|
|
// try resolving a user model with a name matching the principalType
|
2016-11-21 20:51:43 +00:00
|
|
|
var userModel = this.registry.findModel(type);
|
|
|
|
if (userModel) {
|
|
|
|
userModel.findOne(
|
|
|
|
{where: {or: [{username: id}, {email: id}, {id: id}]}},
|
2018-08-08 15:22:20 +00:00
|
|
|
cb
|
|
|
|
);
|
2016-11-21 20:51:43 +00:00
|
|
|
} else {
|
|
|
|
process.nextTick(function() {
|
|
|
|
var err = new Error(g.f('Invalid principal type: %s', type));
|
|
|
|
err.statusCode = 400;
|
|
|
|
err.code = 'INVALID_PRINCIPAL_TYPE';
|
|
|
|
cb(err);
|
|
|
|
});
|
|
|
|
}
|
2015-08-13 15:58:41 +00:00
|
|
|
}
|
2017-01-30 13:56:57 +00:00
|
|
|
return cb.promise;
|
2015-08-13 15:58:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the given principal is mapped to the role
|
|
|
|
* @param {String} principalType Principal type
|
|
|
|
* @param {String|*} principalId Principal id/name
|
|
|
|
* @param {String|*} role Role id/name
|
2016-12-06 14:24:49 +00:00
|
|
|
* @callback {Function} callback Callback function
|
|
|
|
* @param {String|Error} err The error object
|
|
|
|
* @param {Boolean} isMapped is the ACL mapped to the role
|
2015-08-13 15:58:41 +00:00
|
|
|
*/
|
|
|
|
ACL.isMappedToRole = function(principalType, principalId, role, cb) {
|
2017-01-30 13:56:57 +00:00
|
|
|
cb = cb || utils.createPromiseCallback();
|
2015-08-13 15:58:41 +00:00
|
|
|
var self = this;
|
|
|
|
this.resolvePrincipal(principalType, principalId,
|
|
|
|
function(err, principal) {
|
|
|
|
if (err) return cb(err);
|
|
|
|
if (principal != null) {
|
|
|
|
principalId = principal.id;
|
|
|
|
}
|
|
|
|
principalType = principalType || 'ROLE';
|
|
|
|
self.resolvePrincipal('ROLE', role, function(err, role) {
|
|
|
|
if (err || !role) return cb(err, role);
|
|
|
|
self.roleMappingModel.findOne({
|
|
|
|
where: {
|
|
|
|
roleId: role.id,
|
|
|
|
principalType: principalType,
|
2016-04-01 09:14:26 +00:00
|
|
|
principalId: String(principalId),
|
|
|
|
},
|
2015-08-13 15:58:41 +00:00
|
|
|
}, function(err, result) {
|
|
|
|
if (err) return cb(err);
|
|
|
|
return cb(null, !!result);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2017-01-30 13:56:57 +00:00
|
|
|
return cb.promise;
|
2015-08-13 15:58:41 +00:00
|
|
|
};
|
2014-11-04 12:52:49 +00:00
|
|
|
};
|