-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Roles should follow acls #4895
Roles should follow acls #4895
Conversation
Codecov Report
@@ Coverage Diff @@
## master #4895 +/- ##
==========================================
- Coverage 94.3% 94.14% -0.17%
==========================================
Files 121 122 +1
Lines 8760 8845 +85
==========================================
+ Hits 8261 8327 +66
- Misses 499 518 +19
Continue to review full report at Codecov.
|
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.
Thanks for the PR, there's a lot of inconsistencies in the style (I know it should be enforced by the eslint but somehow it isnt). Can you make it consistent, including the trailing semi-colons, spaces in if (condition) {
etc...
spec/ParseRole.spec.js
Outdated
@@ -76,71 +78,13 @@ describe('Parse Role testing', () => { | |||
return role.save({}, { useMasterKey: true }); | |||
}; | |||
|
|||
it("should not recursively load the same role multiple times", (done) => { |
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 should keep that test please.
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.
Alright, no problem.
spec/ParseRole.spec.js
Outdated
// For each role, fetch their sibling, what they inherit | ||
// return with result and roleId for later comparison | ||
const promises = [admin, moderator, contentManager, superModerator].map((role) => { | ||
return auth._getAllRolesNamesForRoleIds([role.id]).then((result) => { | ||
const authRoles = auth.getAuthRoles() |
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.
Ok so this test fails because the roles are not readable by any one so not applied
src/AuthRoles.js
Outdated
for (let index = 0; index < parentIds.length; index++) { | ||
const parentId = parentIds[index]; | ||
const parentRole = this.manifest[parentId] | ||
if(!parentRole) continue; |
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.
can we get curly brackets 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.
sure thing
src/AuthRoles.js
Outdated
* Resolves with a promise once all roles inherited by a role are fetched. | ||
* Inherited roles are roles a single role has access to by the 'roles' relation. | ||
*/ | ||
AuthRoles.prototype.findRolesOfRolesRecursively = function() { |
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 prefer algorithms without side effects instead of mutations of the global object state: ie: instead of storing the properties in the global state object, store the result of each operation locally, and then return the final result after the recursion for example:
function somethingResursive(currentState = {}) {
if (breakingCondition) {
return currentState;
}
const result = doSomething():
const mergedResult = mergeStates(currentState, result);
return somethingResursive(mergedResult);
}
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.
Alright, let me see what I can do about this.
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.
It’s cleaner and more testable usually :) also, please use ES6 classes now :)
src/AuthRoles.js
Outdated
this.auth = auth | ||
this.userId = auth.user ? auth.user.id : undefined | ||
this.master = master | ||
this.isMaster = isMaster; |
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 is the difference between those two? Why do we need both?
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 i dont think we do. I am working on another branch in which I removed all these unneeded and duplicate checks.
src/AuthRoles.js
Outdated
export function AuthRoles(auth: Auth, master, isMaster = false) { | ||
this.auth = auth | ||
this.userId = auth.user ? auth.user.id : undefined | ||
this.master = master |
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 parameter shoould likely be config
or masterConfig
src/AuthRoles.js
Outdated
const name = roleNames[index]; | ||
const statement = acl[name] | ||
if(statement){ | ||
if(statement["read"] === true) 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.
please avoid those at all costs, they don't help readability nor maintainability
if (statement["read"] === true) {
return true;
}
src/AuthRoles.js
Outdated
|
||
// Or | ||
function isAnyExplicitlyGranted(acl, roleNames){ | ||
for (let index = 0; index < roleNames.length; index++) { |
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.
use
roleNames.find((name) => {
/// logic here
});
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
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 have actually found that its faster to loop over the ACL and look through the role names from a set (.has())
I will change it for you to review
src/AuthRoles.js
Outdated
* "read" is true | ||
* @returns {Boolean} | ||
*/ | ||
const _isReadableAcl = (statement) => statement.read === 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.
@flovilmart don't know if this is needed. I don't recall if an ACL with r/w set to false can be available, because the read key all together will not be available. (ie "roleName" : { "write": true }. without read).
But I thought I would explicitly check the 'read' access.
const acl = role.ACL; | ||
// Dont bother checking if the ACL | ||
// is empty or corrupt | ||
if(acl === {} || !acl){ |
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.
Not sure if this could happen
spec/ParseRole.spec.js
Outdated
|
||
// 1 query for the direct roles (parent roles). | ||
// 1 query for each role after that, including the parent role. | ||
expect(restExecute.calls.count()).toEqual(5); |
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.
@flovilmart An increase in calls is to be expected here since for each role we have to know the parent to properly compile the tree.
@flovilmart i am not sure how to request a review this point :) |
I just need to take the time to finally review it and take care of it |
@flovilmart |
.then(response => response.results); | ||
}else{ | ||
const query = new Parse.Query(Parse.Role); | ||
_.forEach(restWhere, (value, key) => { |
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.
@flovilmart
I wanted to add an additional constraint here, to select only needed keys, just in case more are available to limit the returned data.
// limit returned data just in case other keys are available
query.select(['objectId', 'ACL']);
// or simply since acl, createdAt, savedAt are selected by default
query.select('objectId');
But for some reason the query stoped returning data (no objects).
Do you have knowlege of why this could happen ? I have diregarded it since its not replated to the PR,
but was just wondering why it wouldnt
I am worried that we’re taking the problem the wrong way. It seems that you attempt to resolve all the roles with the masterKey and then filter out the ones the user can’t read. Which is well, contrary to the promise of parse. Why don’t we do:
This way, this can only be consistent with the general approach, and the amount of changes will be minimal. |
So, I will give it more thoughts and attempt to find a case where it won't work. |
Well, if it doesn’t work, that has broader implications. Each role, directly owning a user gives the user this roles’s powers. Using those powers we need to discover which other powers are inherited down the line. If this doesn’t work cleanly this way, we’ll go with the original suggestion of using a new ‘inactive’ Boolean but i’d rather avoid. Also if possible, can we not introduce a new AuthRoles thing as other PR’s are working on abstraction of the role module and cache |
So I ran something quickly, and it seems that the main issue that I face is when a role is self referencing:
As you can see in this example, the role can be read only by users from this role, which will probably be true for every role if the developer says that only people with this role will be able to read. Somehow, this is consistent with the ACL strategy, If you can't read somethiung, you can't know you're in it. |
The main reasoning behind your proposed solution is to skip local ACL test all together and rely soley on a list of accessible roles (aclList) to fetch the next one and so on. Also the query will look something like this:
And personally i really hate using "notContainedIn" since it may slow things down on large arrays and collections. |
The solution i have proposed, is no less secure that what Auth.js is doing now. |
Regarding this
I agree with you, roles should follow ACL. Enable/Disable should be avoided. It does not solve the main problem. |
@georgesjamous well, I have a solution that removes the complete necessity of the AuthRoles and that actually is very similar to the current implementation. I have more tests to write for it stay tuned I'll post the branch soon. |
will do |
@georgesjamous did you have a look? Anything that look wrong? |
I haven't had the chance yet. |
I am gonna go ahead and close this then since you have already implemented most changes on |
This is related to #4821
Since roles are used to enforce ACL on another object, they cannot be used to enforce ACL on themselves. So the only way I could think of to do this is to fetch all roles normally and build a tree-like list and compute ACL access manually on each Role.
Code enhancements could still be done, I would like another set of eyes on this since its a security matter. Maybe another more straightforward way is available.
Reasoning:
Branch : R1 -> R2 -> R3 -> R4
This is a role tree-branch where R1 is directly accessible (user is in "users" relation).
R2 is fetched because of
R2.getRoles().add(R1)
. R3 is fetched because ofR3.getRoles().add(R2)
Computation:
In order to compute access, we need to check every node from the inside out.
R4 then R3 then R2 then R1. If at any time the cycle breaks (ie, we stumbled upon a role that is inaccessible) we throw the branch. (ie, if R2 is inaccessible, R4, R3 & R2 are rejected)
Multiple paths are computed in an Or manner. (ie, at least one path need to be ok)
R1 -> R2 -> R3 -> R4 and R6 -> R4 (for R4 we have R6 'Or' R3 -> R2 -> R1)
Caveats:
Throwing a role and be done with it is not enough, a second branch, later on, could provide us with access to a role we already rejected. A brute force operation would be to retry everything whenever a role is accepted. So in order to do this in a better way, the code will keep a reference to the role that rejected a role and will retry it explicitly when the first role is accepted.
Concerns:
Accepting this PR means that it will break some apps since Role ACLs is not being taken into consideration during "read". How about a server level configuration option to select which type of Role permissions to use "legacy" or "advanced" for example.
Or, we could set ACL by default that a role is 'read' by himself if not explicitly set to 'false' when creating a role.