-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
propagate resolved roles in remoting context #2957
propagate resolved roles in remoting context #2957
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? |
4139d7f
to
61c0c85
Compare
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? To accept patch and trigger a build add comment ".ok\W+to\W+test." |
@ebarault thank you for the pull request! @raymondfeng I feel you are the best person to review this proposal at high level, could you PTAL? |
@slnode ok to test |
just rebased, but PR linter states 'PR out of date, needs to be rebased' |
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.
@ebarault I am afraid something went wrong during your rebase. The patch contains a bunch of eslint-related changes now, see https://github.com/strongloop/loopback/pull/2957/files
It may be best to simply hard-reset your feature branch to the latest master and then add your changes again on top of that.
$ git fetch strongloop
$ git reset --hard strongloop/master
$ git push -f
Could you PTAL?
.eslintrc
Outdated
@@ -5,6 +5,7 @@ | |||
"ignoreComments": true, | |||
"ignoreUrls": true, | |||
"ignorePattern": "^\\s*var\\s.+=\\s*(require\\s*\\()|(/)" | |||
}] | |||
}], | |||
"no-unused-expressions": "off", |
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.
Please revert.
Gruntfile.js
Outdated
@@ -4,6 +4,7 @@ | |||
// License text available at https://opensource.org/licenses/MIT | |||
|
|||
/*global module:false*/ | |||
'use strict'; |
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.
Changes in this file looks unrelated to me, could you please revert?
I am little bit confused here. As far as I understand LoopBack's ACL system, there may be several different role arrays:
What is stored in your new context property |
@bajtos about the pr intention: you're right, I should have made it clearer: the one i'm focusing here is the 3rd one: intersection of roles allowed to run the remote method and roles the current user belongs to -or- rephrasing in my terms, to make sure we get each other: the roles for which the user is authorized to run a given remote method, which is in essence the exact purpose of the following course of actions: so when this set of tasks is achieved, for each acl covering the method the question has been solved "is the user in a role that can run the method?" : the general idea is to keep track of this result for further use in methods code or hooks to perform advanced CRUD. A very basic example of when this can be useful: say a user is allowed to run a method by 2 roles, those 2 roles having conceptually very different access levels (custom role $organization:owner on one hand, and built-in smart role $authenticated) : inside that single method implementation you might want to treat the user differently from just an authenticated user, compared to an organization admin. Keeping track of the roleResolving status and making it accessible in the methods code greatly simplifies this process by avoiding running the role resolvers again for each role of interest for the method. |
5f1b896
to
a34f321
Compare
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? |
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.
Reviewed, PTAL.
common/models/acl.js
Outdated
*/ | ||
ACL.prototype.isAllowed = function(defaultPermission) { | ||
var permission = this.permission; | ||
if (permission === ACL.DEFAULT) { |
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.
What a mess :(
I am afraid we don't have any specification (either as a documentation or a executable test) for what's the correct behaviour, therefore we have to rely on what we can see elsewhere in the code and try to not break it.
Could you please capture your reasoning in a code comment, so that future readers understand better why isAllowed
is implemented this way?
common/models/acl.js
Outdated
@@ -399,8 +411,16 @@ module.exports = function(ACL) { | |||
if (!(context instanceof AccessContext)) { | |||
context = new AccessContext(context); | |||
} | |||
// initialize the authorizedRoles array in RemotingContext args options object | |||
var remotingContext = context && context.remotingContext; |
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.
I think context
is always set, see L410-412 above, therefore the check context &&
can be removed.
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.
👍
common/models/acl.js
Outdated
var remotingContext = context && context.remotingContext; | ||
if (remotingContext) { | ||
var options = remotingContext.args.options || {}; | ||
remotingContext.args.options = extend(options, {authorizedRoles: []}); |
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.
I think we shouldn't be adding args.options
property if the invoked method does not have any options
argument. Also what if the remote method expects options of a different type, e.g. an array or an object?
I am proposing the following:
const authorizedRoles = [];
if (remotingContext) {
const options = remotingContext.args.options;
if (options && typeof options === 'object') { // null is object too
options.authorizedRoles = authorizedRoles;
}
}
Maybe move the code modifying remotingContext
to the very end of this method?
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.
fair enough, minus the next comment with regard to the standalone authorizedRoles array
common/models/acl.js
Outdated
var options = remotingContext.args.options || {}; | ||
remotingContext.args.options = extend(options, {authorizedRoles: []}); | ||
} | ||
var authorizedRoles = []; |
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.
I think this should be set to the same array as in remotingContext.args.options
. Unless you want to prevent remote method from tampering with the array value used inside ACL.checkAccessForContext
?
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.
actually yes, it's some sort of security failsafe : i prepare an array of permissions on one side and initialize the content of options.authorizedRoles to [] on the other side.
Then only if the result of checkAccessForContext is ALLOW and set options.authorizedRoles to the value of the array of permissions we've computed. I captured the rationale in a comment
common/models/acl.js
Outdated
@@ -419,13 +439,19 @@ module.exports = function(ACL) { | |||
var effectiveACLs = []; | |||
var staticACLs = self.getStaticACLs(model.modelName, property); | |||
|
|||
// push unique roles in provided array | |||
var addRole = function(roles, role) { | |||
if (role && roles.indexOf(role) === -1) { |
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 perhaps use an object (a key/value map) instead of an array? That way this can be inlined at calling places as authorizedRoles[role] = true
. I think it all depends on how do you expect applications to consume this API. If they will be looking up authorized roles by name, then a key/value map is probably easier to use.
test/acl.test.js
Outdated
var loopback = require('../index'); | ||
var Scope = loopback.Scope; | ||
var ACL = loopback.ACL; | ||
var request = require('supertest'); | ||
var Promise = require('bluebird'); | ||
var supertest = require('supertest-as-promised')(Promise); |
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.
FWIW, the recent versions of supertest provide Promise API natively, supertest-as-promised was deprecated. Let's wait for #3196 to be 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.
#3196 was landed.
test/acl.test.js
Outdated
request = supertest(app); | ||
|
||
// attaching base models | ||
['User', 'Role', 'RoleMapping', 'ACL', 'AccessToken'].map(setupModel); |
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.
app.enableAuth({ dataSource: 'db' })
automatically attaches all required models to the given datasource.- any app model can be accessed via
app.models
, so it should be enough to setvar models = app.models;
app.enableAuthentication({dataSource: 'db'});
models = app.models;
This way, setupModel
shouldn't be needed at all.
test/acl.test.js
Outdated
app.model(models.MyTestModel, {dataSource: 'db'}); | ||
|
||
// beforeRemote hook on MyTestModel, as a promise | ||
models.MyTestModel.onBeforeRemote = new Promise(function(resolve, reject) { |
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.
Uff, I find this very confusing. Can you simplify to the following?
models.MyTestModel.beforeRemote('find', function(ctx, notUsed,next) {
models.MyTestModel.lastRemotingContext = ctx;
next();
});
Unless I am missing something here?
test/acl.test.js
Outdated
|
||
// creating a user, a role and a rolemapping binding that user with that role | ||
return Promise.all([ | ||
models.User.create({username: 'myUser', email: 'myuser@email.com', password: 'password'}), |
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.
We use @example.com
addresses, and usually set password to pass
in the tests.
See http://loopback.io/doc/en/contrib/style-guide.html#email-examples, although it shows password set to bar
(don't know why).
@@ -897,7 +897,7 @@ describe.onServer('Remote Methods', function() { | |||
request(app).get('/TestModels/saveOptions') | |||
.expect(204, function(err) { | |||
if (err) return done(err); | |||
expect(actualOptions).to.eql({accessToken: null}); |
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.
Why is this change needed? The original code should produce a more helpful error message on failure.
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.
so... that's funny : you asked 29 days ago why this change was required, and i could not remember why. It happens that the test as-is fails since actualOptions is no longer equal to {accessToken: null}
but to {accessToken: null, authorizedRoles: {...}}
.
So i reverted my fix, and then you had the error you mentioned in your comment here
i rolled the fix back, testing if accessToken is null is actually what we are testing here
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.
Sounds good.
Is it worth adding a comment explaining that actualOptions
contains other properties too?
Alternatively, can we use expect(actualOptions).to.include({accessToken: null})
?
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.
👍 used this latter form
@ebarault ping - I am just wondering, what's the status of this patch? |
i still plan to finish it in the coming days as part of the in-remote-method access control toolbox |
3fa19b4
to
6738cf4
Compare
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 reviewed your comments/proposal, PTAL
common/models/acl.js
Outdated
*/ | ||
ACL.prototype.isAllowed = function(defaultPermission) { | ||
var permission = this.permission; | ||
if (permission === ACL.DEFAULT) { |
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.
👍
common/models/acl.js
Outdated
@@ -399,8 +411,16 @@ module.exports = function(ACL) { | |||
if (!(context instanceof AccessContext)) { | |||
context = new AccessContext(context); | |||
} | |||
// initialize the authorizedRoles array in RemotingContext args options object | |||
var remotingContext = context && context.remotingContext; |
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.
👍
common/models/acl.js
Outdated
var remotingContext = context && context.remotingContext; | ||
if (remotingContext) { | ||
var options = remotingContext.args.options || {}; | ||
remotingContext.args.options = extend(options, {authorizedRoles: []}); |
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.
fair enough, minus the next comment with regard to the standalone authorizedRoles array
common/models/acl.js
Outdated
var options = remotingContext.args.options || {}; | ||
remotingContext.args.options = extend(options, {authorizedRoles: []}); | ||
} | ||
var authorizedRoles = []; |
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.
actually yes, it's some sort of security failsafe : i prepare an array of permissions on one side and initialize the content of options.authorizedRoles to [] on the other side.
Then only if the result of checkAccessForContext is ALLOW and set options.authorizedRoles to the value of the array of permissions we've computed. I captured the rationale in a comment
common/models/acl.js
Outdated
// add authorizedRoles to remotingContext if overall resolved permission status is ALLOW | ||
if (resolved.permission === ACL.ALLOW && remotingContext) { | ||
remotingContext.args.options.authorizedRoles = authorizedRoles; | ||
}; | ||
return callback(null, resolved); |
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.
agreed
common/models/acl.js
Outdated
// checkAccessForContext resolved permission status is ALLOW, | ||
// else set it to empty object | ||
authorizedRoles = permission === ACL.ALLOW ? authorizedRoles : {}; | ||
setAuthorizedRoles(authorizedRoles); |
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.
reduced down the whole setAuthorizedRoles logic to just these 2 lines in the main code block
common/models/acl.js
Outdated
if (options && typeof options === 'object') { // null is object too | ||
options.authorizedRoles = authorizedRoles; | ||
} | ||
} |
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 related helper to perform check ups on context.remotingContext.args.options before proceeding
@@ -5,10 +5,13 @@ | |||
|
|||
'use strict'; | |||
var assert = require('assert'); | |||
var expect = require('./helpers/expect'); |
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.
👍
common/models/acl.js
Outdated
if (resolved && resolved.permission === ACL.DEFAULT) { | ||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW; | ||
permission = defaultPermission || ACL.ALLOW; | ||
} |
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.
you were proposing to share here the same logic as in ACL.isAllowed() but here were are not quite doing the same thing and not testing the same type of object:
- resolved is not an instance of ACL but an instance of AccessRequest. I'm reluctant in using ACL.isAllowed() on the permission part of an AccessRequest instance. Proper way would be to use the existing AccessRequest.prototype.isAllowed after adapting it to accept a default permission
- we are not testing if allowed, we are changing the permission type if actually DEFAULT, because it is not done either here or there which is used in model.js. This could be avoided and the code simplified/unified if were to align the implementation of
AccessRequest.prototype.isAllowed
If you agree, i can track this as a different issue and address later
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.
Uff, this goes too deep, beyond my current understanding. I'll leave this decision up to you to make.
Perhaps add a code comment explaining why we cannot use ACL.isAllowed()
here?
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.
👍 actually goes too deep to be discussed async i'm afraid ah ah
i'll figure out something
common/models/acl.js
Outdated
// set authorizedRoles in remotingContext options argument if | ||
// checkAccessForContext resolved permission status is ALLOW, | ||
// else set it to empty object | ||
authorizedRoles = permission === ACL.ALLOW ? authorizedRoles : {}; |
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.
then if we commit the changes discussed before, we can change this to
authorizedRoles = resolved.isAllowed() ? authorizedRoles : {};
which would be better
(no need to even pass the defaultPermission since the accessRequest has already the targeted model and hence the defaultPermission)
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 updated code looks pretty good! I left few more comments to address, I hope they should be minor and easy to fix.
Please fix npm test
, right now it's failing:
Remote Methods Model.createOptionsFromRemotingContext sets empty options.accessToken for anonymous requests:
Uncaught AssertionError: expected { Object (accessToken, authorizedRoles) } to deeply equal { accessToken: null }
+ expected - actual
{
"accessToken": [null]
- "authorizedRoles": {}
}
(See e.g. https://travis-ci.org/strongloop/loopback/jobs/208560030)
common/models/acl.js
Outdated
@@ -456,15 +489,34 @@ module.exports = function(ACL) { | |||
if (err) return callback(err, null); | |||
|
|||
var resolved = self.resolvePermission(effectiveACLs, req); | |||
var permission = resolved && resolved.permission; | |||
if (resolved && resolved.permission === ACL.DEFAULT) { |
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.
I think this can be simplified to if (permission === ACL.DEFAULT
, thoughts?
common/models/acl.js
Outdated
if (resolved && resolved.permission === ACL.DEFAULT) { | ||
resolved.permission = (model && model.settings.defaultPermission) || ACL.ALLOW; | ||
permission = defaultPermission || ACL.ALLOW; | ||
} |
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.
Uff, this goes too deep, beyond my current understanding. I'll leave this decision up to you to make.
Perhaps add a code comment explaining why we cannot use ACL.isAllowed()
here?
common/models/acl.js
Outdated
return callback(null, resolved); | ||
}); | ||
}); | ||
return callback.promise; | ||
|
||
// helpers | ||
function setAuthorizedRoles(authorizedRoles) { |
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.
I am proposing to move this function out of ACL.checkAccessForContext
and add remotingContext
as a new parameter, so that the function does not require a closure from ACL.checkAccessForContext
to access that reference.
There are two reasons for my proposal:
- I personally consider such code easier to reason about, because it's easier to see what data is read/touched by
setAuthorizedRoles
- It's a also a (micro) performance optimisation. In the current implementation, V8 has to create a new function every time
ACL.checkAccessForContext
is called. I don't know what's the current status, but V8 was not able to optimise such short-living functions in the past. By moving the function out, there is only one function that's called many times, and therefore V8 can optimise the heck out of it.
I would also like to propose to use a different name, e.g. saveAuthorizedRolesToRemotingContext
, and to omit the //helpers
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.
wouahh, thanks for the optimization trick, done
common/models/acl.js
Outdated
// helpers | ||
function setAuthorizedRoles(authorizedRoles) { | ||
var remotingContext = context.remotingContext; | ||
const options = remotingContext && remotingContext.args.options; |
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 check that remotingContext.args
is set too, just to be 100% sure?
test/acl.test.js
Outdated
.then(function() { | ||
return request.get('/MyTestModels') | ||
.set('X-Access-Token', accessToken.id) | ||
.expect(200); |
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.
This code is repeated in all tests, can you extract a shared helper function please? E.g.
// usage
.then(makeAuthorizedHttpRequest)
// implementation
function makeAuthorizedHttpRequest() {
return request.get('/MyTestModels')
.set(...)
.expect(200);
}
test/acl.test.js
Outdated
app = loopback({localRegistry: true, loadBuiltinModels: true}); | ||
app.use(loopback.rest()); | ||
app.set('remoting', {errorHandler: {debug: true, log: true}}); | ||
app.enableAuth(); |
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.
Please remove in favour of L563.
test/acl.test.js
Outdated
|
||
// creating a custom model | ||
models.MyTestModel = app.registry.createModel('MyTestModel'); | ||
app.model(models.MyTestModel, {dataSource: 'db'}); |
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.
Uff, modifying app.models
directly is not a good practice. I am proposing the following:
const MyTestModel = app.registry.createModel('MyTestModel');
app.model(MyTestModel, {dataSource: 'db'});
// models.MyTestModel was set by app.model() by now
test/acl.test.js
Outdated
app.model(models.MyTestModel, {dataSource: 'db'}); | ||
|
||
// capturing the value of the last remoting context | ||
models.MyTestModel.beforeRemote('find', function(ctx, notUsed, next) { |
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.
Nitpick (feel free to ignore): unused
instead of notUsed
?
990a608
to
7906fb3
Compare
@bajtos : i applied changes from last code review NOTE
More comments inlined. |
*/ | ||
ACL.resolvePermission = function resolvePermission(acls, req) { | ||
if (!(req instanceof AccessRequest)) { | ||
req.registry = this.registry; | ||
req = new AccessRequest(req); |
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.
as I did with AccessContext objects, I propose to add the Registry instance to the AccessRequest definition, so we can access models from that class, this is required by the new settleDefaultPermission
method
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.
When specifying an "arg": "options" in a common/models/xxx.json 'accepts' section, we ran into problems, finding 'authorizedRoles: {}' added to what was intended to be a list of options to pass to a system call, causing problems since we are simply passing the options values forward (in which case authorizedRoles becomes an invalid option). Our workaround was to rename the arg from "options" to something else (eg "lsfOptions". It would be more convenient to not have to worry about this; without knowing the inner workings, having the authorizedRoles in some more uniquely named structure would help the situation.
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.
hi @ejvaitkus : i'm afraid it's not specific to the authorizedRoles
property as this remote method argument is also used to pass remoting context properties downstream to hooks and methods. see https://loopback.io/doc/en/lb3/Using-current-context.html
it's on purpose that the generic arg options
is used as is in LB3.
common/models/acl.js
Outdated
property: req.property, | ||
accessType: req.accessType, | ||
permission: permission || ACL.DEFAULT, | ||
registry: this.registry}); | ||
return res; |
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.
switching to one object param declaration when adding the registry as it reduces chances for further errors (adding things like (param1, param2, null, null, param5)
)
Note: this should the default documented option, as for AccessContext
* @param {String|Error} err The error object | ||
* @param {AccessRequest} result The access permission | ||
* @param {String|Error} err The error object. | ||
* @param {AccessRequest} result The resolved access request. |
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 former definition is very vague
@@ -332,10 +337,11 @@ module.exports = function(ACL) { | |||
var accessTypeQuery = (accessType === ACL.ALL) ? undefined : | |||
{inq: [accessType, ACL.ALL, ACL.EXECUTE]}; | |||
|
|||
var req = new AccessRequest(model, property, accessType); | |||
var req = new AccessRequest({model, property, accessType, registry: this.registry}); | |||
|
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.
adding registry to AccessRequest, going to one object param
|
||
var acls = this.getStaticACLs(model, property); | ||
|
||
// resolved is an instance of AccessRequest | ||
var resolved = this.resolvePermission(acls, req); |
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.
further to commenting, i'd like to ask if ok (here or latter) to switch the name of the variable: resolved is just vague. This should be called something with accessRequest in it to make further code maintenance easier
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.
Good idea. Can we leave it out of scope of this pull request though, to make it easier for me to review the already long list of changes?
* @param {String} accessType The access type | ||
* @param {String} permission The requested permission | ||
* @param {String[]} methodNames The names of involved methods | ||
* @param {Registry} registry The application or global registry | ||
* @returns {AccessRequest} |
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.
jsdoc fixes
@@ -268,14 +287,19 @@ function AccessRequest(model, property, accessType, permission, methodNames) { | |||
this.property = obj.property || AccessContext.ALL; | |||
this.accessType = obj.accessType || AccessContext.ALL; | |||
this.permission = obj.permission || AccessContext.DEFAULT; | |||
this.methodNames = methodNames || []; |
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.
this line is wrong
} else { | ||
this.model = model || AccessContext.ALL; | ||
this.property = property || AccessContext.ALL; | ||
this.accessType = accessType || AccessContext.ALL; | ||
this.permission = permission || AccessContext.DEFAULT; | ||
this.methodNames = methodNames || []; | ||
this.registry = registry; |
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.
adding registry, as in L291
} | ||
// do not create AccessRequest without a registry | ||
assert(this.registry, | ||
'Application registry is mandatory in AccessRequest but missing in provided argument(s)'); | ||
} |
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.
asserting we have a registry in accessrequest instances
this.permission = defaultPermission || 'ALLOW'; | ||
}; | ||
|
||
/** | ||
* Is the request for access allowed? |
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.
new accessrequest instance method to help keeping the code tidier.
see code jsdoc 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.
new accessrequest instance method to help keeping the code tidier.
see code jsdoc comment
This GitHub comment seems not relevant to me, am I missing something?
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.
no idea TBH
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.
Thank you for the update, I think this is 99% ready.
- Should we include
getValueForRoleInAuthorizedRoles
in this pull request? - PTAL at the last three comments related to tests
The rest can be fixed later in a new pull request.
For posterity, after online chat, we decided with @ebarault to land these changes in a single pull request, even though they are mixing refactoring/code cleanup with adding new features, which makes future code archaeology difficult.
|
||
var acls = this.getStaticACLs(model, property); | ||
|
||
// resolved is an instance of AccessRequest | ||
var resolved = this.resolvePermission(acls, req); |
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.
Good idea. Can we leave it out of scope of this pull request though, to make it easier for me to review the already long list of changes?
common/models/acl.js
Outdated
var modelClass = self.registry.findModel(model); | ||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; | ||
} | ||
resolved.settleDefaultPermission(); |
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.
I am wondering why settleDefaultPermission
is not called by resolePermission
? What is the reasoning why a caller of resolvePermission
would want to see unresolved ALC.DEFAULT
?
AFAICT, L339/L345 is one of those places. I am not sure whether that's intentional.
Anyhow, let's leave this improvement out of scope of this pull request.
common/models/acl.js
Outdated
/** | ||
* Check if the request has the permission to access. | ||
* @options {Object} context See below. | ||
* @options {AccessContext} context An AccessContext instance. |
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.
I think this is not correct, the method allows a plain object to be passed in this parameter. How about this?
@options {AccessContext|Object} context An AccessContext instance or a plain object with the following properties.
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.
I think this whole jsdoc block should be mostly the same as we have for AccessContext
constructor, where you have {AccessContext|Object}
type AFAICT.
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.
👍
common/models/acl.js
Outdated
* @property {String} accessType The access type: | ||
* READ, REPLICATE, WRITE, or EXECUTE. | ||
* @param {Function} callback Callback function | ||
* @property {String} accessType The access type: READ, REPLICATE, WRITE, or EXECUTE. |
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.
Nitpick: Isn't this line too long? What is the benefit compared to the original solution? AFAIK, strong-docs supports multi-line parameter descriptions.
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 rolled back
common/models/acl.js
Outdated
* @property {Object[]} principals An array of principals. | ||
* @property {String|Model} model The model name or model class. |
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.
I think we should revert this back to {String|Model} model
. If the caller is passing AccessContext
instance then they don't need a description of properties duplicated here. The property descriptions should be used when passing a plain data object for constructing a new AccessContext
, in which case only model
should be provided (see
loopback/lib/access-context.js
Lines 39 to 42 in 960e118
var model = context.model; | |
model = ('string' === typeof model) ? this.registry.getModel(model) : model; | |
this.model = model; | |
this.modelName = model && model.modelName; |
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.
did it and also removed modelName then
will come back to this latter, we should not be confusing one property for two different things
common/models/acl.js
Outdated
ACL.getValueForRoleInAuthorizedRoles = function(context) { | ||
return true; | ||
}; | ||
|
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.
I am proposing to defer async version of this method until there are users asking for it.
this.permission = defaultPermission || 'ALLOW'; | ||
}; | ||
|
||
/** | ||
* Is the request for access allowed? |
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.
new accessrequest instance method to help keeping the code tidier.
see code jsdoc comment
This GitHub comment seems not relevant to me, am I missing something?
@@ -157,6 +160,8 @@ describe('security ACLs', function() { | |||
acls = acls.map(function(a) { return new ACL(a); }); | |||
|
|||
var perm = ACL.resolvePermission(acls, req); | |||
// remove the registry from AccessRequest instance to ease asserting | |||
delete perm.registry; | |||
assert.deepEqual(perm, {model: 'account', |
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.
Instead of modifying the resolved object, how about using include
assertion from chai
to verify only a subset of model properties?
expect(perm).to.include({
model: 'account',
// ...
methodNames: []
});
It may not work because of issues that were fixed by chaijs/chai#761 (which was not released in a stable version yet), in which case it's ok to keep your implementation, but please add a TODO comment so that we can eventually clean this code.
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.
👍 yeap this does not work ATM, i left a comment
test/acl.test.js
Outdated
}, | ||
} | ||
); | ||
// fix back the original method |
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.
This won't work when the expect
call above fails.
Ideally, we should be creating a fresh app instance for each test and thus it should not matter whether a test changes built-in models. I think you are already doing that, aren't you?
The worser alternative is to move this fix from .then
into .finally
block.
@@ -897,7 +897,7 @@ describe.onServer('Remote Methods', function() { | |||
request(app).get('/TestModels/saveOptions') | |||
.expect(204, function(err) { | |||
if (err) return done(err); | |||
expect(actualOptions).to.eql({accessToken: null}); |
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.
Sounds good.
Is it worth adding a comment explaining that actualOptions
contains other properties too?
Alternatively, can we use expect(actualOptions).to.include({accessToken: null})
?
7906fb3
to
8a34495
Compare
if (!(this instanceof AccessRequest)) { | ||
return new AccessRequest(model, property, accessType); | ||
return new AccessRequest(model, property, accessType, permission, methodNames); |
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.
I think this will not work, because the new argument registry
is not forwarded.
Looks like this decrease comes from the changes in I am proposing to ignore this tiny decrease in code coverage numbers. |
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.
final review
common/models/acl.js
Outdated
var modelClass = self.registry.findModel(model); | ||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; | ||
} | ||
resolved.settleDefaultPermission(); |
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.
👍 very valid point, my initial intention was to put it in the constructor, but i leave this for later
common/models/acl.js
Outdated
/** | ||
* Check if the request has the permission to access. | ||
* @options {Object} context See below. | ||
* @options {AccessContext} context An AccessContext instance. |
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.
👍
common/models/acl.js
Outdated
* @property {String} accessType The access type: READ, REPLICATE, WRITE, or EXECUTE. | ||
* @callback {Function} callback Callback function | ||
* @param {String|Error} err The error object. | ||
* @param {AccessRequest} result The resolved access request. | ||
*/ |
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, you're right, TBH i find this so ugly... modelName
should have been used when seeking to build an accesscontext by the model name...
I also removed modelName then, it's of no use here
@@ -157,6 +160,8 @@ describe('security ACLs', function() { | |||
acls = acls.map(function(a) { return new ACL(a); }); | |||
|
|||
var perm = ACL.resolvePermission(acls, req); | |||
// remove the registry from AccessRequest instance to ease asserting | |||
delete perm.registry; | |||
assert.deepEqual(perm, {model: 'account', |
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.
👍 yeap this does not work ATM, i left a comment
@@ -897,7 +897,7 @@ describe.onServer('Remote Methods', function() { | |||
request(app).get('/TestModels/saveOptions') | |||
.expect(204, function(err) { | |||
if (err) return done(err); | |||
expect(actualOptions).to.eql({accessToken: null}); |
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.
👍 used this latter form
common/models/acl.js
Outdated
var modelClass = self.registry.findModel(model); | ||
resolved.permission = (modelClass && modelClass.settings.defaultPermission) || ACL.ALLOW; | ||
} | ||
resolved.settleDefaultPermission(); |
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.
👍
common/models/acl.js
Outdated
* @property {Object[]} principals An array of principals. | ||
* @property {String|Model} model The model name or model class. |
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.
did it and also removed modelName then
will come back to this latter, we should not be confusing one property for two different things
common/models/acl.js
Outdated
* @property {String} accessType The access type: | ||
* READ, REPLICATE, WRITE, or EXECUTE. | ||
* @param {Function} callback Callback function | ||
* @property {String} accessType The access type: READ, REPLICATE, WRITE, or EXECUTE. |
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 rolled back
this.permission = defaultPermission || 'ALLOW'; | ||
}; | ||
|
||
/** | ||
* Is the request for access allowed? |
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.
no idea TBH
Adds an authorizedRoles object to remotingContext.args.options which contains all the roles (static and dynamic) that are granted to the user when performing a request through strong-remoting to an app with authentication enabled. The authorizedRoles object for example looks like: { $everyone: true, $authenticated: true, myRole: true } NOTE: this pr also covers a number of jsdoc fixes as well as refactoring in ACL.js and access-context.js
8a34495
to
8aa98a8
Compare
Landed 🎉 |
Description
For some advanced CRUD access control in remote methods, it is often required to get/check roles resolved (i.e. allowed) for a user against a method with
Role.getRoles(context)
orRole.isInRole(role, context)
.In addition to clutter the remote methods / hooks code, it forces the app to run again through all static roles and registered dynamic role resolvers, inducing performance loss and extra calls to db.
Meanwhile, the exact same course of action is performed right when the
Model.checkAccess()
method is called to verify if a principal is allowed to access a protected resource :Model.checkAccess(context)
->ACL.checkAccessForContext(context)
->acls.forEach(acl, ...)
->roleModel.isInRole(acl.principalId, context)
This PR proposes to add a new param (
Array
) in remoting context (name proposed:resolvedRoles
, to be discussed) which is filled during Model Access Permission Checking phase with the roles for which the user is allowed to run the remote method.an example of
resolvedRoles
is:['admin', '$owner']
where admin is a custom role statically mapped to a principal, and $owner is a built-in smart roleresolvedRoles
param can then be accessed in remote method and remote method hooks asctx.req.remotingContext.resolvedRoles
To be noted: the
resolvedRoles
param is set in a way that the calling user cannot temper the value to fool the additional access control policies added by user in remote methodsReviewers: please review/discuss the general idea before i provide the required tests
Related issues
Checklist
guide