Merge pull request #3231 from strongloop/warn-on-misconfigured-accessToken-user
Warn on misconfigured access token user
This commit is contained in:
commit
ed4f56c7f8
|
@ -398,9 +398,88 @@ app.enableAuth = function(options) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._verifyAuthModelRelations();
|
||||||
|
|
||||||
this.isAuthEnabled = true;
|
this.isAuthEnabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
app._verifyAuthModelRelations = function() {
|
||||||
|
// Allow unit-tests (but also LoopBack users) to disable the warnings
|
||||||
|
if (this.get('_verifyAuthModelRelations') === false) return;
|
||||||
|
|
||||||
|
const AccessToken = this.registry.findModel('AccessToken');
|
||||||
|
const User = this.registry.findModel('User');
|
||||||
|
this.models().forEach(Model => {
|
||||||
|
if (Model === AccessToken || Model.prototype instanceof AccessToken) {
|
||||||
|
scheduleVerification(Model, verifyAccessTokenRelations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Model === User || Model.prototype instanceof User) {
|
||||||
|
scheduleVerification(Model, verifyUserRelations);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function scheduleVerification(Model, verifyFn) {
|
||||||
|
if (Model.dataSource) {
|
||||||
|
verifyFn(Model);
|
||||||
|
} else {
|
||||||
|
Model.on('attached', () => verifyFn(Model));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyAccessTokenRelations(Model) {
|
||||||
|
const belongsToUser = Model.relations && Model.relations.user;
|
||||||
|
if (belongsToUser) return;
|
||||||
|
|
||||||
|
const relationsConfig = Model.settings.relations || {};
|
||||||
|
const userName = (relationsConfig.user || {}).model;
|
||||||
|
if (userName) {
|
||||||
|
console.warn(
|
||||||
|
'The model %j configures "belongsTo User-like models" relation ' +
|
||||||
|
'with target model %j. However, the model %j is not attached to ' +
|
||||||
|
'the application and therefore cannot be used by this relation. ' +
|
||||||
|
'This typically happens when the application has a custom ' +
|
||||||
|
'custom User subclass, but does not fix AccessToken relations ' +
|
||||||
|
'to use this new model.\n' +
|
||||||
|
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||||
|
Model.modelName, userName, userName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
'The model %j does not have "belongsTo User-like model" relation ' +
|
||||||
|
'configured.\n' +
|
||||||
|
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||||
|
Model.modelName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyUserRelations(Model) {
|
||||||
|
const hasManyTokens = Model.relations && Model.relations.accessTokens;
|
||||||
|
if (hasManyTokens) return;
|
||||||
|
|
||||||
|
const relationsConfig = Model.settings.relations || {};
|
||||||
|
const accessTokenName = (relationsConfig.accessTokens || {}).model;
|
||||||
|
if (accessTokenName) {
|
||||||
|
console.warn(
|
||||||
|
'The model %j configures "hasMany AccessToken-like models" relation ' +
|
||||||
|
'with target model %j. However, the model %j is not attached to ' +
|
||||||
|
'the application and therefore cannot be used by this relation. ' +
|
||||||
|
'This typically happens when the application has a custom ' +
|
||||||
|
'AccessToken subclass, but does not fix User relations to use this ' +
|
||||||
|
'new model.\n' +
|
||||||
|
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||||
|
Model.modelName, accessTokenName, accessTokenName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
'The model %j does not have "hasMany AccessToken-like models" relation ' +
|
||||||
|
'configured.\n' +
|
||||||
|
'Learn more at http://ibm.biz/setup-loopback-auth',
|
||||||
|
Model.modelName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
app.boot = function(options) {
|
app.boot = function(options) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead'));
|
g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead'));
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
// License text available at https://opensource.org/licenses/MIT
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
module.exports = function(registry) {
|
module.exports = function(registry) {
|
||||||
// NOTE(bajtos) we must use static require() due to browserify limitations
|
// NOTE(bajtos) we must use static require() due to browserify limitations
|
||||||
|
|
||||||
|
@ -52,8 +55,33 @@ module.exports = function(registry) {
|
||||||
require('../common/models/checkpoint.js'));
|
require('../common/models/checkpoint.js'));
|
||||||
|
|
||||||
function createModel(definitionJson, customizeFn) {
|
function createModel(definitionJson, customizeFn) {
|
||||||
|
// Clone the JSON definition to allow applications
|
||||||
|
// to modify model settings while not affecting
|
||||||
|
// settings of new models created in the local registry
|
||||||
|
// of another app.
|
||||||
|
// This is needed because require() always returns the same
|
||||||
|
// object instance it loaded during the first call.
|
||||||
|
definitionJson = cloneDeepJson(definitionJson);
|
||||||
|
|
||||||
var Model = registry.createModel(definitionJson);
|
var Model = registry.createModel(definitionJson);
|
||||||
customizeFn(Model);
|
customizeFn(Model);
|
||||||
return Model;
|
return Model;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Because we are cloning objects created by JSON.parse,
|
||||||
|
// the cloning algorithm can stay much simpler than a general-purpose
|
||||||
|
// "cloneDeep" e.g. from lodash.
|
||||||
|
function cloneDeepJson(obj) {
|
||||||
|
const result = Array.isArray(obj) ? [] : {};
|
||||||
|
assert.equal(Object.getPrototypeOf(result), Object.getPrototypeOf(obj));
|
||||||
|
for (const key in obj) {
|
||||||
|
const value = obj[key];
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
result[key] = cloneDeepJson(value);
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -13,17 +13,23 @@ var loopback = require('../');
|
||||||
var extend = require('util')._extend;
|
var extend = require('util')._extend;
|
||||||
var session = require('express-session');
|
var session = require('express-session');
|
||||||
var request = require('supertest');
|
var request = require('supertest');
|
||||||
var app = loopback();
|
|
||||||
|
|
||||||
var Token = loopback.AccessToken.extend('MyToken');
|
var Token, ACL;
|
||||||
var ds = loopback.createDataSource({connector: loopback.Memory});
|
|
||||||
Token.attachTo(ds);
|
|
||||||
var ACL = loopback.ACL;
|
|
||||||
|
|
||||||
describe('loopback.token(options)', function() {
|
describe('loopback.token(options)', function() {
|
||||||
var app;
|
var app;
|
||||||
beforeEach(function(done) {
|
beforeEach(function(done) {
|
||||||
app = loopback();
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
|
Token = app.registry.createModel({
|
||||||
|
name: 'MyToken',
|
||||||
|
base: 'AccessToken',
|
||||||
|
});
|
||||||
|
app.model(Token, {dataSource: 'db'});
|
||||||
|
|
||||||
|
ACL = app.registry.getModel('ACL');
|
||||||
|
|
||||||
createTestingToken.call(this, done);
|
createTestingToken.call(this, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -284,7 +290,6 @@ describe('loopback.token(options)', function() {
|
||||||
' when enableDoubkecheck is true',
|
' when enableDoubkecheck is true',
|
||||||
function(done) {
|
function(done) {
|
||||||
var token = this.token;
|
var token = this.token;
|
||||||
var app = loopback();
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
req.accessToken = null;
|
req.accessToken = null;
|
||||||
next();
|
next();
|
||||||
|
@ -319,7 +324,6 @@ describe('loopback.token(options)', function() {
|
||||||
function(done) {
|
function(done) {
|
||||||
var token = this.token;
|
var token = this.token;
|
||||||
var tokenStub = {id: 'stub id'};
|
var tokenStub = {id: 'stub id'};
|
||||||
var app = loopback();
|
|
||||||
app.use(function(req, res, next) {
|
app.use(function(req, res, next) {
|
||||||
req.accessToken = tokenStub;
|
req.accessToken = tokenStub;
|
||||||
|
|
||||||
|
@ -439,8 +443,22 @@ describe('AccessToken', function() {
|
||||||
describe('app.enableAuth()', function() {
|
describe('app.enableAuth()', function() {
|
||||||
var app;
|
var app;
|
||||||
beforeEach(function setupAuthWithModels() {
|
beforeEach(function setupAuthWithModels() {
|
||||||
app = loopback();
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
app.enableAuth({dataSource: ds});
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
|
Token = app.registry.createModel({
|
||||||
|
name: 'MyToken',
|
||||||
|
base: 'AccessToken',
|
||||||
|
});
|
||||||
|
app.model(Token, {dataSource: 'db'});
|
||||||
|
|
||||||
|
ACL = app.registry.getModel('ACL');
|
||||||
|
|
||||||
|
// Fix User's "hasMany accessTokens" relation to use our new MyToken model
|
||||||
|
const User = app.registry.getModel('User');
|
||||||
|
User.settings.relations.accessTokens.model = 'MyToken';
|
||||||
|
|
||||||
|
app.enableAuth({dataSource: 'db'});
|
||||||
});
|
});
|
||||||
beforeEach(createTestingToken);
|
beforeEach(createTestingToken);
|
||||||
|
|
||||||
|
@ -517,7 +535,7 @@ describe('app.enableAuth()', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores token in the context', function(done) {
|
it('stores token in the context', function(done) {
|
||||||
var TestModel = loopback.createModel('TestModel', {base: 'Model'});
|
var TestModel = app.registry.createModel('TestModel', {base: 'Model'});
|
||||||
TestModel.getToken = function(cb) {
|
TestModel.getToken = function(cb) {
|
||||||
var ctx = LoopBackContext.getCurrentContext();
|
var ctx = LoopBackContext.getCurrentContext();
|
||||||
cb(null, ctx && ctx.get('accessToken') || null);
|
cb(null, ctx && ctx.get('accessToken') || null);
|
||||||
|
@ -527,7 +545,6 @@ describe('app.enableAuth()', function() {
|
||||||
http: {verb: 'GET', path: '/token'},
|
http: {verb: 'GET', path: '/token'},
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = loopback();
|
|
||||||
app.model(TestModel, {dataSource: null});
|
app.model(TestModel, {dataSource: null});
|
||||||
|
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
|
@ -552,8 +569,6 @@ describe('app.enableAuth()', function() {
|
||||||
|
|
||||||
// See https://github.com/strongloop/loopback-context/issues/6
|
// See https://github.com/strongloop/loopback-context/issues/6
|
||||||
it('checks whether context is active', function(done) {
|
it('checks whether context is active', function(done) {
|
||||||
var app = loopback();
|
|
||||||
|
|
||||||
app.enableAuth();
|
app.enableAuth();
|
||||||
app.use(contextMiddleware());
|
app.use(contextMiddleware());
|
||||||
app.use(session({
|
app.use(session({
|
||||||
|
@ -603,7 +618,8 @@ function createTestApp(testToken, settings, done) {
|
||||||
currentUserLiteral: 'me',
|
currentUserLiteral: 'me',
|
||||||
}, settings.token);
|
}, settings.token);
|
||||||
|
|
||||||
var app = loopback();
|
var app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
app.use(cookieParser('secret'));
|
app.use(cookieParser('secret'));
|
||||||
app.use(loopback.token(tokenSettings));
|
app.use(loopback.token(tokenSettings));
|
||||||
|
@ -635,7 +651,7 @@ function createTestApp(testToken, settings, done) {
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
});
|
});
|
||||||
app.use(loopback.rest());
|
app.use(loopback.rest());
|
||||||
app.enableAuth();
|
app.enableAuth({dataSource: 'db'});
|
||||||
|
|
||||||
Object.keys(appSettings).forEach(function(key) {
|
Object.keys(appSettings).forEach(function(key) {
|
||||||
app.set(key, appSettings[key]);
|
app.set(key, appSettings[key]);
|
||||||
|
@ -657,10 +673,8 @@ function createTestApp(testToken, settings, done) {
|
||||||
modelOptions[key] = modelSettings[key];
|
modelOptions[key] = modelSettings[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
var TestModel = loopback.PersistedModel.extend('test', {}, modelOptions);
|
var TestModel = app.registry.createModel('test', {}, modelOptions);
|
||||||
|
app.model(TestModel, {dataSource: 'db'});
|
||||||
TestModel.attachTo(loopback.memory());
|
|
||||||
app.model(TestModel);
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
|
@ -888,6 +888,10 @@ describe('app', function() {
|
||||||
var Customer = app.registry.createModel('Customer', {}, {base: 'User'});
|
var Customer = app.registry.createModel('Customer', {}, {base: 'User'});
|
||||||
app.model(Customer, {dataSource: 'db'});
|
app.model(Customer, {dataSource: 'db'});
|
||||||
|
|
||||||
|
// Fix AccessToken's "belongsTo user" relation to use our new Customer model
|
||||||
|
const AccessToken = app.registry.getModel('AccessToken');
|
||||||
|
AccessToken.settings.relations.user.model = 'Customer';
|
||||||
|
|
||||||
app.enableAuth({dataSource: 'db'});
|
app.enableAuth({dataSource: 'db'});
|
||||||
|
|
||||||
expect(Object.keys(app.models)).to.not.include('User');
|
expect(Object.keys(app.models)).to.not.include('User');
|
||||||
|
|
|
@ -15,5 +15,12 @@
|
||||||
"principalId": "$everyone",
|
"principalId": "$everyone",
|
||||||
"property": "create"
|
"property": "create"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"relations": {
|
||||||
|
"user": {
|
||||||
|
"type": "belongsTo",
|
||||||
|
"model": "user",
|
||||||
|
"foreignKey": "userId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -466,15 +466,13 @@ describe('Replication over REST', function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
function setupServer(done) {
|
function setupServer(done) {
|
||||||
serverApp = loopback();
|
serverApp = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
serverApp.set('remoting', {errorHandler: {debug: true, log: false}});
|
serverApp.set('remoting', {errorHandler: {debug: true, log: false}});
|
||||||
serverApp.enableAuth();
|
|
||||||
|
|
||||||
serverApp.dataSource('db', {connector: 'memory'});
|
serverApp.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
// Setup a custom access-token model that is not shared
|
// Setup a custom access-token model that is not shared
|
||||||
// with the client app
|
// with the client app
|
||||||
var ServerToken = loopback.createModel('ServerToken', {}, {
|
var ServerToken = serverApp.registry.createModel('ServerToken', {}, {
|
||||||
base: 'AccessToken',
|
base: 'AccessToken',
|
||||||
relations: {
|
relations: {
|
||||||
user: {
|
user: {
|
||||||
|
@ -485,18 +483,17 @@ describe('Replication over REST', function() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
serverApp.model(ServerToken, {dataSource: 'db', public: false});
|
serverApp.model(ServerToken, {dataSource: 'db', public: false});
|
||||||
serverApp.model(loopback.ACL, {dataSource: 'db', public: false});
|
|
||||||
serverApp.model(loopback.Role, {dataSource: 'db', public: false});
|
|
||||||
serverApp.model(loopback.RoleMapping, {dataSource: 'db', public: false});
|
|
||||||
|
|
||||||
ServerUser = loopback.createModel('ServerUser', USER_PROPS, USER_OPTS);
|
ServerUser = serverApp.registry.createModel('ServerUser', USER_PROPS, USER_OPTS);
|
||||||
serverApp.model(ServerUser, {
|
serverApp.model(ServerUser, {
|
||||||
dataSource: 'db',
|
dataSource: 'db',
|
||||||
public: true,
|
public: true,
|
||||||
relations: {accessTokens: {model: 'ServerToken'}},
|
relations: {accessTokens: {model: 'ServerToken'}},
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerCar = loopback.createModel('ServerCar', CAR_PROPS, CAR_OPTS);
|
serverApp.enableAuth({dataSource: 'db'});
|
||||||
|
|
||||||
|
ServerCar = serverApp.registry.createModel('ServerCar', CAR_PROPS, CAR_OPTS);
|
||||||
serverApp.model(ServerCar, {dataSource: 'db', public: true});
|
serverApp.model(ServerCar, {dataSource: 'db', public: true});
|
||||||
|
|
||||||
serverApp.use(function(req, res, next) {
|
serverApp.use(function(req, res, next) {
|
||||||
|
@ -518,7 +515,7 @@ describe('Replication over REST', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupClient() {
|
function setupClient() {
|
||||||
clientApp = loopback();
|
clientApp = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
clientApp.dataSource('db', {connector: 'memory'});
|
clientApp.dataSource('db', {connector: 'memory'});
|
||||||
clientApp.dataSource('remote', {
|
clientApp.dataSource('remote', {
|
||||||
connector: 'remote',
|
connector: 'remote',
|
||||||
|
@ -529,23 +526,26 @@ describe('Replication over REST', function() {
|
||||||
// model. This causes the in-process replication to work differently
|
// model. This causes the in-process replication to work differently
|
||||||
// than client-server replication.
|
// than client-server replication.
|
||||||
// As a workaround, we manually setup unique Checkpoint for ClientModel.
|
// As a workaround, we manually setup unique Checkpoint for ClientModel.
|
||||||
var ClientCheckpoint = loopback.Checkpoint.extend('ClientCheckpoint');
|
var ClientCheckpoint = clientApp.registry.createModel({
|
||||||
|
name: 'ClientCheckpoint',
|
||||||
|
base: 'Checkpoint',
|
||||||
|
});
|
||||||
ClientCheckpoint.attachTo(clientApp.dataSources.db);
|
ClientCheckpoint.attachTo(clientApp.dataSources.db);
|
||||||
|
|
||||||
LocalUser = loopback.createModel('LocalUser', USER_PROPS, USER_OPTS);
|
LocalUser = clientApp.registry.createModel('LocalUser', USER_PROPS, USER_OPTS);
|
||||||
if (LocalUser.Change) LocalUser.Change.Checkpoint = ClientCheckpoint;
|
if (LocalUser.Change) LocalUser.Change.Checkpoint = ClientCheckpoint;
|
||||||
clientApp.model(LocalUser, {dataSource: 'db'});
|
clientApp.model(LocalUser, {dataSource: 'db'});
|
||||||
|
|
||||||
LocalCar = loopback.createModel('LocalCar', CAR_PROPS, CAR_OPTS);
|
LocalCar = clientApp.registry.createModel('LocalCar', CAR_PROPS, CAR_OPTS);
|
||||||
LocalCar.Change.Checkpoint = ClientCheckpoint;
|
LocalCar.Change.Checkpoint = ClientCheckpoint;
|
||||||
clientApp.model(LocalCar, {dataSource: 'db'});
|
clientApp.model(LocalCar, {dataSource: 'db'});
|
||||||
|
|
||||||
var remoteOpts = createRemoteModelOpts(USER_OPTS);
|
var remoteOpts = createRemoteModelOpts(USER_OPTS);
|
||||||
RemoteUser = loopback.createModel('RemoteUser', USER_PROPS, remoteOpts);
|
RemoteUser = clientApp.registry.createModel('RemoteUser', USER_PROPS, remoteOpts);
|
||||||
clientApp.model(RemoteUser, {dataSource: 'remote'});
|
clientApp.model(RemoteUser, {dataSource: 'remote'});
|
||||||
|
|
||||||
remoteOpts = createRemoteModelOpts(CAR_OPTS);
|
remoteOpts = createRemoteModelOpts(CAR_OPTS);
|
||||||
RemoteCar = loopback.createModel('RemoteCar', CAR_PROPS, remoteOpts);
|
RemoteCar = clientApp.registry.createModel('RemoteCar', CAR_PROPS, remoteOpts);
|
||||||
clientApp.model(RemoteCar, {dataSource: 'remote'});
|
clientApp.model(RemoteCar, {dataSource: 'remote'});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2374,6 +2374,7 @@ describe('User', function() {
|
||||||
it('handles subclassed user with no accessToken relation', () => {
|
it('handles subclassed user with no accessToken relation', () => {
|
||||||
// setup a new LoopBack app, we don't want to use shared models
|
// setup a new LoopBack app, we don't want to use shared models
|
||||||
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.set('_verifyAuthModelRelations', false);
|
||||||
app.set('remoting', {errorHandler: {debug: true, log: false}});
|
app.set('remoting', {errorHandler: {debug: true, log: false}});
|
||||||
app.dataSource('db', {connector: 'memory'});
|
app.dataSource('db', {connector: 'memory'});
|
||||||
const User = app.registry.createModel({
|
const User = app.registry.createModel({
|
||||||
|
|
Loading…
Reference in New Issue