-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Enable multiple user models inheriting from base class User #2971
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Can one of the admins verify this patch? To accept patch and trigger a build add comment ".ok\W+to\W+test." |
|
Can one of the admins verify this patch? |
3 similar comments
|
Can one of the admins verify this patch? |
|
Can one of the admins verify this patch? |
|
Can one of the admins verify this patch? |
|
Can one of the admins verify this patch? |
1 similar comment
|
Can one of the admins verify this patch? |
|
Can one of the admins verify this patch? To accept patch and trigger a build add comment ".ok\W+to\W+test." |
|
Can one of the admins verify this patch? |
1 similar comment
|
Can one of the admins verify this patch? |
3513deb to
20025be
Compare
|
@bajtos @raymondfeng can you PTAL |
|
Hi @ebarault, thank you for the pull request. The use case you are trying to support makes sense to me. My main concern is about backwards compatibility. Can we modify your implementation to support existing applications that don't have I think the following changes may address the issue:
Thoughts? I would also like to hear from @raymondfeng |
|
Can one of the admins verify this patch? To accept patch and trigger a build add comment ".ok\W+to\W+test." |
|
Can one of the admins verify this patch? |
3 similar comments
|
Can one of the admins verify this patch? |
|
Can one of the admins verify this patch? |
|
Can one of the admins verify this patch? |
ebarault
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos : reviewed your comments (see comments inline) + rebased
ready for review
|
|
||
| var AccessToken = this.constructor; | ||
| var userRelation = AccessToken.relations.user; // may not be set up | ||
| var User = userRelation && userRelation.modelTo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bajtos : I think we need to honour AccessToken.relations.user.modelTo as the default User model. Consider the case where app has custom User and AccessToken models, e.g. in order to customise some of built-in methods and/or settings.
In that case, principalType is not set. Before your change, the code will look up the related user model. After your change, the built-in User model would be incorrectly used.
i moved back to the original implementation following your comment.
although: if I correctly get the docs for Registry.getModelByType(...) we could use getModelByType('User') instead of AccessToken.relations.user.modelTo as the method allows to
to find configured models in models.json over the base model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although: if I correctly get the docs for Registry.getModelByType(...) we could use getModelByType('User') instead of AccessToken.relations.user.modelTo as the method allows to
to find configured models in models.json over the base model.
That may work in most cases, but I would be concerned about changes in edge cases, e.g. when the app has a custom User-based model that's actually not used for authentication, i.e. AccessToken is attached to the base User model. (That's just an example.)
Let's play it safe and keep the current implementation.
| cb(null, isValid); | ||
| process.nextTick(function() { | ||
| cb(null, isValid); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the original implem did not respect the async api
| cb(e); | ||
| process.nextTick(function() { | ||
| cb(e); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
| }); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
callback false if a valid User can't be found with the accessToken's principalType
| 'use strict'; | ||
| var loopback = require('../../lib/loopback'); | ||
| var utils = require('../../lib/utils'); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all changes related to promisifying RoleMapping model are covered in #3169
lib/access-context.js
Outdated
| } | ||
| // iterate on the upper base model | ||
| modelBase = modelBase.definition.settings.base; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@batjos: Uff, I am actually confused, what is the purpose of the code here and below, where you are crawling model hierarchy?
you commented on an earlier implementation:
@bajtos : This does not work with a deeper hierarchy (Customer extends BaseUser extends built-in User).
to what i replied :
@ebarault : you're right, i noted that too. we should test recursively up to the top User model to check if we ultimately get the built-in User model.
I changed the implementation accordingly and added a test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any particular reason against using this simpler version?
var BaseUser = this.registry.getModel('User');
for (var i = 0; i < this.principals.length; i++) {
// ...
// 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;
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way i get Registry.getModel(): this.registry.getModel('User') would actually return one among several models inheriting from built-in User model, not the built-in User Model itself
then i think userModel.prototype instanceof BaseUser would only return true for models being instances or inheriting from that particular User model
do you see things different?
| // no token for userFromAnotherModel | ||
| {userId: userFromAnotherModel.id, principalType: 'AnotherUser'}, | ||
| ]); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking for all tokens and checking against a set of expected data
| accessContext.addPrincipal(principal.type, principal.id); | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed for this helper, as per your suggestion
| expect(user).to.have.property('email', userFromOneModel.email); | ||
| }); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isolated the whole logic of verifying the temp accessToken info in a dedicated helper, as per your suggestion
fe72201 to
c1a8495
Compare
| context = context || {}; | ||
|
|
||
| assert(context.registry, | ||
| 'Application registry is mandatory in AccessContext but missing in provided context'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we fall back to loopback.registry when context.registry is not provided, in order to preserve backwards compatibility? loopback.registry is the global singleton where models like loopback.User are defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we should only fallback to loopback.registry in case localRegistry is set to false, or not set and running a loopback version where it defaults to false. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO we should only fallback to loopback.registry in case localRegistry is set to false, or not set and running a loopback version where it defaults to false.
Sure, that sounds good too. It may be best to leave this change out of scope of this pull request, so that this patch can be finally landed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure !!!
I will take care of this in a second step
|
@ebarault good job! I pushed two commits to your feature branch:
If you are happy with these changes, then please rebase your patch on top of the latest master (it will fix the failing CI) and squash the commits to a single one. (No need to preserve my authorship of those last minor changes.) If you are not, then let's discuss a bit more :) |
|
@bajtos: wow, what a journey ! |
| if (userModel.modelName === 'User') { | ||
| if (userModel.prototype instanceof BaseUser) { | ||
| return p.id; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way i get Registry.getModel(): this.registry.getModel('User') would actually return one among several models inheriting from built-in User model, not the built-in User Model itself
then i think userModel.prototype instanceof BaseUser would only return true for models being instances or inheriting from that particular User model
do you see things different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
registry.getModel() calls registry.findModel() and returns exactly the one model registered with the given name - see lib/registry.js#L299 in current master. Here is the code in juggler adding an entry to modelBuilder.models: lib/model-builder.js#L203.
Perhaps you are confusing registry.getModels() with app.models or registry.getModelByType?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
arff..... yes! :-) i just couldn't see this ! (confusing getModel() with getModelByType() !!)
Then it should do, thanks.
Just one last check: you're confident in model prototypal inheritance in loopback and there's no way a model extending 'User' can return false to instanceof BaseUser ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one last check: you're confident in model prototypal inheritance in loopback and there's no way a model extending 'User' can return false to
instanceof BaseUser?
Yes, I am pretty confident:
-
Model inheritance in LoopBack uses Node.js'
util.inheritsunder the hood, see model-builder.js#L200 -
We are already relying on
instanceofin the following places:
5e3d613 to
084053e
Compare
|
@bajtos: squashed and rebased. all good now :-) let's land it ! |
084053e to
b490421
Compare
Allow LoopBack applications to configure multiple User models and share the same AccessToken model. To enable this feature: 1) In your custom AccessToken model: - add a new property "principalType" of type "string". - configure the relation "belongsTo user" as polymorphic, using "principalType" as the discriminator 2) In your User models: - Configure the "hasMany accessTokens" relation as polymorphic, using "principalType" as the discriminator When creating custom Role and Principal instances, set your User model's name as the value of "prinicipalType".
b490421 to
9fe084f
Compare
|
@ebarault I have amended the commit message to include more details, most notably basic usage instructions. I hope I got them right. As I was re-reading the code to find how to use this new feature, it occurred to me that there is one test missing. We are verifying that Could you please contribute the documentation for this feature too? @crandmck is the best person to help you to figure out where to put the new content, I would personally start in http://loopback.io/doc/en/lb3/Authentication-authorization-and-permissions.html and/or the nested pages: |
|
@bajtos :
LGTM: we pretty much managed to make this that simple to use in the end
yes of course
ok for new PR |
|
🎉 🎉 🎉 LANDED 🎉 🎉 🎉 As you said, @ebarault, it was quite a journey to get here. Thank you very much for your persistence and all the effort you have invested to make this happen. I appreciate it a lot! 🙇 |
|
Many thanks @bajtos for joining forces in that quest ! |
|
@ebarault big thanks for getting this into loopback. If you're working on the documentation feel free to ping me to proofread or test it. |
|
@beeman : i'll definitely will :-) just need to have a coupled of final tweaks to this feature merged in master before working on the documentation |
|
Bonjour @ebarault, I have one Project model based on PersistedModel, two user models: AppUser and ContractorUser base on User model with a Mysql Database to keep my ACL rules and having exactly codebase of this example application (https://github.com/strongloop/loopback-example-access-control ). I'm wondering if you can tell me what's the best practice to set the principalType and principalId in the accessContext, inside a remote hook or an operation hook ? ERROR: As {"type":"AppUser", "id":"3"} is excepted, AppUser 3 has an admin role to access find property of Project Model. Pls find, my db data below: and my security debug log below, Thanks in advance. |

Description
Currently, the loopback app does not support defining multiple user models inheriting from the base class User, for mainly 2 reasons:
static relations binding the User base class with some key related models
--> belongsTo relation between base class AccessToken and base class User
--> belongsTo relation between base class RoleMapping and base class User
--> hasMany relation between base class User and base class AccessToken
app.registrymethodgetModelByType()which is used at several places to get a reference on the User child model by its parent class type, but which does not support multiple User inherited modelsThis PR proposes to rely on polymorphic relations to allow the loopback app supporting multiple User inherited models.
getModelByType(loopback.User)is not used anymore to get the User model as they can be multipleprincipalModelName / userModelNameis added to RoleMapping and AccessToken base class to support the polymorphic relations in selecting the rightmodelToAll existing impacted tests have been updated.
Usage
To enable this feature:
using "principalType" as the discriminator
using "principalType" as the discriminator
When creating custom Role and Principal instances, set your
User model's name as the value of "prinicipalType".
Related issues
#2516
Checklist
guide