Merge pull request #3140 from pierreclr/feature/allow-mutiple-owners-resolving
Fix OWNER role to handle multiple relations
This commit is contained in:
commit
658d228789
|
@ -222,6 +222,8 @@ module.exports = function(Role) {
|
||||||
* @promise
|
* @promise
|
||||||
*/
|
*/
|
||||||
Role.isOwner = function isOwner(modelClass, modelId, userId, principalType, options, callback) {
|
Role.isOwner = function isOwner(modelClass, modelId, userId, principalType, options, callback) {
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
if (!callback && typeof options === 'function') {
|
if (!callback && typeof options === 'function') {
|
||||||
callback = options;
|
callback = options;
|
||||||
options = {};
|
options = {};
|
||||||
|
@ -238,7 +240,7 @@ module.exports = function(Role) {
|
||||||
debug('isOwner(): %s %s userId: %s principalType: %s',
|
debug('isOwner(): %s %s userId: %s principalType: %s',
|
||||||
modelClass && modelClass.modelName, modelId, userId, principalType);
|
modelClass && modelClass.modelName, modelId, userId, principalType);
|
||||||
|
|
||||||
// Return false if userId is missing
|
// Resolve isOwner false if userId is missing
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
|
@ -246,6 +248,23 @@ module.exports = function(Role) {
|
||||||
return callback.promise;
|
return callback.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// At this stage, principalType is valid in one of 2 following condition:
|
||||||
|
// 1. the app has a single user model and principalType is 'USER'
|
||||||
|
// 2. the app has multiple user models and principalType is not 'USER'
|
||||||
|
// multiple user models
|
||||||
|
var isMultipleUsers = _isMultipleUsers();
|
||||||
|
var isPrincipalTypeValid =
|
||||||
|
(!isMultipleUsers && principalType === Principal.USER) ||
|
||||||
|
(isMultipleUsers && principalType !== Principal.USER);
|
||||||
|
|
||||||
|
// Resolve isOwner false if principalType is invalid
|
||||||
|
if (!isPrincipalTypeValid) {
|
||||||
|
process.nextTick(function() {
|
||||||
|
callback(null, false);
|
||||||
|
});
|
||||||
|
return callback.promise;
|
||||||
|
}
|
||||||
|
|
||||||
// Is the modelClass User or a subclass of User?
|
// Is the modelClass User or a subclass of User?
|
||||||
if (isUserClass(modelClass)) {
|
if (isUserClass(modelClass)) {
|
||||||
var userModelName = modelClass.modelName;
|
var userModelName = modelClass.modelName;
|
||||||
|
@ -265,10 +284,25 @@ module.exports = function(Role) {
|
||||||
}
|
}
|
||||||
debug('Model found: %j', inst);
|
debug('Model found: %j', inst);
|
||||||
|
|
||||||
// Historically, for principalType USER, we were resolving isOwner()
|
var ownerRelations = modelClass.settings.ownerRelations;
|
||||||
// as true if the model has "userId" or "owner" property matching
|
if (!ownerRelations) {
|
||||||
// id of the current user (principalId), even though there was no
|
return legacyOwnershipCheck(inst);
|
||||||
// belongsTo relation set up.
|
} else {
|
||||||
|
return checkOwnership(inst);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return callback.promise;
|
||||||
|
|
||||||
|
// NOTE Historically, for principalType USER, we were resolving isOwner()
|
||||||
|
// as true if the model has "userId" or "owner" property matching
|
||||||
|
// id of the current user (principalId), even though there was no
|
||||||
|
// belongsTo relation set up.
|
||||||
|
// Additionaly, the original implementation did not support the
|
||||||
|
// possibility for a model to have multiple related users: when
|
||||||
|
// testing belongsTo relations, the first related user failing the
|
||||||
|
// ownership check induced the whole isOwner() to resolve as false.
|
||||||
|
// This behaviour will be pruned at next LoopBack major release.
|
||||||
|
function legacyOwnershipCheck(inst) {
|
||||||
var ownerId = inst.userId || inst.owner;
|
var ownerId = inst.userId || inst.owner;
|
||||||
if (principalType === Principal.USER && ownerId && 'function' !== typeof ownerId) {
|
if (principalType === Principal.USER && ownerId && 'function' !== typeof ownerId) {
|
||||||
return callback(null, matches(ownerId, userId));
|
return callback(null, matches(ownerId, userId));
|
||||||
|
@ -282,9 +316,16 @@ module.exports = function(Role) {
|
||||||
if (!belongsToUser) {
|
if (!belongsToUser) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// checking related user
|
// checking related user
|
||||||
var userModelName = rel.modelTo.modelName;
|
var relatedUser = rel.modelTo;
|
||||||
if (principalType === Principal.USER || principalType === userModelName) {
|
var userModelName = relatedUser.modelName;
|
||||||
|
var isMultipleUsers = _isMultipleUsers(relatedUser);
|
||||||
|
// a relation can be considered for isOwner resolution if:
|
||||||
|
// 1. the app has a single user model and principalType is 'USER'
|
||||||
|
// 2. the app has multiple user models and principalType is the related user model name
|
||||||
|
if ((!isMultipleUsers && principalType === Principal.USER) ||
|
||||||
|
(isMultipleUsers && principalType === userModelName)) {
|
||||||
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
||||||
inst[r](processRelatedUser);
|
inst[r](processRelatedUser);
|
||||||
return;
|
return;
|
||||||
|
@ -295,15 +336,72 @@ module.exports = function(Role) {
|
||||||
callback(null, false);
|
callback(null, false);
|
||||||
|
|
||||||
function processRelatedUser(err, user) {
|
function processRelatedUser(err, user) {
|
||||||
if (!err && user) {
|
if (err || !user) return callback(err, false);
|
||||||
debug('User found: %j', user.id);
|
|
||||||
callback(null, matches(user.id, userId));
|
debug('User found: %j', user.id);
|
||||||
} else {
|
callback(null, matches(user.id, userId));
|
||||||
callback(err, false);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkOwnership(inst) {
|
||||||
|
var ownerRelations = inst.constructor.settings.ownerRelations;
|
||||||
|
// collecting related users
|
||||||
|
var relWithUsers = [];
|
||||||
|
for (var r in modelClass.relations) {
|
||||||
|
var rel = modelClass.relations[r];
|
||||||
|
// relation should be belongsTo and target a User based class
|
||||||
|
if (rel.type !== 'belongsTo' || !isUserClass(rel.modelTo)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// checking related user
|
||||||
|
var relatedUser = rel.modelTo;
|
||||||
|
var userModelName = relatedUser.modelName;
|
||||||
|
var isMultipleUsers = _isMultipleUsers(relatedUser);
|
||||||
|
// a relation can be considered for isOwner resolution if:
|
||||||
|
// 1. the app has a single user model and principalType is 'USER'
|
||||||
|
// 2. the app has multiple user models and principalType is the related user model name
|
||||||
|
// In addition, if an array of relations if provided with the ownerRelations option,
|
||||||
|
// then the given relation name is further checked against this array
|
||||||
|
if ((!isMultipleUsers && principalType === Principal.USER) ||
|
||||||
|
(isMultipleUsers && principalType === userModelName)) {
|
||||||
|
debug('Checking relation %s to %s: %j', r, userModelName, rel);
|
||||||
|
if (ownerRelations === true) {
|
||||||
|
relWithUsers.push(r);
|
||||||
|
} else if (Array.isArray(ownerRelations) && ownerRelations.indexOf(r) !== -1) {
|
||||||
|
relWithUsers.push(r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
if (relWithUsers.length === 0) {
|
||||||
return callback.promise;
|
debug('No matching belongsTo relation found for model %j and user: %j principalType: %j',
|
||||||
|
modelId, userId, principalType);
|
||||||
|
return callback(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check related users: someSeries is used to avoid spamming the db
|
||||||
|
async.someSeries(relWithUsers, processRelation, callback);
|
||||||
|
|
||||||
|
function processRelation(r, cb) {
|
||||||
|
inst[r](function processRelatedUser(err, user) {
|
||||||
|
if (err || !user) return cb(err, false);
|
||||||
|
|
||||||
|
debug('User found: %j (through %j)', user.id, r);
|
||||||
|
cb(null, matches(user.id, userId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper function to check if the app user config is multiple users or
|
||||||
|
// single user. It can be used with or without a reference user model.
|
||||||
|
// In case no user model is provided, we use the registry to get any of the
|
||||||
|
// user model by type. The relation with AccessToken is used to check
|
||||||
|
// if polymorphism is used, and thus if multiple users.
|
||||||
|
function _isMultipleUsers(userModel) {
|
||||||
|
var oneOfUserModels = userModel || _this.registry.getModelByType('User');
|
||||||
|
var accessTokensRel = oneOfUserModels.relations.accessTokens;
|
||||||
|
return !!(accessTokensRel && accessTokensRel.polymorphic);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
Role.registerResolver(Role.AUTHENTICATED, function(role, context, callback) {
|
||||||
|
|
|
@ -458,6 +458,16 @@ app._verifyAuthModelRelations = function() {
|
||||||
|
|
||||||
function verifyUserRelations(Model) {
|
function verifyUserRelations(Model) {
|
||||||
const hasManyTokens = Model.relations && Model.relations.accessTokens;
|
const hasManyTokens = Model.relations && Model.relations.accessTokens;
|
||||||
|
|
||||||
|
// display a temp warning message for users using multiple users config
|
||||||
|
if (hasManyTokens.polymorphic) {
|
||||||
|
console.warn(
|
||||||
|
'The app configuration follows the multiple user models setup ' +
|
||||||
|
'as described in http://ibm.biz/setup-loopback-auth',
|
||||||
|
'The built-in role resolver $owner is not currently compatible ' +
|
||||||
|
'with this configuration and should not be used in production.');
|
||||||
|
}
|
||||||
|
|
||||||
if (hasManyTokens) return;
|
if (hasManyTokens) return;
|
||||||
|
|
||||||
const relationsConfig = Model.settings.relations || {};
|
const relationsConfig = Model.settings.relations || {};
|
||||||
|
|
|
@ -8,6 +8,7 @@ var expect = require('./helpers/expect');
|
||||||
var request = require('supertest');
|
var request = require('supertest');
|
||||||
var loopback = require('../');
|
var loopback = require('../');
|
||||||
var ctx = require('../lib/access-context');
|
var ctx = require('../lib/access-context');
|
||||||
|
var extend = require('util')._extend;
|
||||||
var AccessContext = ctx.AccessContext;
|
var AccessContext = ctx.AccessContext;
|
||||||
var Principal = ctx.Principal;
|
var Principal = ctx.Principal;
|
||||||
var Promise = require('bluebird');
|
var Promise = require('bluebird');
|
||||||
|
@ -22,6 +23,7 @@ describe('Multiple users with custom principalType', function() {
|
||||||
beforeEach(function setupAppAndModels() {
|
beforeEach(function setupAppAndModels() {
|
||||||
// create a local app object that does not share state with other tests
|
// create a local app object that does not share state with other tests
|
||||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.set('_verifyAuthModelRelations', false);
|
||||||
app.dataSource('db', {connector: 'memory'});
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
var userModelOptions = {
|
var userModelOptions = {
|
||||||
|
@ -350,37 +352,38 @@ describe('Multiple users with custom principalType', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('built-in role resolver', function() {
|
describe('built-in role resolvers', function() {
|
||||||
it('supports AUTHENTICATED', function() {
|
it('supports $authenticated', function() {
|
||||||
return Role.isInRole(Role.AUTHENTICATED, userOneBaseContext)
|
return Role.isInRole(Role.AUTHENTICATED, userOneBaseContext)
|
||||||
.then(function(isInRole) {
|
.then(function(isInRole) {
|
||||||
expect(isInRole).to.be.true();
|
expect(isInRole).to.be.true();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports UNAUTHENTICATED', function() {
|
it('supports $unauthenticated', function() {
|
||||||
return Role.isInRole(Role.UNAUTHENTICATED, userOneBaseContext)
|
return Role.isInRole(Role.UNAUTHENTICATED, userOneBaseContext)
|
||||||
.then(function(isInRole) {
|
.then(function(isInRole) {
|
||||||
expect(isInRole).to.be.false();
|
expect(isInRole).to.be.false();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports OWNER', function() {
|
describe('$owner', function() {
|
||||||
var Album = app.registry.createModel('Album', {
|
it('supports legacy behavior with relations', function() {
|
||||||
name: String,
|
var Album = app.registry.createModel('Album', {
|
||||||
userId: Number,
|
name: String,
|
||||||
}, {
|
userId: Number,
|
||||||
relations: {
|
}, {
|
||||||
user: {
|
relations: {
|
||||||
type: 'belongsTo',
|
user: {
|
||||||
model: 'OneUser',
|
type: 'belongsTo',
|
||||||
foreignKey: 'userId',
|
model: 'OneUser',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
app.model(Album, {dataSource: 'db'});
|
||||||
app.model(Album, {dataSource: 'db'});
|
|
||||||
|
|
||||||
return Album.create({name: 'album', userId: userFromOneModel.id})
|
return Album.create({name: 'album', userId: userFromOneModel.id})
|
||||||
.then(function(album) {
|
.then(function(album) {
|
||||||
var validContext = {
|
var validContext = {
|
||||||
principalType: OneUser.modelName,
|
principalType: OneUser.modelName,
|
||||||
|
@ -393,37 +396,197 @@ describe('Multiple users with custom principalType', function() {
|
||||||
.then(function(isOwner) {
|
.then(function(isOwner) {
|
||||||
expect(isOwner).to.be.true();
|
expect(isOwner).to.be.true();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('expects OWNER to resolve false if owner has incorrect principalType', function() {
|
|
||||||
var Album = app.registry.createModel('Album', {
|
|
||||||
name: String,
|
|
||||||
userId: Number,
|
|
||||||
}, {
|
|
||||||
relations: {
|
|
||||||
user: {
|
|
||||||
type: 'belongsTo',
|
|
||||||
model: 'OneUser',
|
|
||||||
foreignKey: 'userId',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
app.model(Album, {dataSource: 'db'});
|
|
||||||
|
|
||||||
return Album.create({name: 'album', userId: userFromOneModel.id})
|
// With multiple users config, we cannot resolve a user based just on
|
||||||
|
// his id, as many users from different models could have the same id.
|
||||||
|
it('legacy behavior resolves false without belongsTo relation', function() {
|
||||||
|
var Album = app.registry.createModel('Album', {
|
||||||
|
name: String,
|
||||||
|
userId: Number,
|
||||||
|
owner: Number,
|
||||||
|
});
|
||||||
|
app.model(Album, {dataSource: 'db'});
|
||||||
|
|
||||||
|
return Album.create({
|
||||||
|
name: 'album',
|
||||||
|
userId: userFromOneModel.id,
|
||||||
|
owner: userFromOneModel.id,
|
||||||
|
})
|
||||||
.then(function(album) {
|
.then(function(album) {
|
||||||
var invalidContext = {
|
var authContext = {
|
||||||
principalType: AnotherUser.modelName,
|
principalType: OneUser.modelName,
|
||||||
principalId: userFromOneModel.id,
|
principalId: userFromOneModel.id,
|
||||||
model: Album,
|
model: Album,
|
||||||
id: album.id,
|
id: album.id,
|
||||||
};
|
};
|
||||||
return Role.isInRole(Role.OWNER, invalidContext);
|
return Role.isInRole(Role.OWNER, authContext);
|
||||||
})
|
})
|
||||||
.then(function(isOwner) {
|
.then(function(isOwner) {
|
||||||
expect(isOwner).to.be.false();
|
expect(isOwner).to.be.false();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('legacy behavior resolves false if owner has incorrect principalType', function() {
|
||||||
|
var Album = app.registry.createModel('Album', {
|
||||||
|
name: String,
|
||||||
|
userId: Number,
|
||||||
|
}, {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'OneUser',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
app.model(Album, {dataSource: 'db'});
|
||||||
|
|
||||||
|
return Album.create({name: 'album', userId: userFromOneModel.id})
|
||||||
|
.then(function(album) {
|
||||||
|
var invalidPrincipalTypes = [
|
||||||
|
'invalidContextName',
|
||||||
|
'USER',
|
||||||
|
AnotherUser.modelName,
|
||||||
|
];
|
||||||
|
var invalidContexts = invalidPrincipalTypes.map(principalType => {
|
||||||
|
return {
|
||||||
|
principalType,
|
||||||
|
principalId: userFromOneModel.id,
|
||||||
|
model: Album,
|
||||||
|
id: album.id,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Promise.map(invalidContexts, context => {
|
||||||
|
return Role.isInRole(Role.OWNER, context)
|
||||||
|
.then(isOwner => {
|
||||||
|
return {
|
||||||
|
principalType: context.principalType,
|
||||||
|
isOwner,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
expect(result).to.eql([
|
||||||
|
{principalType: 'invalidContextName', isOwner: false},
|
||||||
|
{principalType: 'USER', isOwner: false},
|
||||||
|
{principalType: AnotherUser.modelName, isOwner: false},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('resolves the owner using the corrent belongsTo relation',
|
||||||
|
function() {
|
||||||
|
// passing {ownerRelations: true} will enable the new $owner role resolver
|
||||||
|
// with any belongsTo relation allowing to resolve truthy
|
||||||
|
var Message = createModelWithOptions(
|
||||||
|
'ModelWithAllRelations',
|
||||||
|
{ownerRelations: true}
|
||||||
|
);
|
||||||
|
|
||||||
|
var messages = [
|
||||||
|
{content: 'firstMessage', customerId: userFromOneModel.id},
|
||||||
|
{
|
||||||
|
content: 'secondMessage',
|
||||||
|
customerId: userFromOneModel.id,
|
||||||
|
shopKeeperId: userFromAnotherModel.id,
|
||||||
|
},
|
||||||
|
|
||||||
|
// this is the incriminated message where two foreignKeys have the
|
||||||
|
// same id but points towards two different user models. Although
|
||||||
|
// customers should come from userFromOneModel and shopKeeperIds should
|
||||||
|
// come from userFromAnotherModel. The inverted situation still resolves
|
||||||
|
// isOwner true for both the customer and the shopKeeper
|
||||||
|
{
|
||||||
|
content: 'thirdMessage',
|
||||||
|
customerId: userFromAnotherModel.id,
|
||||||
|
shopKeeperId: userFromOneModel.id,
|
||||||
|
},
|
||||||
|
|
||||||
|
{content: 'fourthMessage', customerId: userFromAnotherModel.id},
|
||||||
|
{content: 'fifthMessage'},
|
||||||
|
];
|
||||||
|
return Promise.map(messages, msg => {
|
||||||
|
return Message.create(msg);
|
||||||
|
})
|
||||||
|
.then(messages => {
|
||||||
|
return Promise.all([
|
||||||
|
isOwnerForMessage(userFromOneModel, messages[0]),
|
||||||
|
isOwnerForMessage(userFromAnotherModel, messages[0]),
|
||||||
|
isOwnerForMessage(userFromOneModel, messages[1]),
|
||||||
|
isOwnerForMessage(userFromAnotherModel, messages[1]),
|
||||||
|
|
||||||
|
isOwnerForMessage(userFromOneModel, messages[2]),
|
||||||
|
isOwnerForMessage(userFromAnotherModel, messages[2]),
|
||||||
|
|
||||||
|
isOwnerForMessage(userFromAnotherModel, messages[3]),
|
||||||
|
isOwnerForMessage(userFromOneModel, messages[4]),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
expect(result).to.eql([
|
||||||
|
{userFrom: 'OneUser', msg: 'firstMessage', isOwner: true},
|
||||||
|
{userFrom: 'AnotherUser', msg: 'firstMessage', isOwner: false},
|
||||||
|
{userFrom: 'OneUser', msg: 'secondMessage', isOwner: true},
|
||||||
|
{userFrom: 'AnotherUser', msg: 'secondMessage', isOwner: true},
|
||||||
|
|
||||||
|
// these 2 tests fail because we cannot resolve ownership with
|
||||||
|
// multiple owners on a single model instance with a classic
|
||||||
|
// belongsTo relation, we need to use belongsTo with polymorphic
|
||||||
|
// discriminator to distinguish between the 2 models
|
||||||
|
{userFrom: 'OneUser', msg: 'thirdMessage', isOwner: false},
|
||||||
|
{userFrom: 'AnotherUser', msg: 'thirdMessage', isOwner: false},
|
||||||
|
|
||||||
|
{userFrom: 'AnotherUser', msg: 'fourthMessage', isOwner: false},
|
||||||
|
{userFrom: 'OneUser', msg: 'fifthMessage', isOwner: false},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
function isOwnerForMessage(user, msg) {
|
||||||
|
var accessContext = {
|
||||||
|
principalType: user.constructor.modelName,
|
||||||
|
principalId: user.id,
|
||||||
|
model: msg.constructor,
|
||||||
|
id: msg.id,
|
||||||
|
};
|
||||||
|
return Role.isInRole(Role.OWNER, accessContext)
|
||||||
|
.then(isOwner => {
|
||||||
|
return {
|
||||||
|
userFrom: user.constructor.modelName,
|
||||||
|
msg: msg.content,
|
||||||
|
isOwner,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createModelWithOptions(name, options) {
|
||||||
|
var baseOptions = {
|
||||||
|
relations: {
|
||||||
|
sender: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'OneUser',
|
||||||
|
foreignKey: 'customerId',
|
||||||
|
},
|
||||||
|
receiver: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'AnotherUser',
|
||||||
|
foreignKey: 'shopKeeperId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
options = extend(baseOptions, options);
|
||||||
|
var Model = app.registry.createModel(
|
||||||
|
name,
|
||||||
|
{content: String, senderType: String},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
app.model(Model, {dataSource: 'db'});
|
||||||
|
return Model;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isMappedToRole()', function() {
|
describe('isMappedToRole()', function() {
|
||||||
|
|
|
@ -8,6 +8,7 @@ var assert = require('assert');
|
||||||
var sinon = require('sinon');
|
var sinon = require('sinon');
|
||||||
var loopback = require('../index');
|
var loopback = require('../index');
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
|
var extend = require('util')._extend;
|
||||||
var expect = require('./helpers/expect');
|
var expect = require('./helpers/expect');
|
||||||
var Promise = require('bluebird');
|
var Promise = require('bluebird');
|
||||||
|
|
||||||
|
@ -363,7 +364,8 @@ describe('role model', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support owner role resolver', function(done) {
|
// this test should be split to address one resolver at a time
|
||||||
|
it('supports built-in role resolvers', function(done) {
|
||||||
Role.registerResolver('returnPromise', function(role, context) {
|
Role.registerResolver('returnPromise', function(role, context) {
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
process.nextTick(function() {
|
process.nextTick(function() {
|
||||||
|
@ -489,48 +491,249 @@ describe('role model', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolves OWNER via "userId" property with no relation', function() {
|
describe('$owner role resolver', function() {
|
||||||
var Album = app.registry.createModel('Album', {
|
var sender, receiver;
|
||||||
name: String,
|
var users = [
|
||||||
userId: Number,
|
{username: 'sender', email: 'sender@example.com', password: 'pass'},
|
||||||
});
|
{username: 'receiver', email: 'receiver@example.com', password: 'pass'},
|
||||||
app.model(Album, {dataSource: 'db'});
|
];
|
||||||
|
|
||||||
var user;
|
describe('ownerRelations not set (legacy behaviour)', () => {
|
||||||
return User.create({email: 'test@example.com', password: 'pass'})
|
it('resolves the owner via property "userId"', function() {
|
||||||
|
var user;
|
||||||
|
var Album = app.registry.createModel('Album', {
|
||||||
|
name: String,
|
||||||
|
userId: Number,
|
||||||
|
});
|
||||||
|
app.model(Album, {dataSource: 'db'});
|
||||||
|
|
||||||
|
return User.create({email: 'test@example.com', password: 'pass'})
|
||||||
|
.then(u => {
|
||||||
|
user = u;
|
||||||
|
return Album.create({name: 'Album 1', userId: user.id});
|
||||||
|
})
|
||||||
|
.then(album => {
|
||||||
|
return Role.isInRole(Role.OWNER, {
|
||||||
|
principalType: ACL.USER,
|
||||||
|
principalId: user.id,
|
||||||
|
model: Album,
|
||||||
|
id: album.id,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(isInRole => expect(isInRole).to.be.true());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the owner via property "owner"', function() {
|
||||||
|
var user;
|
||||||
|
var Album = app.registry.createModel('Album', {
|
||||||
|
name: String,
|
||||||
|
owner: Number,
|
||||||
|
});
|
||||||
|
app.model(Album, {dataSource: 'db'});
|
||||||
|
|
||||||
|
return User.create({email: 'test@example.com', password: 'pass'})
|
||||||
|
.then(u => {
|
||||||
|
user = u;
|
||||||
|
return Album.create({name: 'Album 1', owner: user.id});
|
||||||
|
})
|
||||||
|
.then(album => {
|
||||||
|
return Role.isInRole(Role.OWNER, {
|
||||||
|
principalType: ACL.USER,
|
||||||
|
principalId: user.id,
|
||||||
|
model: Album,
|
||||||
|
id: album.id,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(isInRole => expect(isInRole).to.be.true());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the owner via a belongsTo relation', function() {
|
||||||
|
// passing no options will result calling
|
||||||
|
// the legacy $owner role resolver behavior
|
||||||
|
var Message = givenModelWithSenderReceiverRelations('ModelWithNoOptions');
|
||||||
|
|
||||||
|
return givenUsers()
|
||||||
|
.then(() => {
|
||||||
|
var messages = [
|
||||||
|
{content: 'firstMessage', senderId: sender.id},
|
||||||
|
{content: 'secondMessage', receiverId: receiver.id},
|
||||||
|
{content: 'thirdMessage'},
|
||||||
|
];
|
||||||
|
return Promise.map(messages, msg => {
|
||||||
|
return Message.create(msg);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(messages => {
|
||||||
|
return Promise.all([
|
||||||
|
isOwnerForMessage(sender, messages[0]),
|
||||||
|
isOwnerForMessage(receiver, messages[1]),
|
||||||
|
isOwnerForMessage(receiver, messages[2]),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
expect(result).to.eql([
|
||||||
|
{user: 'sender', msg: 'firstMessage', isOwner: true},
|
||||||
|
{user: 'receiver', msg: 'secondMessage', isOwner: false},
|
||||||
|
{user: 'receiver', msg: 'thirdMessage', isOwner: false},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves as false without belongsTo relation', function() {
|
||||||
|
var user;
|
||||||
|
var Album = app.registry.createModel(
|
||||||
|
'Album',
|
||||||
|
{
|
||||||
|
name: String,
|
||||||
|
userId: Number,
|
||||||
|
owner: Number,
|
||||||
|
},
|
||||||
|
// passing {ownerRelations: true} will enable the new $owner role resolver
|
||||||
|
// and hence resolve false when no belongsTo relation is defined
|
||||||
|
{ownerRelations: true}
|
||||||
|
);
|
||||||
|
app.model(Album, {dataSource: 'db'});
|
||||||
|
|
||||||
|
return User.create({email: 'test@example.com', password: 'pass'})
|
||||||
.then(u => {
|
.then(u => {
|
||||||
user = u;
|
user = u;
|
||||||
return Album.create({name: 'Album 1', userId: user.id});
|
return Album.create({name: 'Album 1', userId: user.id, owner: user.id});
|
||||||
})
|
})
|
||||||
.then(album => {
|
.then(album => {
|
||||||
return Role.isInRole(Role.OWNER, {
|
return Role.isInRole(Role.OWNER, {
|
||||||
principalType: ACL.USER, principalId: user.id,
|
principalType: ACL.USER,
|
||||||
model: Album, id: album.id,
|
principalId: user.id,
|
||||||
|
model: Album,
|
||||||
|
id: album.id,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(isInRole => expect(isInRole).to.be.true());
|
.then(isInRole => expect(isInRole).to.be.false());
|
||||||
});
|
|
||||||
|
|
||||||
it('resolves OWNER via "owner" property with no relation', function() {
|
|
||||||
var Album = app.registry.createModel('Album', {
|
|
||||||
name: String,
|
|
||||||
owner: Number,
|
|
||||||
});
|
});
|
||||||
app.model(Album, {dataSource: 'db'});
|
|
||||||
|
|
||||||
var user;
|
it('resolves the owner using the corrent belongsTo relation', function() {
|
||||||
return User.create({email: 'test@example.com', password: 'pass'})
|
// passing {ownerRelations: true} will enable the new $owner role resolver
|
||||||
.then(u => {
|
// with any belongsTo relation allowing to resolve truthy
|
||||||
user = u;
|
var Message = givenModelWithSenderReceiverRelations(
|
||||||
return Album.create({name: 'Album 1', owner: user.id});
|
'ModelWithAllRelations',
|
||||||
})
|
{ownerRelations: true}
|
||||||
.then(album => {
|
);
|
||||||
return Role.isInRole(Role.OWNER, {
|
|
||||||
principalType: ACL.USER, principalId: user.id,
|
return givenUsers()
|
||||||
model: Album, id: album.id,
|
.then(() => {
|
||||||
|
var messages = [
|
||||||
|
{content: 'firstMessage', senderId: sender.id},
|
||||||
|
{content: 'secondMessage', receiverId: receiver.id},
|
||||||
|
{content: 'thirdMessage'},
|
||||||
|
];
|
||||||
|
return Promise.map(messages, msg => {
|
||||||
|
return Message.create(msg);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(isInRole => expect(isInRole).to.be.true());
|
.then(messages => {
|
||||||
|
return Promise.all([
|
||||||
|
isOwnerForMessage(sender, messages[0]),
|
||||||
|
isOwnerForMessage(receiver, messages[1]),
|
||||||
|
isOwnerForMessage(receiver, messages[2]),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
expect(result).to.eql([
|
||||||
|
{user: 'sender', msg: 'firstMessage', isOwner: true},
|
||||||
|
{user: 'receiver', msg: 'secondMessage', isOwner: true},
|
||||||
|
{user: 'receiver', msg: 'thirdMessage', isOwner: false},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows fine-grained control of which relations grant ownership',
|
||||||
|
function() {
|
||||||
|
// passing {ownerRelations: true} will enable the new $owner role resolver
|
||||||
|
// with a specified list of belongsTo relations allowing to resolve truthy
|
||||||
|
var Message = givenModelWithSenderReceiverRelations(
|
||||||
|
'ModelWithCoercedRelations',
|
||||||
|
{ownerRelations: ['receiver']}
|
||||||
|
);
|
||||||
|
|
||||||
|
return givenUsers()
|
||||||
|
.then(() => {
|
||||||
|
var messages = [
|
||||||
|
{content: 'firstMessage', senderId: sender.id},
|
||||||
|
{content: 'secondMessage', receiverId: receiver.id},
|
||||||
|
{content: 'thirdMessage'},
|
||||||
|
];
|
||||||
|
return Promise.map(messages, msg => {
|
||||||
|
return Message.create(msg);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(messages => {
|
||||||
|
return Promise.all([
|
||||||
|
isOwnerForMessage(sender, messages[0]),
|
||||||
|
isOwnerForMessage(receiver, messages[1]),
|
||||||
|
isOwnerForMessage(receiver, messages[2]),
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
expect(result).to.eql([
|
||||||
|
{user: 'sender', msg: 'firstMessage', isOwner: false},
|
||||||
|
{user: 'receiver', msg: 'secondMessage', isOwner: true},
|
||||||
|
{user: 'receiver', msg: 'thirdMessage', isOwner: false},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
function givenUsers() {
|
||||||
|
return Promise.map(users, user => {
|
||||||
|
return User.create(user);
|
||||||
|
})
|
||||||
|
.then(users => {
|
||||||
|
sender = users[0];
|
||||||
|
receiver = users[1];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOwnerForMessage(user, msg) {
|
||||||
|
var accessContext = {
|
||||||
|
principalType: ACL.USER,
|
||||||
|
principalId: user.id,
|
||||||
|
model: msg.constructor,
|
||||||
|
id: msg.id,
|
||||||
|
};
|
||||||
|
return Role.isInRole(Role.OWNER, accessContext)
|
||||||
|
.then(isOwner => {
|
||||||
|
return {
|
||||||
|
user: user.username,
|
||||||
|
msg: msg.content,
|
||||||
|
isOwner,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function givenModelWithSenderReceiverRelations(name, options) {
|
||||||
|
var baseOptions = {
|
||||||
|
relations: {
|
||||||
|
sender: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User',
|
||||||
|
foreignKey: 'senderId',
|
||||||
|
},
|
||||||
|
receiver: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User',
|
||||||
|
foreignKey: 'receiverId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
options = extend(baseOptions, options);
|
||||||
|
var Model = app.registry.createModel(
|
||||||
|
name,
|
||||||
|
{content: String},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
app.model(Model, {dataSource: 'db'});
|
||||||
|
return Model;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes accessToken to modelClass.findById when resolving OWNER', () => {
|
it('passes accessToken to modelClass.findById when resolving OWNER', () => {
|
||||||
|
|
Loading…
Reference in New Issue