483 lines
16 KiB
JavaScript
483 lines
16 KiB
JavaScript
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
|
|
// 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 request = require('supertest');
|
|
var loopback = require('../');
|
|
var ctx = require('../lib/access-context');
|
|
var AccessContext = ctx.AccessContext;
|
|
var Principal = ctx.Principal;
|
|
var Promise = require('bluebird');
|
|
|
|
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});
|
|
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}));
|
|
|
|
// 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'}),
|
|
])
|
|
.spread(function(u1, u2, r) {
|
|
userFromOneModel = u1;
|
|
userFromAnotherModel = u2;
|
|
userRole = r;
|
|
userOneBaseContext = {
|
|
principalType: OneUser.modelName,
|
|
principalId: userFromOneModel.id,
|
|
};
|
|
});
|
|
});
|
|
|
|
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',
|
|
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'},
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
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',
|
|
function() {
|
|
return Promise.all([
|
|
OneUser.resetPassword({email: options.email}),
|
|
waitForResetRequestAndVerify,
|
|
]);
|
|
});
|
|
|
|
function waitForResetRequestAndVerify() {
|
|
return waitForEvent(OneUser, 'resetPasswordRequest')
|
|
.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);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
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});
|
|
});
|
|
|
|
describe('getUser()', function() {
|
|
it('returns user although principals contain non USER principals',
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
|
|
it('returns user although principals contain invalid principals',
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
|
|
it('supports any level of built-in User model inheritance',
|
|
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,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// helper
|
|
function addToAccessContext(list) {
|
|
list.forEach(function(principal) {
|
|
expect(principal).to.exist();
|
|
accessContext.addPrincipal(principal.type, principal.id);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('role model', function() {
|
|
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(
|
|
{principalType: OneUser.modelName, principalId: userFromOneModel.id})
|
|
.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(
|
|
{principalType: 'invalidModelName', principalId: userFromOneModel.id})
|
|
.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(
|
|
{principalType: OneUser.modelName, principalId: userFromOneModel.id})
|
|
.then(function(principal) {
|
|
return principal.user();
|
|
})
|
|
.then(function(user) {
|
|
expect(user).to.have.property('id', userFromOneModel.id);
|
|
});
|
|
});
|
|
|
|
it('returns null when created with invalid principalType', function() {
|
|
return userRole.principals.create(
|
|
{principalType: 'invalidModelName', principalId: userFromOneModel.id})
|
|
.then(function(principal) {
|
|
return principal.user();
|
|
})
|
|
.then(function(user) {
|
|
expect(user).to.not.exist();
|
|
});
|
|
});
|
|
});
|
|
|
|
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(
|
|
userOneBaseContext)
|
|
.then(function(roles) {
|
|
expect(roles).to.eql([
|
|
Role.AUTHENTICATED,
|
|
Role.EVERYONE,
|
|
userRole.id,
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('built-in role resolver', function() {
|
|
it('supports AUTHENTICATED', function() {
|
|
return Role.isInRole(Role.AUTHENTICATED, userOneBaseContext)
|
|
.then(function(isInRole) {
|
|
expect(isInRole).to.be.true();
|
|
});
|
|
});
|
|
|
|
it('supports UNAUTHENTICATED', function() {
|
|
return Role.isInRole(Role.UNAUTHENTICATED, userOneBaseContext)
|
|
.then(function(isInRole) {
|
|
expect(isInRole).to.be.false();
|
|
});
|
|
});
|
|
|
|
it('supports OWNER', 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 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();
|
|
});
|
|
});
|
|
|
|
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})
|
|
.then(function(album) {
|
|
var invalidContext = {
|
|
principalType: AnotherUser.modelName,
|
|
principalId: userFromOneModel.id,
|
|
model: Album,
|
|
id: album.id,
|
|
};
|
|
return Role.isInRole(Role.OWNER, invalidContext);
|
|
})
|
|
.then(function(isOwner) {
|
|
expect(isOwner).to.be.false();
|
|
});
|
|
});
|
|
});
|
|
|
|
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',
|
|
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');
|
|
}
|
|
);
|
|
});
|
|
|
|
it('reports isMappedToRole by user.username using custom user principalType',
|
|
function() {
|
|
return ACL.isMappedToRole(OneUser.modelName, userFromOneModel.username, 'userRole')
|
|
.then(function(isMappedToRole) {
|
|
expect(isMappedToRole).to.be.true();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
};
|
|
|
|
function getIds(array) {
|
|
return array.map(function(it) { return it.id; });
|
|
};
|
|
});
|