Skip to content

Conversation

@ebarault
Copy link
Contributor

@ebarault ebarault commented Mar 9, 2017

Description

Add a hasMultipleUserModelsConfig property on loopback application to reflect the User setup throughout the application
Perform sanity check on the go on user model config to detect and warn the user (and eventually stop the application in case the config is not workable)

This continues the work started by @bajtos in #3231
First draft provided for general discussion.

Related issues

#3140 (warn on misconfigured user/accessToken model relations)
#3231 (enable multiple owner $owner resolving)
#2971 (enable loopback to handle multiple user models)

Checklist

  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style
    guide

@0candy 0candy added the #wip label Mar 9, 2017
@crandmck crandmck added the review label Mar 9, 2017
@bajtos bajtos self-requested a review March 9, 2017 12:45
@ebarault ebarault changed the title first commit for general discussion Detect User model config at app boot Mar 9, 2017
scheduleVerification(Model, verifyUserRelations);
}
verifyUserModelsSetup().then(()=> {
verifyRelationsSetup();
Copy link
Contributor Author

@ebarault ebarault Mar 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general idea of why not make the whole two together:

  1. first we detect the user models config status
  2. then we can use it in verifyRelationsSetup to even go further in the bad config detection since things get to change according to one config or the other

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general idea of why not make the whole two together:

  1. first we detect the user models config status
  2. then we can use it in verifyRelationsSetup to even go further in the bad config detection since things get to change according to one config or the other

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Code-wise, this line can be simplified to verifyUserModelsSetup.then(verifyRelationsSetup).

scheduleVerification(Model, verifyUserRelations);
}
verifyUserModelsSetup().then(()=> {
verifyRelationsSetup();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general idea of why not make the whole two together:

  1. first we detect the user models config status
  2. then we can use it in verifyRelationsSetup to even go further in the bad config detection since things get to change according to one config or the other

});
}
});
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i needed to change @bajtos first implem to add async support as we now expect on this function to resolve when done, so we can use it to first check user models config and then check how the relations are set up

// we use Promise.reflect() here to let all the tests go without throwing,
// this way we can get all the logs for all models we are checking
return Promise.map(userModels, (model) => {
return scheduleVerification(model, hasMultipleUserModelsConfig).reflect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified (although I am not sure if the line won't become too long):

return Promise.map(userModels, 
  model => scheduleVerification(model, hasMultipleUserModelsConfig).reflect());

.then((inspections) => {
// proceed to next checkups if no error
let hasErrors = inspections.filter(inspection => {
return inspection.isRejected();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto:

inspections.filter(inspection => inspection.isRejected());

);
});

delete self.hasMultipleUserModelsConfig;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is in an arrow function =>, you should be able to use this. Arrow functions preserve the value of this reference.


delete self.hasMultipleUserModelsConfig;

if (!dismissRejection) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is another example of a double-negative.

if (Model.dataSource) {
try {
resolve(verifyFn(Model));
} catch (e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the Promise constructor is required to catch errors thrown by the factory function, therefore it shouldn't be needed to catch them ourselves.

Copy link
Contributor Author

@ebarault ebarault Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, i checked. If i just do resolve(veryFn(Model))
then the verifyFn throws an error, the following error will be caught when trying to reflect the inspection result in scheduleVerification(model, hasMultipleUserModelsConfig).reflect();:

TypeError: cannot get fulfillment value of a non-fulfilled promise

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's weird. This is what Bluebird's docs say about the error:

https://github.com/petkaantonov/bluebird/blob/master/docs/docs/error-explanations.md#error-cannot-get-fulfillment-value-of-a-non-fulfilled-promise

You can get this error when you're trying to call .value or .error when inspecting a promise where the promise has not been fulfilled or rejected yet.

For example:

var p = Promise.delay(1000);
p.inspect().value();

Consider using .isPending() .isFulfilled() and .isRejected() in order to inspect the promise for status.

This looks like a possible bug in bluebird to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nevermind... thanks for digging, the error was on my side...

return this._verifyAuthModelRelations(app._warnOnBadAuthModelRelations)
.then(() => {
this.isAuthEnabled = true;
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as _verifyAuthModelRelations is now async and returns a promise, this change is required
also i made enableAuth() return a promise to, as it's de facto async too now, it's not a breaking change to me, the method can still be called in an app while not caring about the returned promise if most if not all use cases.

This is useful in the tests so we can wait for enableAuth to return before checking the displayed warnings

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am quite concerned about this change. We use app.isAuthEnabled in other places (e.g.

if (app.isAuthEnabled) {
var AccessToken = registry.getModelByType('AccessToken');
handlers.push(loopback.token({model: AccessToken, app: app}));
}
), but possibly in any 3rd party LoopBack code to detect whether the app has authentication enabled. While our REST middleware makes this check few ticks later (at which time this promise chain has already finished), I think it's not safe to assume that everybody else is doing the same.

Having said that, I agree that making enableAuth async & promise returning is a good direction.

What is your reasoning for enabling isAuthEnabled only after auth-model relations have been verified? What would happen if we keep only _verifyAuthModelRelations async and set app.isAuthEnabled right on the first tick, as we are doing now?

BTW in the current proposal, the app will end up in an inconsistent state when _verifyAuthModelRelations -models and the remoting authorization hook are all set up, but the flag is saying otherwise.

Copy link
Contributor Author

@ebarault ebarault Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, i agree this is the main point to discuss.

In loopback i think this is not a problem since the features/modules relying on this flag comes few ticks later, but i agree we should care about 3rd party apps.
I dropped this implementation quickly to make this visible and get your feedback.

In fact i noted when implementing the tests that i could not easily test directly the _verifyAuthModelRelations method alone since it require the auth models and relations are all set up. Which is usually a benefit of using enableAuth(), so i needed to call _verifyAuthModelRelations through enableAuth, and then way for the latter's returned promise to resolve to properly get the warning messages. So at first i just modified enableAuth to return a promise. Then i considered the meaning of the flag and decided to set it after the health check has resolved.

The rationale:
Leaving aside backward compat for a sec: a valid auth setup health check function would not result in setting an isAuthEnabled flag true if the auth model was incorrect is some ways. We could project for v4 that the app deliberately stops in situations that now trigger only warnings, until the user fixes the issues or activate some failover flags to bypass the situation. Thoughts?

👉 For now: Setting app.isAuthEnabling to true before the call _verifyAuthModelRelations would probably not have a big impact actually and could keep a variant where it still set it sync and let the method return async after the checks have completed:

  • either the relations are not well set for single user model and the flag will be set and the app will warn, this is the current behavior, also this does not mean that the flag should actually be set since the app is not configured properly
  • or the multiple users config if not good (including relations), the flag will have been set but the app will crash, so there's not risk in setting the flag sync

So this could be

  // ...
  // still sync up to this point
  this.isAuthEnabled = true;
  return this._verifyAuthModelRelations(app._warnOnBadAuthModelRelations); // async
}

As the enableAuth() method currently returns nothing, there's no risk that it will conflict in existing 3rd party apps.

Of course we can additionaly already implement the target implement with a flag and set the flag only latter and decide to halt the app if warnings occur with a feature flag (wrapping your other proposal about an option to return sync or async)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dropped this implementation quickly to make this visible and get your feedback.

👍

Leaving aside backward compat for a sec: a valid auth setup health check function would not result in setting an isAuthEnabled flag true if the auth model was incorrect is some ways. We could project for v4 that the app deliberately stops in situations that now trigger only warnings, until the user fixes the issues or activate some failover flags to bypass the situation. Thoughts?

I agree that if the backwards compatibility was not an issue (e.g. in v4), then enableAuth should fail when the configuration is invalid.

// still sync up to this point
this.isAuthEnabled = true;
return this._verifyAuthModelRelations(app._warnOnBadAuthModelRelations); // async

👍 let's take this approach now.

const warnOnBadSetup = this.get('_warnOnBadAuthModelsSetup') !== false;
// Prevent the app from being killed although the configuration is inconsistent
const abortOnBadUserSetup = this.get('_abortOnBadUserSetup') !== false;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reworded the flags to avoid double negations

Model.modelName, userName, userName);
const modelTo = (relationsConfig.user || {}).model;
if (modelTo) {
warnFn('CUSTOM_USER_MODEL_NOT_AVAILABLE', {modelFrom, modelTo});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suggest changing the var names to modelFrom and modelTo, rather than modelName and userName, as it makes the tests more readable.
if you'd like to keep the original names, i will map the original names so the tests are readable

if (modelTo) {
warnFn('CUSTOM_ACCESS_TOKEN_MODEL_NOT_AVAILABLE', {modelFrom, modelTo});
return;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito

};

// function warnOnBadAuthModelRelations(code, args) {
app._warnOnBadAuthModelRelations = function(code, args) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

built-in warning function ("private")
i actually need it to be attached on the app object so i can swap it in the tests when calling enableAuth()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered passing the custom implementation via enableAuth options instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a big deal though.

this._verifyAuthModelRelations();

this.isAuthEnabled = true;
return this._verifyAuthModelRelations(app._warnOnBadAuthModelRelations)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In existing applications that are not waiting for enableAuth to resolve, when the verification code throws an error, the rejected promise will end up as unhandled promise rejection. This typically produces only a console warning, it does not crash the application as an uncaught error would.

(node:90355) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 3): Error

I am proposing to introduce an options flag to control the sync/async behaviour. For example:

// old way - throws error
app.enableAuth({dataSource: 'db'});

// new way - returns rejected promise
app.enableAuth({dataSource: 'db', async: true})
  .then(...)
  .catch(...);

// alternatively, replace the flag with a new method name
app.enableAuthAsync({dataSource: 'db'});

(The "Async" prefix is already used by Bluebird's promisify API)

app.enableAuth();
expect(app.isAuthEnabled).to.equal(true);
return app.enableAuth().then(() => {
expect(app.isAuthEnabled).to.equal(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This goes back to my first comment https://github.com/strongloop/loopback/pull/3265/files#r107652656.

When we have to change a test in a significant way, as we are doing here, then I think it's a very strong sign that we are breaking backwards compatibility.

app.enableAuth({dataSource: 'db'});

expect(Object.keys(app.models)).to.include.members(AUTH_MODELS);
return app.enableAuth({dataSource: 'db'}).then(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change shouldn't be needed, because we are attaching the models synchronously in the current implementation. Unless I am missing something?

AccessToken.settings.relations.user.model = 'Customer';

app.enableAuth({dataSource: 'db'});
return app.enableAuth({dataSource: 'db'}).then(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, I think this change is not needed.

// 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('_warnOnBadAuthModelsSetup', false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind reverting this rename? I don't really care about the name, it's just to avoid code churn.

Copy link
Contributor Author

@ebarault ebarault Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would mean that it's the exact same name as the app method
app. _verifyAuthModelRelations()
is it problematic?

the rationale for this change is: the _verifyAuthModelRelations method, for a same name, has now a larger scope than the equally named flag

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would mean that it's the exact same name as the app method

That shouldn't matter, because the flag is stored in a config hash-map.

the rationale for this change is: the _verifyAuthModelRelations method, for a same name, has now a larger scope than the equally named flag

I see your point, let's make the proposed change then.

@ebarault ebarault changed the title Detect User model config at app boot Validate User model config at app boot Mar 24, 2017
@stale stale bot added the stale label Aug 23, 2017
@stale stale bot closed this Aug 23, 2017
@0candy 0candy removed the review label Aug 23, 2017
@stale
Copy link

stale bot commented Aug 23, 2017

This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants