Merge pull request #2957 from ebarault/propagate-resolved-roles-in-context
propagate resolved roles in remoting context
This commit is contained in:
commit
cb7e2114ec
|
@ -34,6 +34,7 @@ var g = require('../../lib/globalize');
|
||||||
var loopback = require('../../lib/loopback');
|
var loopback = require('../../lib/loopback');
|
||||||
var utils = require('../../lib/utils');
|
var utils = require('../../lib/utils');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
var extend = require('util')._extend;
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
var debug = require('debug')('loopback:security:acl');
|
var debug = require('debug')('loopback:security:acl');
|
||||||
|
|
||||||
|
@ -204,11 +205,12 @@ module.exports = function(ACL) {
|
||||||
/*!
|
/*!
|
||||||
* 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 {AccessRequest} req The access request
|
||||||
* @returns {AccessRequest} result The effective ACL
|
* @returns {AccessRequest} result The resolved access request
|
||||||
*/
|
*/
|
||||||
ACL.resolvePermission = function resolvePermission(acls, req) {
|
ACL.resolvePermission = function resolvePermission(acls, req) {
|
||||||
if (!(req instanceof AccessRequest)) {
|
if (!(req instanceof AccessRequest)) {
|
||||||
|
req.registry = this.registry;
|
||||||
req = new AccessRequest(req);
|
req = new AccessRequest(req);
|
||||||
}
|
}
|
||||||
// Sort by the matching score in descending order
|
// Sort by the matching score in descending order
|
||||||
|
@ -250,9 +252,16 @@ module.exports = function(ACL) {
|
||||||
debug('with score:', acl.score(req));
|
debug('with score:', acl.score(req));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
|
||||||
var res = new AccessRequest(req.model, req.property, req.accessType,
|
|
||||||
permission || ACL.DEFAULT);
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -316,8 +325,8 @@ module.exports = function(ACL) {
|
||||||
* @param {String} property The property/method/relation name.
|
* @param {String} property The property/method/relation name.
|
||||||
* @param {String} accessType The access type.
|
* @param {String} accessType The access type.
|
||||||
* @callback {Function} callback Callback function.
|
* @callback {Function} callback Callback function.
|
||||||
* @param {String|Error} err The error object
|
* @param {String|Error} err The error object.
|
||||||
* @param {AccessRequest} result The access permission
|
* @param {AccessRequest} result The resolved access request.
|
||||||
*/
|
*/
|
||||||
ACL.checkPermission = function checkPermission(principalType, principalId,
|
ACL.checkPermission = function checkPermission(principalType, principalId,
|
||||||
model, property, accessType,
|
model, property, accessType,
|
||||||
|
@ -332,10 +341,11 @@ module.exports = function(ACL) {
|
||||||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined :
|
var accessTypeQuery = (accessType === ACL.ALL) ? undefined :
|
||||||
{inq: [accessType, ACL.ALL, ACL.EXECUTE]};
|
{inq: [accessType, ACL.ALL, ACL.EXECUTE]};
|
||||||
|
|
||||||
var req = new AccessRequest(model, property, accessType);
|
var req = new AccessRequest({model, property, accessType, registry: this.registry});
|
||||||
|
|
||||||
var acls = this.getStaticACLs(model, property);
|
var acls = this.getStaticACLs(model, property);
|
||||||
|
|
||||||
|
// resolved is an instance of AccessRequest
|
||||||
var resolved = this.resolvePermission(acls, req);
|
var resolved = this.resolvePermission(acls, req);
|
||||||
|
|
||||||
if (resolved && resolved.permission === ACL.DENY) {
|
if (resolved && resolved.permission === ACL.DENY) {
|
||||||
|
@ -355,11 +365,8 @@ module.exports = function(ACL) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
acls = acls.concat(dynACLs);
|
acls = acls.concat(dynACLs);
|
||||||
|
// resolved is an instance of AccessRequest
|
||||||
resolved = self.resolvePermission(acls, req);
|
resolved = self.resolvePermission(acls, req);
|
||||||
if (resolved && resolved.permission === ACL.DEFAULT) {
|
|
||||||
var modelClass = self.registry.findModel(model);
|
|
||||||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW;
|
|
||||||
}
|
|
||||||
return callback(null, resolved);
|
return callback(null, resolved);
|
||||||
});
|
});
|
||||||
return callback.promise;
|
return callback.promise;
|
||||||
|
@ -377,30 +384,63 @@ module.exports = function(ACL) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the request has the permission to access.
|
* Check if the request has the permission to access.
|
||||||
* @options {Object} context See below.
|
* @options {AccessContext|Object} context
|
||||||
|
* An AccessContext instance or a plain object with the following properties.
|
||||||
* @property {Object[]} principals An array of principals.
|
* @property {Object[]} principals An array of principals.
|
||||||
* @property {String|Model} model The model name or model class.
|
* @property {String|Model} model The model name or model class.
|
||||||
* @property {*} id The model instance ID.
|
* @property {*} modelId The model instance ID.
|
||||||
* @property {String} property The property/method/relation name.
|
* @property {String} property The property/method/relation name.
|
||||||
* @property {String} accessType The access type:
|
* @property {String} accessType The access type:
|
||||||
* READ, REPLICATE, WRITE, or EXECUTE.
|
* READ, REPLICATE, WRITE, or EXECUTE.
|
||||||
* @param {Function} callback Callback function
|
* @callback {Function} callback Callback function
|
||||||
|
* @param {String|Error} err The error object.
|
||||||
|
* @param {AccessRequest} result The resolved access request.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ACL.checkAccessForContext = function(context, callback) {
|
ACL.checkAccessForContext = function(context, callback) {
|
||||||
if (!callback) callback = utils.createPromiseCallback();
|
if (!callback) callback = utils.createPromiseCallback();
|
||||||
var self = this;
|
var self = this;
|
||||||
self.resolveRelatedModels();
|
self.resolveRelatedModels();
|
||||||
var roleModel = self.roleModel;
|
var roleModel = self.roleModel;
|
||||||
|
|
||||||
context.registry = this.registry;
|
|
||||||
if (!(context instanceof AccessContext)) {
|
if (!(context instanceof AccessContext)) {
|
||||||
|
context.registry = this.registry;
|
||||||
context = new AccessContext(context);
|
context = new AccessContext(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authorizedRoles = {};
|
||||||
|
var remotingContext = context.remotingContext;
|
||||||
var model = context.model;
|
var model = context.model;
|
||||||
|
var modelDefaultPermission = model && model.settings.defaultPermission;
|
||||||
var property = context.property;
|
var property = context.property;
|
||||||
var accessType = context.accessType;
|
var accessType = context.accessType;
|
||||||
var modelName = context.modelName;
|
var modelName = context.modelName;
|
||||||
|
@ -414,7 +454,13 @@ module.exports = function(ACL) {
|
||||||
{inq: [ACL.REPLICATE, ACL.WRITE, ACL.ALL]} :
|
{inq: [ACL.REPLICATE, ACL.WRITE, ACL.ALL]} :
|
||||||
{inq: [accessType, ACL.ALL]};
|
{inq: [accessType, ACL.ALL]};
|
||||||
|
|
||||||
var req = new AccessRequest(modelName, property, accessType, ACL.DEFAULT, methodNames);
|
var req = new AccessRequest({
|
||||||
|
model: modelName,
|
||||||
|
property,
|
||||||
|
accessType,
|
||||||
|
permission: ACL.DEFAULT,
|
||||||
|
methodNames,
|
||||||
|
registry: this.registry});
|
||||||
|
|
||||||
var effectiveACLs = [];
|
var effectiveACLs = [];
|
||||||
var staticACLs = self.getStaticACLs(model.modelName, property);
|
var staticACLs = self.getStaticACLs(model.modelName, property);
|
||||||
|
@ -445,6 +491,9 @@ module.exports = function(ACL) {
|
||||||
function(err, inRole) {
|
function(err, inRole) {
|
||||||
if (!err && inRole) {
|
if (!err && inRole) {
|
||||||
effectiveACLs.push(acl);
|
effectiveACLs.push(acl);
|
||||||
|
// add the role to authorizedRoles if allowed
|
||||||
|
if (acl.isAllowed(modelDefaultPermission))
|
||||||
|
authorizedRoles[acl.principalId] = true;
|
||||||
}
|
}
|
||||||
done(err, acl);
|
done(err, acl);
|
||||||
});
|
});
|
||||||
|
@ -455,18 +504,31 @@ module.exports = function(ACL) {
|
||||||
async.parallel(inRoleTasks, function(err, results) {
|
async.parallel(inRoleTasks, function(err, results) {
|
||||||
if (err) return callback(err, null);
|
if (err) return callback(err, null);
|
||||||
|
|
||||||
|
// resolved is an instance of AccessRequest
|
||||||
var resolved = self.resolvePermission(effectiveACLs, req);
|
var resolved = self.resolvePermission(effectiveACLs, req);
|
||||||
if (resolved && resolved.permission === ACL.DEFAULT) {
|
|
||||||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW;
|
|
||||||
}
|
|
||||||
debug('---Resolved---');
|
debug('---Resolved---');
|
||||||
resolved.debug();
|
resolved.debug();
|
||||||
|
|
||||||
|
// 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);
|
||||||
return callback(null, resolved);
|
return callback(null, resolved);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return callback.promise;
|
return callback.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
@ -489,9 +551,9 @@ module.exports = function(ACL) {
|
||||||
modelId: modelId,
|
modelId: modelId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.checkAccessForContext(context, function(err, access) {
|
this.checkAccessForContext(context, function(err, accessRequest) {
|
||||||
if (err) callback(err);
|
if (err) callback(err);
|
||||||
else callback(null, access.permission !== ACL.DENY);
|
else callback(null, accessRequest.isAllowed());
|
||||||
});
|
});
|
||||||
return callback.promise;
|
return callback.promise;
|
||||||
};
|
};
|
||||||
|
@ -510,7 +572,9 @@ module.exports = function(ACL) {
|
||||||
* Resolve a principal by type/id
|
* Resolve a principal by type/id
|
||||||
* @param {String} type Principal type - ROLE/APP/USER
|
* @param {String} type Principal type - ROLE/APP/USER
|
||||||
* @param {String|Number} id Principal id or name
|
* @param {String|Number} id Principal id or name
|
||||||
* @param {Function} cb Callback function
|
* @callback {Function} callback Callback function
|
||||||
|
* @param {String|Error} err The error object
|
||||||
|
* @param {Object} result An instance of principal (Role, Application or User)
|
||||||
*/
|
*/
|
||||||
ACL.resolvePrincipal = function(type, id, cb) {
|
ACL.resolvePrincipal = function(type, id, cb) {
|
||||||
cb = cb || utils.createPromiseCallback();
|
cb = cb || utils.createPromiseCallback();
|
||||||
|
@ -530,7 +594,7 @@ module.exports = function(ACL) {
|
||||||
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb);
|
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// try resolving a user model that matches principalType
|
// try resolving a user model with a name matching the principalType
|
||||||
var userModel = this.registry.findModel(type);
|
var userModel = this.registry.findModel(type);
|
||||||
if (userModel) {
|
if (userModel) {
|
||||||
userModel.findOne(
|
userModel.findOne(
|
||||||
|
@ -553,7 +617,9 @@ module.exports = function(ACL) {
|
||||||
* @param {String} principalType Principal type
|
* @param {String} principalType Principal type
|
||||||
* @param {String|*} principalId Principal id/name
|
* @param {String|*} principalId Principal id/name
|
||||||
* @param {String|*} role Role id/name
|
* @param {String|*} role Role id/name
|
||||||
* @param {Function} cb Callback function
|
* @callback {Function} callback Callback function
|
||||||
|
* @param {String|Error} err The error object
|
||||||
|
* @param {Boolean} isMapped is the ACL mapped to the role
|
||||||
*/
|
*/
|
||||||
ACL.isMappedToRole = function(principalType, principalId, role, cb) {
|
ACL.isMappedToRole = function(principalType, principalId, role, cb) {
|
||||||
cb = cb || utils.createPromiseCallback();
|
cb = cb || utils.createPromiseCallback();
|
||||||
|
|
|
@ -12,17 +12,29 @@ var debug = require('debug')('loopback:security:access-context');
|
||||||
* Access context represents the context for a request to access protected
|
* Access context represents the context for a request to access protected
|
||||||
* resources
|
* resources
|
||||||
*
|
*
|
||||||
|
* NOTE While the method expects an array of principals in the AccessContext instance/object,
|
||||||
|
* it also accepts a single principal defined with the following properties:
|
||||||
|
* ```js
|
||||||
|
* {
|
||||||
|
* // AccessContext instance/object
|
||||||
|
* // ..
|
||||||
|
* principalType: 'somePrincipalType', // APP, ROLE, USER, or custom user model name
|
||||||
|
* principalId: 'somePrincipalId',
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
* @class
|
* @class
|
||||||
* @options {Object} context The context object
|
* @options {AccessContext|Object} context An AccessContext instance or an object
|
||||||
* @property {Principal[]} principals An array of principals
|
* @property {Principal[]} principals An array of principals
|
||||||
* @property {Function} model The model class
|
* @property {Function} model The model class
|
||||||
* @property {String} modelName The model name
|
* @property {String} modelName The model name
|
||||||
* @property {String} modelId The model id
|
* @property {*} modelId The model id
|
||||||
* @property {String} property The model property/method/relation name
|
* @property {String} property The model property/method/relation name
|
||||||
* @property {String} method The model method to be invoked
|
* @property {String} method The model method to be invoked
|
||||||
* @property {String} accessType The access type
|
* @property {String} accessType The access type: READ, REPLICATE, WRITE, or EXECUTE.
|
||||||
* @property {AccessToken} accessToken The access token
|
* @property {AccessToken} accessToken The access token resolved for the request
|
||||||
*
|
* @property {RemotingContext} remotingContext The request's remoting context
|
||||||
|
* @property {Registry} registry The application or global registry
|
||||||
* @returns {AccessContext}
|
* @returns {AccessContext}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
|
@ -250,16 +262,23 @@ Principal.prototype.equals = function(p) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A request to access protected resources.
|
* A request to access protected resources.
|
||||||
* @param {String} model The model name
|
*
|
||||||
* @param {String} property
|
* The method can either be called with the following signature or with a single
|
||||||
|
* argument: an AccessRequest instance or an object containing all the required properties.
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
* @options {String|AccessRequest|Object} model|req The model name,<br>
|
||||||
|
* or an AccessRequest instance/object.
|
||||||
|
* @param {String} property The property/method/relation name
|
||||||
* @param {String} accessType The access type
|
* @param {String} accessType The access type
|
||||||
* @param {String} permission The requested permission
|
* @param {String} permission The requested permission
|
||||||
|
* @param {String[]} methodNames The names of involved methods
|
||||||
|
* @param {Registry} registry The application or global registry
|
||||||
* @returns {AccessRequest}
|
* @returns {AccessRequest}
|
||||||
* @class
|
|
||||||
*/
|
*/
|
||||||
function AccessRequest(model, property, accessType, permission, methodNames) {
|
function AccessRequest(model, property, accessType, permission, methodNames, registry) {
|
||||||
if (!(this instanceof AccessRequest)) {
|
if (!(this instanceof AccessRequest)) {
|
||||||
return new AccessRequest(model, property, accessType);
|
return new AccessRequest(model, property, accessType, permission, methodNames);
|
||||||
}
|
}
|
||||||
if (arguments.length === 1 && typeof model === 'object') {
|
if (arguments.length === 1 && typeof model === 'object') {
|
||||||
// The argument is an object that contains all required properties
|
// The argument is an object that contains all required properties
|
||||||
|
@ -268,14 +287,19 @@ function AccessRequest(model, property, accessType, permission, methodNames) {
|
||||||
this.property = obj.property || AccessContext.ALL;
|
this.property = obj.property || AccessContext.ALL;
|
||||||
this.accessType = obj.accessType || AccessContext.ALL;
|
this.accessType = obj.accessType || AccessContext.ALL;
|
||||||
this.permission = obj.permission || AccessContext.DEFAULT;
|
this.permission = obj.permission || AccessContext.DEFAULT;
|
||||||
this.methodNames = methodNames || [];
|
this.methodNames = obj.methodNames || [];
|
||||||
|
this.registry = obj.registry;
|
||||||
} else {
|
} else {
|
||||||
this.model = model || AccessContext.ALL;
|
this.model = model || AccessContext.ALL;
|
||||||
this.property = property || AccessContext.ALL;
|
this.property = property || AccessContext.ALL;
|
||||||
this.accessType = accessType || AccessContext.ALL;
|
this.accessType = accessType || AccessContext.ALL;
|
||||||
this.permission = permission || AccessContext.DEFAULT;
|
this.permission = permission || AccessContext.DEFAULT;
|
||||||
this.methodNames = methodNames || [];
|
this.methodNames = methodNames || [];
|
||||||
|
this.registry = registry;
|
||||||
}
|
}
|
||||||
|
// do not create AccessRequest without a registry
|
||||||
|
assert(this.registry,
|
||||||
|
'Application registry is mandatory in AccessRequest but missing in provided argument(s)');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,6 +332,28 @@ AccessRequest.prototype.exactlyMatches = function(acl) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settle the accessRequest's permission if DEFAULT
|
||||||
|
* In most situations, the default permission can be resolved from the nested model
|
||||||
|
* config. An default permission can also be explicitly provided to override it or
|
||||||
|
* cope with AccessRequest instances without a nested model (e.g. model is '*')
|
||||||
|
*
|
||||||
|
* @param {String} defaultPermission (optional) the default permission to apply
|
||||||
|
*/
|
||||||
|
|
||||||
|
AccessRequest.prototype.settleDefaultPermission = function(defaultPermission) {
|
||||||
|
if (this.permission !== 'DEFAULT')
|
||||||
|
return;
|
||||||
|
|
||||||
|
var modelName = this.model;
|
||||||
|
if (!defaultPermission) {
|
||||||
|
var modelClass = this.registry.findModel(modelName);
|
||||||
|
defaultPermission = modelClass && modelClass.settings.defaultPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.permission = defaultPermission || 'ALLOW';
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is the request for access allowed?
|
* Is the request for access allowed?
|
||||||
*
|
*
|
||||||
|
|
137
test/acl.test.js
137
test/acl.test.js
|
@ -5,10 +5,13 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
var assert = require('assert');
|
var assert = require('assert');
|
||||||
|
var expect = require('./helpers/expect');
|
||||||
var loopback = require('../index');
|
var loopback = require('../index');
|
||||||
var Scope = loopback.Scope;
|
var Scope = loopback.Scope;
|
||||||
var ACL = loopback.ACL;
|
var ACL = loopback.ACL;
|
||||||
var request = require('supertest');
|
var request = require('supertest');
|
||||||
|
var Promise = require('bluebird');
|
||||||
|
var supertest = require('supertest');
|
||||||
var Role = loopback.Role;
|
var Role = loopback.Role;
|
||||||
var RoleMapping = loopback.RoleMapping;
|
var RoleMapping = loopback.RoleMapping;
|
||||||
var User = loopback.User;
|
var User = loopback.User;
|
||||||
|
@ -157,11 +160,24 @@ describe('security ACLs', function() {
|
||||||
acls = acls.map(function(a) { return new ACL(a); });
|
acls = acls.map(function(a) { return new ACL(a); });
|
||||||
|
|
||||||
var perm = ACL.resolvePermission(acls, req);
|
var perm = ACL.resolvePermission(acls, req);
|
||||||
|
// remove the registry from AccessRequest instance to ease asserting
|
||||||
|
delete perm.registry;
|
||||||
assert.deepEqual(perm, {model: 'account',
|
assert.deepEqual(perm, {model: 'account',
|
||||||
property: 'find',
|
property: 'find',
|
||||||
accessType: 'WRITE',
|
accessType: 'WRITE',
|
||||||
permission: 'ALLOW',
|
permission: 'ALLOW',
|
||||||
methodNames: []});
|
methodNames: []});
|
||||||
|
|
||||||
|
// NOTE: when fixed in chaijs, use this implement rather than modifying
|
||||||
|
// the resolved access request
|
||||||
|
//
|
||||||
|
// expect(perm).to.deep.include({
|
||||||
|
// model: 'account',
|
||||||
|
// property: 'find',
|
||||||
|
// accessType: 'WRITE',
|
||||||
|
// permission: 'ALLOW',
|
||||||
|
// methodNames: [],
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow access to models for the given principal by wildcard', function() {
|
it('should allow access to models for the given principal by wildcard', function() {
|
||||||
|
@ -250,6 +266,7 @@ describe('security ACLs', function() {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ACL default permission is to DENY for model Customer
|
||||||
Customer.settings.defaultPermission = ACL.DENY;
|
Customer.settings.defaultPermission = ACL.DENY;
|
||||||
|
|
||||||
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE,
|
ACL.checkPermission(ACL.USER, 'u001', 'Customer', 'name', ACL.WRITE,
|
||||||
|
@ -472,3 +489,123 @@ describe('access check', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('authorized roles propagation in RemotingContext', function() {
|
||||||
|
var app, request, accessToken;
|
||||||
|
var models = {};
|
||||||
|
|
||||||
|
beforeEach(setupAppAndRequest);
|
||||||
|
|
||||||
|
it('contains all authorized roles for a principal if query is allowed', function() {
|
||||||
|
return createACLs('MyTestModel', [
|
||||||
|
{permission: ACL.ALLOW, principalId: '$everyone'},
|
||||||
|
{permission: ACL.ALLOW, principalId: '$authenticated'},
|
||||||
|
{permission: ACL.ALLOW, principalId: 'myRole'},
|
||||||
|
])
|
||||||
|
.then(makeAuthorizedHttpRequestOnMyTestModel)
|
||||||
|
.then(function() {
|
||||||
|
var ctx = models.MyTestModel.lastRemotingContext;
|
||||||
|
expect(ctx.args.options.authorizedRoles).to.eql(
|
||||||
|
{
|
||||||
|
$everyone: true,
|
||||||
|
$authenticated: true,
|
||||||
|
myRole: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain any denied role even if query is allowed', function() {
|
||||||
|
return createACLs('MyTestModel', [
|
||||||
|
{permission: ACL.ALLOW, principalId: '$everyone'},
|
||||||
|
{permission: ACL.DENY, principalId: '$authenticated'},
|
||||||
|
{permission: ACL.ALLOW, principalId: 'myRole'},
|
||||||
|
])
|
||||||
|
.then(makeAuthorizedHttpRequestOnMyTestModel)
|
||||||
|
.then(function() {
|
||||||
|
var ctx = models.MyTestModel.lastRemotingContext;
|
||||||
|
expect(ctx.args.options.authorizedRoles).to.eql(
|
||||||
|
{
|
||||||
|
$everyone: true,
|
||||||
|
myRole: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors default permission setting', function() {
|
||||||
|
// default permission is set to DENY for MyTestModel
|
||||||
|
models.MyTestModel.settings.defaultPermission = ACL.DENY;
|
||||||
|
|
||||||
|
return createACLs('MyTestModel', [
|
||||||
|
{permission: ACL.DEFAULT, principalId: '$everyone'},
|
||||||
|
{permission: ACL.DENY, principalId: '$authenticated'},
|
||||||
|
{permission: ACL.ALLOW, principalId: 'myRole'},
|
||||||
|
])
|
||||||
|
.then(makeAuthorizedHttpRequestOnMyTestModel)
|
||||||
|
.then(function() {
|
||||||
|
var ctx = models.MyTestModel.lastRemotingContext;
|
||||||
|
expect(ctx.args.options.authorizedRoles).to.eql(
|
||||||
|
// '$everyone' is not expected as default permission is DENY
|
||||||
|
{myRole: true}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
function setupAppAndRequest() {
|
||||||
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.use(loopback.rest());
|
||||||
|
app.set('remoting', {errorHandler: {debug: true, log: true}});
|
||||||
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
request = supertest(app);
|
||||||
|
|
||||||
|
app.enableAuth({dataSource: 'db'});
|
||||||
|
models = app.models;
|
||||||
|
|
||||||
|
// creating a custom model
|
||||||
|
const MyTestModel = app.registry.createModel('MyTestModel');
|
||||||
|
app.model(MyTestModel, {dataSource: 'db'});
|
||||||
|
|
||||||
|
// capturing the value of the last remoting context
|
||||||
|
models.MyTestModel.beforeRemote('find', function(ctx, unused, next) {
|
||||||
|
models.MyTestModel.lastRemotingContext = ctx;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// creating a user, a role and a rolemapping binding that user with that role
|
||||||
|
return Promise.all([
|
||||||
|
models.User.create({username: 'myUser', email: 'myuser@example.com', password: 'pass'}),
|
||||||
|
models.Role.create({name: 'myRole'}),
|
||||||
|
])
|
||||||
|
.spread(function(myUser, myRole) {
|
||||||
|
return Promise.all([
|
||||||
|
myRole.principals.create({principalType: 'USER', principalId: myUser.id}),
|
||||||
|
models.User.login({username: 'myUser', password: 'pass'}),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.spread(function(role, token) {
|
||||||
|
accessToken = token;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createACLs(model, acls) {
|
||||||
|
acls = acls.map(function(acl) {
|
||||||
|
return models.ACL.create({
|
||||||
|
principalType: acl.principalType || ACL.ROLE,
|
||||||
|
principalId: acl.principalId,
|
||||||
|
model: acl.model || model,
|
||||||
|
property: acl.property || ACL.ALL,
|
||||||
|
accessType: acl.accessType || ACL.ALL,
|
||||||
|
permission: acl.permission,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Promise.all(acls);
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeAuthorizedHttpRequestOnMyTestModel() {
|
||||||
|
return request.get('/MyTestModels')
|
||||||
|
.set('X-Access-Token', accessToken.id)
|
||||||
|
.expect(200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -897,7 +897,7 @@ describe.onServer('Remote Methods', function() {
|
||||||
request(app).get('/TestModels/saveOptions')
|
request(app).get('/TestModels/saveOptions')
|
||||||
.expect(204, function(err) {
|
.expect(204, function(err) {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
expect(actualOptions).to.eql({accessToken: null});
|
expect(actualOptions).to.include({accessToken: null});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue