2018-01-03 04:05:53 +00:00
|
|
|
// Copyright IBM Corp. 2016,2018. All Rights Reserved.
|
2016-11-21 20:51:43 +00:00
|
|
|
// Node module: loopback
|
|
|
|
// This file is licensed under the MIT License.
|
|
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
var expect = require('./helpers/expect');
|
|
|
|
var loopback = require('../');
|
|
|
|
var ctx = require('../lib/access-context');
|
2017-01-26 12:07:20 +00:00
|
|
|
var extend = require('util')._extend;
|
2016-11-21 20:51:43 +00:00
|
|
|
var AccessContext = ctx.AccessContext;
|
|
|
|
var Principal = ctx.Principal;
|
|
|
|
var Promise = require('bluebird');
|
2017-10-19 11:29:08 +00:00
|
|
|
const waitForEvent = require('./helpers/wait-for-event');
|
|
|
|
const supertest = require('supertest');
|
|
|
|
const loggers = require('./helpers/error-loggers');
|
|
|
|
const logServerErrorsOtherThan = loggers.logServerErrorsOtherThan;
|
2016-11-21 20:51:43 +00:00
|
|
|
|
|
|
|
describe('Multiple users with custom principalType', function() {
|
|
|
|
this.timeout(10000);
|
|
|
|
|
|
|
|
var commonCredentials = {email: 'foo@bar.com', password: 'bar'};
|
|
|
|
var app, OneUser, AnotherUser, AccessToken, Role,
|
|
|
|
userFromOneModel, userFromAnotherModel, userRole, userOneBaseContext;
|
|
|
|
|
|
|
|
beforeEach(function setupAppAndModels() {
|
|
|
|
// create a local app object that does not share state with other tests
|
|
|
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
2017-01-26 12:07:20 +00:00
|
|
|
app.set('_verifyAuthModelRelations', false);
|
2018-05-18 13:10:19 +00:00
|
|
|
app.set('remoting', {rest: {handleErrors: false}});
|
2016-11-21 20:51:43 +00:00
|
|
|
app.dataSource('db', {connector: 'memory'});
|
|
|
|
|
|
|
|
var userModelOptions = {
|
|
|
|
base: 'User',
|
|
|
|
// forceId is set to false for the purpose of updating the same affected user within the
|
|
|
|
// `Email Update` test cases.
|
|
|
|
forceId: false,
|
|
|
|
// Speed up the password hashing algorithm for tests
|
|
|
|
saltWorkFactor: 4,
|
|
|
|
};
|
|
|
|
|
|
|
|
// create and attach 2 User-based models
|
|
|
|
OneUser = createUserModel(app, 'OneUser', userModelOptions);
|
|
|
|
AnotherUser = createUserModel(app, 'AnotherUser', userModelOptions);
|
|
|
|
|
|
|
|
AccessToken = app.registry.getModel('AccessToken');
|
|
|
|
app.model(AccessToken, {dataSource: 'db'});
|
|
|
|
|
|
|
|
Role = app.registry.getModel('Role');
|
|
|
|
app.model(Role, {dataSource: 'db'});
|
|
|
|
|
|
|
|
// Update AccessToken and Users to bind them through polymorphic relations
|
|
|
|
AccessToken.belongsTo('user', {idName: 'id', polymorphic: {idType: 'string',
|
|
|
|
foreignKey: 'userId', discriminator: 'principalType'}});
|
|
|
|
OneUser.hasMany('accessTokens', {polymorphic: {foreignKey: 'userId',
|
|
|
|
discriminator: 'principalType'}});
|
|
|
|
AnotherUser.hasMany('accessTokens', {polymorphic: {foreignKey: 'userId',
|
|
|
|
discriminator: 'principalType'}});
|
|
|
|
|
|
|
|
app.enableAuth({dataSource: 'db'});
|
|
|
|
app.use(loopback.token({model: AccessToken}));
|
2017-10-19 11:29:08 +00:00
|
|
|
app.use(loopback.rest());
|
2016-11-21 20:51:43 +00:00
|
|
|
|
|
|
|
// create one user per user model to use them throughout the tests
|
|
|
|
return Promise.all([
|
|
|
|
OneUser.create(commonCredentials),
|
|
|
|
AnotherUser.create(commonCredentials),
|
|
|
|
Role.create({name: 'userRole'}),
|
|
|
|
])
|
2017-12-12 08:33:15 +00:00
|
|
|
.spread(function(u1, u2, r) {
|
|
|
|
userFromOneModel = u1;
|
|
|
|
userFromAnotherModel = u2;
|
|
|
|
userRole = r;
|
|
|
|
userOneBaseContext = {
|
|
|
|
principalType: OneUser.modelName,
|
|
|
|
principalId: userFromOneModel.id,
|
|
|
|
};
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('User.login', function() {
|
|
|
|
it('works for one user model and valid credentials', function() {
|
|
|
|
return OneUser.login(commonCredentials)
|
|
|
|
.then(function(accessToken) {
|
|
|
|
assertGoodToken(accessToken, userFromOneModel);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('works for a second user model and valid credentials', function() {
|
|
|
|
return AnotherUser.login(commonCredentials)
|
|
|
|
.then(function(accessToken) {
|
|
|
|
assertGoodToken(accessToken, userFromAnotherModel);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('fails when credentials are not correct', function() {
|
|
|
|
return OneUser.login({email: 'foo@bar.com', password: 'invalid'})
|
|
|
|
.then(
|
|
|
|
function onSuccess() {
|
|
|
|
throw new Error('OneUser.login() should have failed');
|
|
|
|
},
|
|
|
|
function onError(err) {
|
|
|
|
expect(err).to.have.property('code', 'LOGIN_FAILED');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function assertGoodToken(accessToken, user) {
|
|
|
|
if (accessToken instanceof AccessToken) {
|
|
|
|
accessToken = accessToken.toJSON();
|
|
|
|
}
|
|
|
|
expect(accessToken.id, 'token id').to.have.lengthOf(64);
|
|
|
|
expect(accessToken).to.have.property('userId', user.id);
|
|
|
|
expect(accessToken).to.have.property('principalType', user.constructor.definition.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('User.logout', function() {
|
|
|
|
it('logs out a user from user model 1 without logging out user from model 2',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
var tokenOfOneUser;
|
|
|
|
return Promise.all([
|
|
|
|
OneUser.login(commonCredentials),
|
|
|
|
AnotherUser.login(commonCredentials),
|
|
|
|
])
|
|
|
|
.spread(function(t1, t2) {
|
|
|
|
tokenOfOneUser = t1;
|
|
|
|
return OneUser.logout(tokenOfOneUser.id);
|
|
|
|
})
|
|
|
|
.then(function() {
|
|
|
|
return AccessToken.find({});
|
|
|
|
})
|
|
|
|
.then(function(allTokens) {
|
|
|
|
var data = allTokens.map(function(token) {
|
|
|
|
return {userId: token.userId, principalType: token.principalType};
|
|
|
|
});
|
|
|
|
expect(data).to.eql([
|
|
|
|
// no token for userFromAnotherModel
|
|
|
|
{userId: userFromAnotherModel.id, principalType: 'AnotherUser'},
|
|
|
|
]);
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('Password Reset', function() {
|
|
|
|
describe('User.resetPassword(options)', function() {
|
|
|
|
var options = {
|
|
|
|
email: 'foo@bar.com',
|
|
|
|
redirect: 'http://foobar.com/reset-password',
|
|
|
|
};
|
|
|
|
|
|
|
|
it('creates a temp accessToken to allow a user to change password',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
return Promise.all([
|
|
|
|
OneUser.resetPassword({email: options.email}),
|
|
|
|
waitForResetRequestAndVerify,
|
|
|
|
]);
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
|
|
|
|
function waitForResetRequestAndVerify() {
|
|
|
|
return waitForEvent(OneUser, 'resetPasswordRequest')
|
2017-12-12 08:33:15 +00:00
|
|
|
.then(function(info) {
|
|
|
|
assertGoodToken(info.accessToken, userFromOneModel);
|
|
|
|
return info.accessToken.user.getAsync();
|
|
|
|
})
|
|
|
|
.then(function(user) {
|
|
|
|
expect(user).to.have.property('id', userFromOneModel.id);
|
|
|
|
expect(user).to.have.property('email', userFromOneModel.email);
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('AccessToken (session) invalidation when changing email', function() {
|
|
|
|
var anotherUserFromOneModel;
|
|
|
|
|
|
|
|
it('impact only the related user', function() {
|
|
|
|
return OneUser.create({email: 'original@example.com', password: 'bar'})
|
|
|
|
.then(function(u) {
|
|
|
|
anotherUserFromOneModel = u;
|
|
|
|
return Promise.all([
|
|
|
|
OneUser.login({email: 'original@example.com', password: 'bar'}),
|
|
|
|
OneUser.login(commonCredentials),
|
|
|
|
AnotherUser.login(commonCredentials),
|
|
|
|
]);
|
|
|
|
})
|
|
|
|
.then(function() {
|
|
|
|
return anotherUserFromOneModel.updateAttribute('email', 'updated@example.com');
|
|
|
|
})
|
|
|
|
.then(function() {
|
|
|
|
// we need to sort on principalType to ensure stability in results' order
|
|
|
|
return AccessToken.find({'order': 'principalType ASC'});
|
|
|
|
})
|
|
|
|
.then(function(allTokens) {
|
|
|
|
var data = allTokens.map(function(token) {
|
|
|
|
return {userId: token.userId, principalType: token.principalType};
|
|
|
|
});
|
|
|
|
expect(data).to.eql([
|
|
|
|
// no token for anotherUserFromOneModel
|
|
|
|
{userId: userFromAnotherModel.id, principalType: 'AnotherUser'},
|
|
|
|
{userId: userFromOneModel.id, principalType: 'OneUser'},
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('AccessContext', function() {
|
|
|
|
var ThirdUser, userFromThirdModel, accessContext;
|
|
|
|
|
|
|
|
beforeEach(function() {
|
|
|
|
accessContext = new AccessContext({registry: OneUser.registry});
|
|
|
|
});
|
|
|
|
|
2017-02-03 20:53:27 +00:00
|
|
|
describe('getUser()', function() {
|
|
|
|
it('returns user although principals contain non USER principals',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
return Promise.try(function() {
|
|
|
|
addToAccessContext([
|
|
|
|
{type: Principal.ROLE},
|
|
|
|
{type: Principal.APP},
|
|
|
|
{type: Principal.SCOPE},
|
|
|
|
{type: OneUser.modelName, id: userFromOneModel.id},
|
|
|
|
]);
|
|
|
|
var user = accessContext.getUser();
|
|
|
|
expect(user).to.eql({
|
|
|
|
id: userFromOneModel.id,
|
|
|
|
principalType: OneUser.modelName,
|
|
|
|
});
|
2017-02-03 20:53:27 +00:00
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
|
2017-02-03 20:53:27 +00:00
|
|
|
it('returns user although principals contain invalid principals',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
return Promise.try(function() {
|
|
|
|
addToAccessContext([
|
|
|
|
{type: 'AccessToken'},
|
|
|
|
{type: 'invalidModelName'},
|
|
|
|
{type: OneUser.modelName, id: userFromOneModel.id},
|
|
|
|
]);
|
|
|
|
var user = accessContext.getUser();
|
|
|
|
expect(user).to.eql({
|
|
|
|
id: userFromOneModel.id,
|
|
|
|
principalType: OneUser.modelName,
|
|
|
|
});
|
2017-02-03 20:53:27 +00:00
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('supports any level of built-in User model inheritance',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
ThirdUser = createUserModel(app, 'ThirdUser', {base: 'OneUser'});
|
|
|
|
return ThirdUser.create(commonCredentials)
|
|
|
|
.then(function(userFromThirdModel) {
|
|
|
|
accessContext.addPrincipal(ThirdUser.modelName, userFromThirdModel.id);
|
|
|
|
var user = accessContext.getUser();
|
|
|
|
expect(user).to.eql({
|
|
|
|
id: userFromThirdModel.id,
|
|
|
|
principalType: ThirdUser.modelName,
|
|
|
|
});
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// helper
|
|
|
|
function addToAccessContext(list) {
|
|
|
|
list.forEach(function(principal) {
|
|
|
|
expect(principal).to.exist();
|
|
|
|
accessContext.addPrincipal(principal.type, principal.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-03-20 13:15:44 +00:00
|
|
|
describe('Role model', function() {
|
2016-11-21 20:51:43 +00:00
|
|
|
this.timeout(10000);
|
|
|
|
|
|
|
|
var RoleMapping, ACL, user;
|
|
|
|
|
|
|
|
beforeEach(function() {
|
|
|
|
ACL = app.registry.getModel('ACL');
|
|
|
|
app.model(ACL, {dataSource: 'db'});
|
|
|
|
|
|
|
|
RoleMapping = app.registry.getModel('RoleMapping');
|
|
|
|
app.model(RoleMapping, {dataSource: 'db'});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('role.users()', function() {
|
|
|
|
it('returns users when using custom user principalType', function() {
|
|
|
|
return userRole.principals.create(
|
2018-08-08 15:22:20 +00:00
|
|
|
{principalType: OneUser.modelName, principalId: userFromOneModel.id}
|
|
|
|
)
|
2016-11-21 20:51:43 +00:00
|
|
|
.then(function() {
|
|
|
|
return userRole.users({where: {principalType: OneUser.modelName}});
|
|
|
|
})
|
|
|
|
.then(getIds)
|
|
|
|
.then(function(userIds) {
|
|
|
|
expect(userIds).to.eql([userFromOneModel.id]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('returns empty array when using invalid principalType', function() {
|
|
|
|
return userRole.principals.create(
|
2018-08-08 15:22:20 +00:00
|
|
|
{principalType: 'invalidModelName', principalId: userFromOneModel.id}
|
|
|
|
)
|
2016-11-21 20:51:43 +00:00
|
|
|
.then(function() {
|
|
|
|
return userRole.users({where: {principalType: 'invalidModelName'}});
|
|
|
|
})
|
|
|
|
.then(function(users) {
|
|
|
|
expect(users).to.be.empty();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('principal.user()', function() {
|
|
|
|
it('returns the correct user instance', function() {
|
|
|
|
return userRole.principals.create(
|
2018-08-08 15:22:20 +00:00
|
|
|
{principalType: OneUser.modelName, principalId: userFromOneModel.id}
|
|
|
|
).then(function(principal) {
|
|
|
|
return principal.user();
|
|
|
|
}).then(function(user) {
|
|
|
|
expect(user).to.have.property('id', userFromOneModel.id);
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('returns null when created with invalid principalType', function() {
|
|
|
|
return userRole.principals.create(
|
2018-08-08 15:22:20 +00:00
|
|
|
{principalType: 'invalidModelName', principalId: userFromOneModel.id}
|
|
|
|
).then(function(principal) {
|
|
|
|
return principal.user();
|
|
|
|
}).then(function(user) {
|
|
|
|
expect(user).to.not.exist();
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('isInRole() & getRole()', function() {
|
|
|
|
beforeEach(function() {
|
|
|
|
return userRole.principals.create({principalType: OneUser.modelName,
|
|
|
|
principalId: userFromOneModel.id});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('supports isInRole()', function() {
|
|
|
|
return Role.isInRole('userRole', userOneBaseContext)
|
|
|
|
.then(function(isInRole) {
|
|
|
|
expect(isInRole).to.be.true();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('supports getRoles()', function() {
|
|
|
|
return Role.getRoles(
|
2018-08-08 15:22:20 +00:00
|
|
|
userOneBaseContext
|
|
|
|
).then(function(roles) {
|
|
|
|
expect(roles).to.eql([
|
|
|
|
Role.AUTHENTICATED,
|
|
|
|
Role.EVERYONE,
|
|
|
|
userRole.id,
|
|
|
|
]);
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
describe('built-in role resolvers', function() {
|
|
|
|
it('supports $authenticated', function() {
|
2016-11-21 20:51:43 +00:00
|
|
|
return Role.isInRole(Role.AUTHENTICATED, userOneBaseContext)
|
|
|
|
.then(function(isInRole) {
|
|
|
|
expect(isInRole).to.be.true();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
it('supports $unauthenticated', function() {
|
2016-11-21 20:51:43 +00:00
|
|
|
return Role.isInRole(Role.UNAUTHENTICATED, userOneBaseContext)
|
|
|
|
.then(function(isInRole) {
|
|
|
|
expect(isInRole).to.be.false();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
describe('$owner', function() {
|
|
|
|
it('supports legacy behavior with relations', function() {
|
|
|
|
var Album = app.registry.createModel('Album', {
|
|
|
|
name: String,
|
|
|
|
userId: Number,
|
|
|
|
}, {
|
|
|
|
relations: {
|
|
|
|
user: {
|
|
|
|
type: 'belongsTo',
|
|
|
|
model: 'OneUser',
|
|
|
|
foreignKey: 'userId',
|
|
|
|
},
|
2016-11-21 20:51:43 +00:00
|
|
|
},
|
2017-01-26 12:07:20 +00:00
|
|
|
});
|
|
|
|
app.model(Album, {dataSource: 'db'});
|
2016-11-21 20:51:43 +00:00
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
return Album.create({name: 'album', userId: userFromOneModel.id})
|
2017-12-12 08:33:15 +00:00
|
|
|
.then(function(album) {
|
|
|
|
var validContext = {
|
|
|
|
principalType: OneUser.modelName,
|
|
|
|
principalId: userFromOneModel.id,
|
|
|
|
model: Album,
|
|
|
|
id: album.id,
|
|
|
|
};
|
|
|
|
return Role.isInRole(Role.OWNER, validContext);
|
|
|
|
})
|
|
|
|
.then(function(isOwner) {
|
|
|
|
expect(isOwner).to.be.true();
|
|
|
|
});
|
2017-02-03 20:53:27 +00:00
|
|
|
});
|
|
|
|
|
2017-01-26 12:07:20 +00:00
|
|
|
// 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,
|
|
|
|
})
|
2017-12-12 08:33:15 +00:00
|
|
|
.then(function(album) {
|
|
|
|
var authContext = {
|
|
|
|
principalType: OneUser.modelName,
|
|
|
|
principalId: userFromOneModel.id,
|
|
|
|
model: Album,
|
|
|
|
id: album.id,
|
|
|
|
};
|
|
|
|
return Role.isInRole(Role.OWNER, authContext);
|
|
|
|
})
|
|
|
|
.then(function(isOwner) {
|
|
|
|
expect(isOwner).to.be.false();
|
|
|
|
});
|
2017-01-26 12:07:20 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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})
|
2017-12-12 08:33:15 +00:00
|
|
|
.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},
|
|
|
|
]);
|
2017-01-26 12:07:20 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it.skip('resolves the owner using the corrent belongsTo relation',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
2017-01-26 12:07:20 +00:00
|
|
|
// passing {ownerRelations: true} will enable the new $owner role resolver
|
|
|
|
// with any belongsTo relation allowing to resolve truthy
|
2017-12-12 08:33:15 +00:00
|
|
|
var Message = createModelWithOptions(
|
|
|
|
'ModelWithAllRelations',
|
|
|
|
{ownerRelations: true}
|
|
|
|
);
|
|
|
|
|
|
|
|
var messages = [
|
|
|
|
{content: 'firstMessage', customerId: userFromOneModel.id},
|
|
|
|
{
|
|
|
|
content: 'secondMessage',
|
|
|
|
customerId: userFromOneModel.id,
|
|
|
|
shopKeeperId: userFromAnotherModel.id,
|
|
|
|
},
|
2017-01-26 12:07:20 +00:00
|
|
|
|
2017-12-12 08:33:15 +00:00
|
|
|
// 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,
|
|
|
|
},
|
2017-01-26 12:07:20 +00:00
|
|
|
|
2017-12-12 08:33:15 +00:00
|
|
|
{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},
|
|
|
|
]);
|
|
|
|
});
|
2017-01-26 12:07:20 +00:00
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
2017-01-26 12:07:20 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('isMappedToRole()', function() {
|
|
|
|
beforeEach(function() {
|
|
|
|
return userRole.principals.create(userOneBaseContext);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('resolves user by id using custom user principalType', function() {
|
|
|
|
return ACL.resolvePrincipal(OneUser.modelName, userFromOneModel.id)
|
|
|
|
.then(function(principal) {
|
|
|
|
expect(principal.id).to.eql(userFromOneModel.id);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('throws error with code \'INVALID_PRINCIPAL_TYPE\' when principalType is incorrect',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
return ACL.resolvePrincipal('incorrectPrincipalType', userFromOneModel.id)
|
|
|
|
.then(
|
|
|
|
function onSuccess() {
|
|
|
|
throw new Error('ACL.resolvePrincipal() should have failed');
|
|
|
|
},
|
|
|
|
function onError(err) {
|
|
|
|
expect(err).to.have.property('statusCode', 400);
|
|
|
|
expect(err).to.have.property('code', 'INVALID_PRINCIPAL_TYPE');
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
|
|
|
|
it('reports isMappedToRole by user.username using custom user principalType',
|
2017-12-12 08:33:15 +00:00
|
|
|
function() {
|
|
|
|
return ACL.isMappedToRole(OneUser.modelName, userFromOneModel.username, 'userRole')
|
|
|
|
.then(function(isMappedToRole) {
|
|
|
|
expect(isMappedToRole).to.be.true();
|
|
|
|
});
|
|
|
|
});
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-10-19 11:29:08 +00:00
|
|
|
describe('setPassword', () => {
|
|
|
|
let resetToken;
|
|
|
|
beforeEach(givenResetPasswordTokenForOneUser);
|
|
|
|
|
|
|
|
it('sets password when the access token belongs to the user', () => {
|
|
|
|
return supertest(app)
|
|
|
|
.post('/OneUsers/reset-password')
|
|
|
|
.set('Authorization', resetToken.id)
|
|
|
|
.send({newPassword: 'new-pass'})
|
|
|
|
.expect(204)
|
|
|
|
.then(() => {
|
|
|
|
return supertest(app)
|
|
|
|
.post('/OneUsers/login')
|
|
|
|
.send({email: commonCredentials.email, password: 'new-pass'})
|
|
|
|
.expect(200);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('fails when the access token belongs to a different user mode', () => {
|
|
|
|
logServerErrorsOtherThan(403, app);
|
|
|
|
return supertest(app)
|
|
|
|
.post('/AnotherUsers/reset-password')
|
|
|
|
.set('Authorization', resetToken.id)
|
|
|
|
.send({newPassword: 'new-pass'})
|
|
|
|
.expect(403)
|
|
|
|
.then(() => {
|
|
|
|
return supertest(app)
|
|
|
|
.post('/AnotherUsers/login')
|
|
|
|
.send(commonCredentials)
|
|
|
|
.expect(200);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function givenResetPasswordTokenForOneUser() {
|
|
|
|
return Promise.all([
|
|
|
|
OneUser.resetPassword({email: commonCredentials.email}),
|
|
|
|
waitForEvent(OneUser, 'resetPasswordRequest'),
|
|
|
|
])
|
2017-12-12 08:33:15 +00:00
|
|
|
.spread((reset, info) => resetToken = info.accessToken);
|
2017-10-19 11:29:08 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-10-27 07:47:07 +00:00
|
|
|
describe('changePassword', () => {
|
|
|
|
let token;
|
|
|
|
beforeEach(givenTokenForOneUser);
|
|
|
|
|
|
|
|
it('changes password when the access token belongs to the user', () => {
|
|
|
|
return supertest(app)
|
|
|
|
.post('/OneUsers/change-password')
|
|
|
|
.set('Authorization', token.id)
|
|
|
|
.send({
|
|
|
|
oldPassword: commonCredentials.password,
|
|
|
|
newPassword: 'new-pass',
|
|
|
|
})
|
|
|
|
.expect(204)
|
|
|
|
.then(() => {
|
|
|
|
return supertest(app)
|
|
|
|
.post('/OneUsers/login')
|
|
|
|
.send({email: commonCredentials.email, password: 'new-pass'})
|
|
|
|
.expect(200);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('fails when the access token belongs to a different user mode', () => {
|
|
|
|
logServerErrorsOtherThan(403, app);
|
|
|
|
return supertest(app)
|
|
|
|
.post('/AnotherUsers/change-password')
|
|
|
|
.set('Authorization', token.id)
|
|
|
|
.send({
|
|
|
|
oldPassword: commonCredentials.password,
|
|
|
|
newPassword: 'new-pass',
|
|
|
|
})
|
|
|
|
.expect(403)
|
|
|
|
.then(() => {
|
|
|
|
return supertest(app)
|
|
|
|
.post('/AnotherUsers/login')
|
|
|
|
.send(commonCredentials)
|
|
|
|
.expect(200);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function givenTokenForOneUser() {
|
|
|
|
return OneUser.login(commonCredentials).then(t => token = t);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-03-20 13:15:44 +00:00
|
|
|
describe('authorization', () => {
|
|
|
|
beforeEach(givenProductModelAllowingOnlyUserRoleAccess);
|
|
|
|
|
|
|
|
it('allows users belonging to authorized role', () => {
|
|
|
|
logServerErrorsOtherThan(200, app);
|
|
|
|
return userFromOneModel.createAccessToken()
|
|
|
|
.then(token => {
|
|
|
|
return supertest(app)
|
|
|
|
.get('/Products')
|
|
|
|
.set('Authorization', token.id)
|
|
|
|
.expect(200, []);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('rejects other users', () => {
|
|
|
|
logServerErrorsOtherThan(401, app);
|
|
|
|
return userFromAnotherModel.createAccessToken()
|
|
|
|
.then(token => {
|
|
|
|
return supertest(app)
|
|
|
|
.get('/Products')
|
|
|
|
.set('Authorization', token.id)
|
|
|
|
.expect(401);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function givenProductModelAllowingOnlyUserRoleAccess() {
|
|
|
|
const Product = app.registry.createModel({
|
|
|
|
name: 'Product',
|
|
|
|
acls: [
|
|
|
|
{
|
|
|
|
'principalType': 'ROLE',
|
|
|
|
'principalId': '$everyone',
|
|
|
|
'permission': 'DENY',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
'principalType': 'ROLE',
|
|
|
|
'principalId': userRole.name,
|
|
|
|
'permission': 'ALLOW',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
app.model(Product, {dataSource: 'db'});
|
|
|
|
|
|
|
|
return userRole.principals.create({
|
|
|
|
principalType: OneUser.modelName,
|
|
|
|
principalId: userFromOneModel.id,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-11-21 20:51:43 +00:00
|
|
|
// helpers
|
|
|
|
function createUserModel(app, name, options) {
|
|
|
|
var model = app.registry.createModel(Object.assign({name: name}, options));
|
|
|
|
app.model(model, {dataSource: 'db'});
|
|
|
|
model.setMaxListeners(0); // allow many User.afterRemote's to be called
|
|
|
|
return model;
|
|
|
|
}
|
|
|
|
|
|
|
|
function waitForEvent(emitter, name) {
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
emitter.once(name, resolve);
|
|
|
|
});
|
2018-09-12 15:19:48 +00:00
|
|
|
}
|
2016-11-21 20:51:43 +00:00
|
|
|
|
|
|
|
function getIds(array) {
|
|
|
|
return array.map(function(it) { return it.id; });
|
2018-09-12 15:19:48 +00:00
|
|
|
}
|
2016-11-21 20:51:43 +00:00
|
|
|
});
|