From 79f441b9c4e614dd39e35ca22008fd17dfc403e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 24 Feb 2017 14:07:41 +0100 Subject: [PATCH] Verify User and AccessToken relations at startup Modify `app.enableAuth()` to verify that (custom) User and AccessToken models have correctly configured their hasMany/belongsTo relations and print a warning otherwise. --- lib/application.js | 79 +++++++++++++++++++ test/access-token.test.js | 4 + test/app.test.js | 4 + .../common/models/access-token.json | 11 ++- test/user.test.js | 1 + 5 files changed, 97 insertions(+), 2 deletions(-) diff --git a/lib/application.js b/lib/application.js index 92bf7200..62db7858 100644 --- a/lib/application.js +++ b/lib/application.js @@ -398,9 +398,88 @@ app.enableAuth = function(options) { } }; + this._verifyAuthModelRelations(); + 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) { throw new Error( g.f('{{`app.boot`}} was removed, use the new module {{loopback-boot}} instead')); diff --git a/test/access-token.test.js b/test/access-token.test.js index a8f20752..dace75b5 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -454,6 +454,10 @@ describe('app.enableAuth()', function() { 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); diff --git a/test/app.test.js b/test/app.test.js index b160ce35..ab56eff3 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -888,6 +888,10 @@ describe('app', function() { var Customer = app.registry.createModel('Customer', {}, {base: 'User'}); 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'}); expect(Object.keys(app.models)).to.not.include('User'); diff --git a/test/fixtures/access-control/common/models/access-token.json b/test/fixtures/access-control/common/models/access-token.json index 6b42d503..6061de02 100644 --- a/test/fixtures/access-control/common/models/access-token.json +++ b/test/fixtures/access-control/common/models/access-token.json @@ -15,5 +15,12 @@ "principalId": "$everyone", "property": "create" } - ] -} \ No newline at end of file + ], + "relations": { + "user": { + "type": "belongsTo", + "model": "user", + "foreignKey": "userId" + } + } +} diff --git a/test/user.test.js b/test/user.test.js index f9b7d5d7..a87c0f82 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -2374,6 +2374,7 @@ describe('User', function() { it('handles subclassed user with no accessToken relation', () => { // setup a new LoopBack app, we don't want to use shared models app = loopback({localRegistry: true, loadBuiltinModels: true}); + app.set('_verifyAuthModelRelations', false); app.set('remoting', {errorHandler: {debug: true, log: false}}); app.dataSource('db', {connector: 'memory'}); const User = app.registry.createModel({