Merge pull request #2971 from ebarault/enable-multiple-user-models
Enable multiple user models inheriting from base class User
This commit is contained in:
commit
304ecc4784
|
@ -146,6 +146,16 @@ module.exports = function(AccessToken) {
|
|||
var userRelation = AccessToken.relations.user; // may not be set up
|
||||
var User = userRelation && userRelation.modelTo;
|
||||
|
||||
// redefine user model if accessToken's principalType is available
|
||||
if (this.principalType) {
|
||||
User = AccessToken.registry.findModel(this.principalType);
|
||||
if (!User) {
|
||||
process.nextTick(function() {
|
||||
return cb(null, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
var created = this.created.getTime();
|
||||
var elapsedSeconds = (now - created) / 1000;
|
||||
|
@ -157,14 +167,18 @@ module.exports = function(AccessToken) {
|
|||
elapsedSeconds < secondsToLive;
|
||||
|
||||
if (isValid) {
|
||||
process.nextTick(function() {
|
||||
cb(null, isValid);
|
||||
});
|
||||
} else {
|
||||
this.destroy(function(err) {
|
||||
cb(err, isValid);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
process.nextTick(function() {
|
||||
cb(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -395,6 +395,7 @@ module.exports = function(ACL) {
|
|||
self.resolveRelatedModels();
|
||||
var roleModel = self.roleModel;
|
||||
|
||||
context.registry = this.registry;
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
@ -480,6 +481,7 @@ module.exports = function(ACL) {
|
|||
assert(token, 'Access token is required');
|
||||
if (!callback) callback = utils.createPromiseCallback();
|
||||
var context = new AccessContext({
|
||||
registry: this.registry,
|
||||
accessToken: token,
|
||||
model: model,
|
||||
property: method,
|
||||
|
@ -514,6 +516,7 @@ module.exports = function(ACL) {
|
|||
cb = cb || utils.createPromiseCallback();
|
||||
type = type || ACL.ROLE;
|
||||
this.resolveRelatedModels();
|
||||
|
||||
switch (type) {
|
||||
case ACL.ROLE:
|
||||
this.roleModel.findOne({where: {or: [{name: id}, {id: id}]}}, cb);
|
||||
|
@ -527,12 +530,21 @@ module.exports = function(ACL) {
|
|||
{where: {or: [{name: id}, {email: id}, {id: id}]}}, cb);
|
||||
break;
|
||||
default:
|
||||
// try resolving a user model that matches principalType
|
||||
var userModel = this.registry.findModel(type);
|
||||
if (userModel) {
|
||||
userModel.findOne(
|
||||
{where: {or: [{username: id}, {email: id}, {id: id}]}},
|
||||
cb);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
var err = new Error(g.f('Invalid principal type: %s', type));
|
||||
err.statusCode = 400;
|
||||
err.code = 'INVALID_PRINCIPAL_TYPE';
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
return cb.promise;
|
||||
};
|
||||
|
||||
|
|
|
@ -63,9 +63,17 @@ module.exports = function(RoleMapping) {
|
|||
RoleMapping.prototype.user = function(callback) {
|
||||
callback = callback || utils.createPromiseCallback();
|
||||
this.constructor.resolveRelatedModels();
|
||||
var userModel;
|
||||
|
||||
if (this.principalType === RoleMapping.USER) {
|
||||
var userModel = this.constructor.userModel;
|
||||
userModel = this.constructor.userModel;
|
||||
userModel.findById(this.principalId, callback);
|
||||
return callback.promise;
|
||||
}
|
||||
|
||||
// try resolving a user model that matches principalType
|
||||
userModel = this.constructor.registry.findModel(this.principalType);
|
||||
if (userModel) {
|
||||
userModel.findById(this.principalId, callback);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
"principalType": {
|
||||
"type": "string",
|
||||
"description": "The principal type, such as user, application, or role"
|
||||
"description": "The principal type, such as USER, APPLICATION, ROLE, or user model name in case of multiple user models"
|
||||
},
|
||||
"principalId": {
|
||||
"type": "string",
|
||||
|
|
|
@ -9,9 +9,9 @@ var debug = require('debug')('loopback:security:role');
|
|||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
var utils = require('../../lib/utils');
|
||||
|
||||
var AccessContext = require('../../lib/access-context').AccessContext;
|
||||
|
||||
var ctx = require('../../lib/access-context');
|
||||
var AccessContext = ctx.AccessContext;
|
||||
var Principal = ctx.Principal;
|
||||
var RoleMapping = loopback.RoleMapping;
|
||||
|
||||
assert(RoleMapping, 'RoleMapping model must be defined before Role model');
|
||||
|
@ -70,7 +70,8 @@ module.exports = function(Role) {
|
|||
callback = utils.createPromiseCallback();
|
||||
}
|
||||
}
|
||||
if (!query) query = {};
|
||||
query = query || {};
|
||||
query.where = query.where || {};
|
||||
|
||||
roleModel.resolveRelatedModels();
|
||||
var relsToModels = {
|
||||
|
@ -86,8 +87,29 @@ module.exports = function(Role) {
|
|||
roles: ACL.ROLE,
|
||||
};
|
||||
|
||||
var model = relsToModels[rel];
|
||||
listByPrincipalType(this, model, relsToTypes[rel], query, callback);
|
||||
var principalModel = relsToModels[rel];
|
||||
var principalType = relsToTypes[rel];
|
||||
|
||||
// redefine user model and user type if user principalType is custom (available and not "USER")
|
||||
var isCustomUserPrincipalType = rel === 'users' &&
|
||||
query.where.principalType &&
|
||||
query.where.principalType !== RoleMapping.USER;
|
||||
|
||||
if (isCustomUserPrincipalType) {
|
||||
var registry = this.constructor.registry;
|
||||
principalModel = registry.findModel(query.where.principalType);
|
||||
principalType = query.where.principalType;
|
||||
}
|
||||
// make sure we don't keep principalType in userModel query
|
||||
delete query.where.principalType;
|
||||
|
||||
if (principalModel) {
|
||||
listByPrincipalType(this, principalModel, principalType, query, callback);
|
||||
} else {
|
||||
process.nextTick(function() {
|
||||
callback(null, []);
|
||||
});
|
||||
}
|
||||
return callback.promise;
|
||||
};
|
||||
});
|
||||
|
@ -102,10 +124,11 @@ module.exports = function(Role) {
|
|||
* @param {Function} [callback] callback function called with `(err, models)` arguments.
|
||||
*/
|
||||
function listByPrincipalType(context, model, principalType, query, callback) {
|
||||
if (callback === undefined) {
|
||||
if (callback === undefined && typeof query === 'function') {
|
||||
callback = query;
|
||||
query = {};
|
||||
}
|
||||
query = query || {};
|
||||
|
||||
roleModel.roleMappingModel.find({
|
||||
where: {roleId: context.id, principalType: principalType},
|
||||
|
@ -303,6 +326,7 @@ module.exports = function(Role) {
|
|||
* @promise
|
||||
*/
|
||||
Role.isInRole = function(role, context, callback) {
|
||||
context.registry = this.registry;
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
@ -421,9 +445,9 @@ module.exports = function(Role) {
|
|||
callback = utils.createPromiseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
if (!options) options = {};
|
||||
|
||||
context.registry = this.registry;
|
||||
if (!(context instanceof AccessContext)) {
|
||||
context = new AccessContext(context);
|
||||
}
|
||||
|
|
|
@ -683,13 +683,18 @@ module.exports = function(User) {
|
|||
return process.nextTick(cb);
|
||||
|
||||
var AccessToken = accessTokenRelation.modelTo;
|
||||
|
||||
var query = {userId: {inq: userIds}};
|
||||
var tokenPK = AccessToken.definition.idName() || 'id';
|
||||
if (options.accessToken && tokenPK in options.accessToken) {
|
||||
query[tokenPK] = {neq: options.accessToken[tokenPK]};
|
||||
}
|
||||
|
||||
// add principalType in AccessToken.query if using polymorphic relations
|
||||
// between AccessToken and User
|
||||
var relatedUser = AccessToken.relations.user;
|
||||
var isRelationPolymorphic = relatedUser.polymorphic && !relatedUser.modelTo;
|
||||
if (isRelationPolymorphic) {
|
||||
query.principalType = this.modelName;
|
||||
}
|
||||
AccessToken.deleteAll(query, options, cb);
|
||||
};
|
||||
|
||||
|
|
|
@ -32,9 +32,12 @@ function AccessContext(context) {
|
|||
}
|
||||
context = context || {};
|
||||
|
||||
assert(context.registry,
|
||||
'Application registry is mandatory in AccessContext but missing in provided context');
|
||||
this.registry = context.registry;
|
||||
this.principals = context.principals || [];
|
||||
var model = context.model;
|
||||
model = ('string' === typeof model) ? loopback.getModel(model) : model;
|
||||
model = ('string' === typeof model) ? this.registry.getModel(model) : model;
|
||||
this.model = model;
|
||||
this.modelName = model && model.modelName;
|
||||
|
||||
|
@ -62,6 +65,7 @@ function AccessContext(context) {
|
|||
var principalType = context.principalType || Principal.USER;
|
||||
var principalId = context.principalId || undefined;
|
||||
var principalName = context.principalName || undefined;
|
||||
|
||||
if (principalId) {
|
||||
this.addPrincipal(principalType, principalId, principalName);
|
||||
}
|
||||
|
@ -124,11 +128,25 @@ AccessContext.prototype.addPrincipal = function(principalType, principalId, prin
|
|||
* @returns {*}
|
||||
*/
|
||||
AccessContext.prototype.getUserId = function() {
|
||||
var BaseUser = this.registry.getModel('User');
|
||||
for (var i = 0; i < this.principals.length; i++) {
|
||||
var p = this.principals[i];
|
||||
var isBuiltinPrincipal = p.type === Principal.APP ||
|
||||
p.type === Principal.ROLE ||
|
||||
p.type == Principal.SCOPE;
|
||||
if (isBuiltinPrincipal) continue;
|
||||
|
||||
// the principalType must either be 'USER'
|
||||
if (p.type === Principal.USER) {
|
||||
return p.id;
|
||||
}
|
||||
|
||||
// or permit to resolve a valid user model
|
||||
var userModel = this.registry.findModel(p.type);
|
||||
if (!userModel) continue;
|
||||
if (userModel.prototype instanceof BaseUser) {
|
||||
return p.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -189,8 +207,9 @@ AccessContext.prototype.debug = function() {
|
|||
* This class represents the abstract notion of a principal, which can be used
|
||||
* to represent any entity, such as an individual, a corporation, and a login id
|
||||
* @param {String} type The principal type
|
||||
* @param {*} id The princiapl id
|
||||
* @param {*} id The principal id
|
||||
* @param {String} [name] The principal name
|
||||
* @param {String} modelName The principal model name
|
||||
* @returns {Principal}
|
||||
* @class
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,444 @@
|
|||
// 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('getUserId()', function() {
|
||||
it('returns userId 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 userId = accessContext.getUserId();
|
||||
expect(userId).to.equal(userFromOneModel.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns userId although principals contain invalid principals',
|
||||
function() {
|
||||
return Promise.try(function() {
|
||||
addToAccessContext([
|
||||
{type: 'AccessToken'},
|
||||
{type: 'invalidModelName'},
|
||||
{type: OneUser.modelName, id: userFromOneModel.id},
|
||||
]);
|
||||
var userId = accessContext.getUserId();
|
||||
expect(userId).to.equal(userFromOneModel.id);
|
||||
});
|
||||
});
|
||||
|
||||
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 userId = accessContext.getUserId();
|
||||
expect(userId).to.equal(userFromThirdModel.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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) {
|
||||
return Role.isInRole(
|
||||
Role.OWNER,
|
||||
{
|
||||
principalType: OneUser.modelName,
|
||||
principalId: userFromOneModel.id,
|
||||
model: Album,
|
||||
id: album.id,
|
||||
});
|
||||
})
|
||||
.then(function(isInRole) {
|
||||
expect(isInRole).to.be.true();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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; });
|
||||
};
|
||||
});
|
|
@ -28,11 +28,12 @@ describe('User', function() {
|
|||
var validMixedCaseEmailCredentials = {email: 'Foo@bar.com', password: 'bar'};
|
||||
var invalidCredentials = {email: 'foo1@bar.com', password: 'invalid'};
|
||||
var incompleteCredentials = {password: 'bar1'};
|
||||
var validCredentialsUser, validCredentialsEmailVerifiedUser;
|
||||
|
||||
// Create a local app variable to prevent clashes with the global
|
||||
// variable shared by all tests. While this should not be necessary if
|
||||
// the tests were written correctly, it turns out that's not the case :(
|
||||
var app;
|
||||
var app = null;
|
||||
|
||||
beforeEach(function setupAppAndModels(done) {
|
||||
// override the global app object provided by test/support.js
|
||||
|
@ -89,8 +90,12 @@ describe('User', function() {
|
|||
|
||||
User.create(validCredentials, function(err, user) {
|
||||
if (err) return done(err);
|
||||
|
||||
User.create(validCredentialsEmailVerified, done);
|
||||
validCredentialsUser = user;
|
||||
User.create(validCredentialsEmailVerified, function(err, user) {
|
||||
if (err) return done(err);
|
||||
validCredentialsEmailVerifiedUser = user;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -279,14 +284,15 @@ describe('User', function() {
|
|||
|
||||
it('invalidates the user\'s accessToken when the user is deleted all', function(done) {
|
||||
var userIds = [];
|
||||
var accessTokenId;
|
||||
var users;
|
||||
async.series([
|
||||
function(next) {
|
||||
User.create([
|
||||
{name: 'myname', email: 'b@c.com', password: 'bar'},
|
||||
{name: 'myname', email: 'd@c.com', password: 'bar'},
|
||||
], function(err, users) {
|
||||
userIds = users.map(function(u) {
|
||||
], function(err, createdUsers) {
|
||||
users = createdUsers;
|
||||
userIds = createdUsers.map(function(u) {
|
||||
return u.pk;
|
||||
});
|
||||
next(err);
|
||||
|
@ -294,17 +300,15 @@ describe('User', function() {
|
|||
},
|
||||
function(next) {
|
||||
User.login({email: 'b@c.com', password: 'bar'}, function(err, accessToken) {
|
||||
accessTokenId = accessToken.userId;
|
||||
if (err) return next(err);
|
||||
assert(accessTokenId);
|
||||
assertGoodToken(accessToken, users[0]);
|
||||
next();
|
||||
});
|
||||
},
|
||||
function(next) {
|
||||
User.login({email: 'd@c.com', password: 'bar'}, function(err, accessToken) {
|
||||
accessTokenId = accessToken.userId;
|
||||
if (err) return next(err);
|
||||
assert(accessTokenId);
|
||||
assertGoodToken(accessToken, users[1]);
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
@ -424,12 +428,11 @@ describe('User', function() {
|
|||
});
|
||||
|
||||
it('allows login with password exactly 72 characters long', function(done) {
|
||||
User.create({email: 'b@c.com', password: pass72Char}, function(err) {
|
||||
User.create({email: 'b@c.com', password: pass72Char}, function(err, user) {
|
||||
if (err) return done(err);
|
||||
User.login({email: 'b@c.com', password: pass72Char}, function(err, accessToken) {
|
||||
if (err) return done(err);
|
||||
assertGoodToken(accessToken);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, user);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -502,9 +505,7 @@ describe('User', function() {
|
|||
describe('User.login', function() {
|
||||
it('Login a user by providing credentials', function(done) {
|
||||
User.login(validCredentials, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -513,9 +514,7 @@ describe('User', function() {
|
|||
it('Login a user by providing email credentials (email case-sensitivity off)', function(done) {
|
||||
User.settings.caseSensitiveEmail = false;
|
||||
User.login(validMixedCaseEmailCredentials, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -531,10 +530,8 @@ describe('User', function() {
|
|||
|
||||
it('Login a user by providing credentials with TTL', function(done) {
|
||||
User.login(validCredentialsWithTTL, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, validCredentialsWithTTL.ttl);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -547,10 +544,8 @@ describe('User', function() {
|
|||
|
||||
User.findById(accessToken.userId, function(err, user) {
|
||||
user.createAccessToken(120, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, 120);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -566,10 +561,8 @@ describe('User', function() {
|
|||
User.findById(accessToken.userId, function(err, user) {
|
||||
user.createAccessToken(120)
|
||||
.then(function(accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, 120);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
|
||||
done();
|
||||
})
|
||||
|
@ -588,17 +581,13 @@ describe('User', function() {
|
|||
this.accessTokens.create({ttl: ttl / 2}, cb);
|
||||
};
|
||||
User.login(validCredentialsWithTTL, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, 1800);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
|
||||
User.findById(accessToken.userId, function(err, user) {
|
||||
user.createAccessToken(120, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, 60);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
// Restore create access token
|
||||
User.prototype.createAccessToken = createToken;
|
||||
|
||||
|
@ -617,18 +606,14 @@ describe('User', function() {
|
|||
this.accessTokens.create({ttl: ttl / 2, scopes: options.scope}, cb);
|
||||
};
|
||||
User.login(validCredentialsWithTTLAndScope, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, 1800);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
assert.equal(accessToken.scopes, 'all');
|
||||
|
||||
User.findById(accessToken.userId, function(err, user) {
|
||||
user.createAccessToken(120, {scope: 'default'}, function(err, accessToken) {
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert.equal(accessToken.ttl, 60);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
assert.equal(accessToken.scopes, 'default');
|
||||
// Restore create access token
|
||||
User.prototype.createAccessToken = createToken;
|
||||
|
@ -652,13 +637,13 @@ describe('User', function() {
|
|||
it('Login should only allow correct credentials - promise variant', function(done) {
|
||||
User.login(invalidCredentials)
|
||||
.then(function(accessToken) {
|
||||
assert(!accessToken);
|
||||
expect(accessToken, 'accessToken').to.not.exist();
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(function(err) {
|
||||
assert(err);
|
||||
assert.equal(err.code, 'LOGIN_FAILED');
|
||||
expect(err, 'err').to.exist();
|
||||
expect(err).to.have.property('code', 'LOGIN_FAILED');
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -666,8 +651,8 @@ describe('User', function() {
|
|||
|
||||
it('Login a user providing incomplete credentials', function(done) {
|
||||
User.login(incompleteCredentials, function(err, accessToken) {
|
||||
assert(err);
|
||||
assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED');
|
||||
expect(err, 'err').to.exist();
|
||||
expect(err).to.have.property('code', 'USERNAME_EMAIL_REQUIRED');
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -676,13 +661,13 @@ describe('User', function() {
|
|||
it('Login a user providing incomplete credentials - promise variant', function(done) {
|
||||
User.login(incompleteCredentials)
|
||||
.then(function(accessToken) {
|
||||
assert(!accessToken);
|
||||
expect(accessToken, 'accessToken').to.not.exist();
|
||||
|
||||
done();
|
||||
})
|
||||
.catch(function(err) {
|
||||
assert(err);
|
||||
assert.equal(err.code, 'USERNAME_EMAIL_REQUIRED');
|
||||
expect(err, 'err').to.exist();
|
||||
expect(err).to.have.property('code', 'USERNAME_EMAIL_REQUIRED');
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -699,9 +684,7 @@ describe('User', function() {
|
|||
|
||||
var accessToken = res.body;
|
||||
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
assert(accessToken.user === undefined);
|
||||
|
||||
done();
|
||||
|
@ -811,8 +794,11 @@ describe('User', function() {
|
|||
});
|
||||
});
|
||||
|
||||
function assertGoodToken(accessToken) {
|
||||
assert(accessToken.userId);
|
||||
function assertGoodToken(accessToken, user) {
|
||||
if (accessToken instanceof AccessToken) {
|
||||
accessToken = accessToken.toJSON();
|
||||
}
|
||||
expect(accessToken).to.have.property('userId', user.pk);
|
||||
assert(accessToken.id);
|
||||
assert.equal(accessToken.id.length, 64);
|
||||
}
|
||||
|
@ -880,7 +866,7 @@ describe('User', function() {
|
|||
|
||||
it('Login a user by with email verification', function(done) {
|
||||
User.login(validCredentialsEmailVerified, function(err, accessToken) {
|
||||
assertGoodToken(accessToken);
|
||||
assertGoodToken(accessToken, validCredentialsEmailVerifiedUser);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -889,7 +875,7 @@ describe('User', function() {
|
|||
it('Login a user by with email verification - promise variant', function(done) {
|
||||
User.login(validCredentialsEmailVerified)
|
||||
.then(function(accessToken) {
|
||||
assertGoodToken(accessToken);
|
||||
assertGoodToken(accessToken, validCredentialsEmailVerifiedUser);
|
||||
|
||||
done();
|
||||
})
|
||||
|
@ -909,7 +895,7 @@ describe('User', function() {
|
|||
|
||||
var accessToken = res.body;
|
||||
|
||||
assertGoodToken(accessToken);
|
||||
assertGoodToken(accessToken, validCredentialsEmailVerifiedUser);
|
||||
assert(accessToken.user === undefined);
|
||||
|
||||
done();
|
||||
|
@ -1079,8 +1065,7 @@ describe('User', function() {
|
|||
|
||||
it('logs in a user by with realm', function(done) {
|
||||
User.login(credentialWithRealm, function(err, accessToken) {
|
||||
assertGoodToken(accessToken);
|
||||
assert.equal(accessToken.userId, user1.pk);
|
||||
assertGoodToken(accessToken, user1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -1088,8 +1073,7 @@ describe('User', function() {
|
|||
|
||||
it('logs in a user by with realm in username', function(done) {
|
||||
User.login(credentialRealmInUsername, function(err, accessToken) {
|
||||
assertGoodToken(accessToken);
|
||||
assert.equal(accessToken.userId, user1.pk);
|
||||
assertGoodToken(accessToken, user1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -1097,8 +1081,7 @@ describe('User', function() {
|
|||
|
||||
it('logs in a user by with realm in email', function(done) {
|
||||
User.login(credentialRealmInEmail, function(err, accessToken) {
|
||||
assertGoodToken(accessToken);
|
||||
assert.equal(accessToken.userId, user1.pk);
|
||||
assertGoodToken(accessToken, user1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -1115,8 +1098,7 @@ describe('User', function() {
|
|||
|
||||
it('logs in a user by with realm', function(done) {
|
||||
User.login(credentialWithRealm, function(err, accessToken) {
|
||||
assertGoodToken(accessToken);
|
||||
assert.equal(accessToken.userId, user1.pk);
|
||||
assertGoodToken(accessToken, user1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -1139,7 +1121,7 @@ describe('User', function() {
|
|||
login(logout);
|
||||
|
||||
function login(fn) {
|
||||
User.login({email: 'foo@bar.com', password: 'bar'}, fn);
|
||||
User.login(validCredentials, fn);
|
||||
}
|
||||
|
||||
function logout(err, accessToken) {
|
||||
|
@ -1152,7 +1134,7 @@ describe('User', function() {
|
|||
login(logout);
|
||||
|
||||
function login(fn) {
|
||||
User.login({email: 'foo@bar.com', password: 'bar'}, fn);
|
||||
User.login(validCredentials, fn);
|
||||
}
|
||||
|
||||
function logout(err, accessToken) {
|
||||
|
@ -1171,14 +1153,12 @@ describe('User', function() {
|
|||
.post('/test-users/login')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.send({email: 'foo@bar.com', password: 'bar'})
|
||||
.send(validCredentials)
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
|
||||
var accessToken = res.body;
|
||||
|
||||
assert(accessToken.userId);
|
||||
assert(accessToken.id);
|
||||
assertGoodToken(accessToken, validCredentialsUser);
|
||||
|
||||
fn(null, accessToken.id);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue