Skip to content

Support Authorities Without Role Prefix in RoleHierarchyImpl Builder #15272

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

Closed

Conversation

nielsbasjes
Copy link
Contributor

@marcusdacoregio here is my take on #15264

This is 3 changes (hence 3 commits):

  • The proposed way of being able to add the mapping from any authority into 1 or more roles.
  • I found that if you add to a hierarchy builder the same role with some extra roles the existing roles are discarded. This is different from what you get with the text format.
  • The tests to makes sure this actually works became unreadable (hence I wasn't sure anymore) so I added a few helper functions to make it a lot better to understand what the test does.

Are the documentation changes I created enough?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jun 18, 2024
@marcusdacoregio marcusdacoregio self-assigned this Jun 18, 2024
@marcusdacoregio marcusdacoregio added in: core An issue in spring-security-core type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Jun 18, 2024
@marcusdacoregio
Copy link
Contributor

Hi @nielsbasjes, thanks for the report.

I found that if you add to a hierarchy builder the same role with some extra roles the existing roles are discarded. This is different from what you get with the text format.

Would you please move this to another PR? From my perspective it isn't exactly a bug but a support that should be added to the builder. A new PR is better so we can track exactly when/what changed.

@nielsbasjes nielsbasjes force-pushed the AuthorityRoleHierarchy branch from ceac3fd to 35ded39 Compare June 19, 2024 17:20
@nielsbasjes
Copy link
Contributor Author

Hi @nielsbasjes, thanks for the report.

I found that if you add to a hierarchy builder the same role with some extra roles the existing roles are discarded. This is different from what you get with the text format.

Would you please move this to another PR? From my perspective it isn't exactly a bug but a support that should be added to the builder. A new PR is better so we can track exactly when/what changed.

I have removed that part from this PR.
After you have approved this I'll put that up for separate approval (to avoid needless merge complexity).

@nielsbasjes
Copy link
Contributor Author

@marcusdacoregio is this how you want this merge request?
If not then just tell me what I need to change.

@marcusdacoregio marcusdacoregio added this to the 6.4.0-M1 milestone Jun 24, 2024
@marcusdacoregio
Copy link
Contributor

Hi @nielsbasjes. I plan to review this PR before 6.4.0-M1 on July 15th. I'll leave my feedback if there is anything else to be changed.

@nielsbasjes
Copy link
Contributor Author

@marcusdacoregio How shall I submit the other change? I'm asking because it is partially overlapping code.

@marcusdacoregio marcusdacoregio changed the title Authority and RoleHierarchy Support Authorities Without Role Prefix in RoleHierarchyImpl Builder Jun 25, 2024
.theseAuthorities("ROLE_A", "ROLE_B", "ROLE_C", "ROLE_D");

assertHierarchy(roleHierarchyImpl).givesToAuthorities("C")
.theseAuthorities("C", "ROLE_B", "ROLE_C", "ROLE_D", "ROLE_E", "ROLE_F");
Copy link
Contributor

Choose a reason for hiding this comment

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

As of now, using .authority("C").implies("E", "F", "B"), means that C will have ROLE_E, ROLE_F and ROLE_B. What if I want to say that authority user:write implies in user:read, user:block, for example?

I'm thinking that when using authority(...) the role prefix should be ignored for the implied authorities too. It should be possible to do .authority("user:write").implies("user:read", "user:delete"). What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My mindset when writing this was that

  • the authority is a "label" that is provided externally provided (via authentication).
  • the role is a "label" that is internal to use to actually give access to things.

So from your example assuming the 'user:...' are externally provided authorities I see this as a possible way of doing this:

.authority("user:read")  .implies("USER_READ") 
.authority("user:write") .implies("USER_WRITE")
.authority("user:delete").implies("USER_DELETE")

.authority("TeamXYZMembers").implies("USER_READ", "USER_WRITE")

.role("USER_WRITE")      .implies("USER_READ", "USER_DELETE")

This would make all members of "Team XYZ" have the roles USER_READ, USER_WRITE and USER_DELETE.

Primary question: Does this reasoning make sense? It is correct?
If this is correct; Perhaps it should be documented as such?
(if you want I'll put up a separate merge request with such documentation)

The way I normally structure this is that in an application I define roles being groups of users with the same set of tasks like "Administrator", "Manager", "User", etc. Those are then assigned to the user in an external IGA/IAM system (often based on department/job title/...).

Then via the authentication of the specific user; the application receives one or more of these "external roles" as the authorities via the authentication.

From there my application has something like this

.authority("User")         .implies("USER_READ") 
.authority("Manager")      .implies("USER_WRITE")
.authority("Administrator").implies("USER_DELETE", "SHOW_DASHBOARD")

.role("USER_WRITE")       .implies("USER_READ")
.role("USER_DELETE")      .implies("USER_WRITE")

As said; I you want I'm willing to make a first draft on documenting this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want to be able to imply an authority then something like this is needed:
.authority("user:write").impliesAuthority("user:read", "user:delete")

A separate function is needed because there is no way to make any distinction between authority or role because they are both just a string.

Also

  • I currently disagree with this approach given how I look at the authority (external) and role (internal).
  • It is not needed to get the desired effect as shown in the example I posted yesterday.

Signed-off-by: Niels Basjes <[email protected]>
@marcusdacoregio
Copy link
Contributor

Hi @nielsbasjes. I was talking to @jzheaux about this feature and we come to a conclusion that we should refrain from adding the .authority(...) method to the builder. The main reason is because that we can cause more confusion with methods with a not so predictable behavior, like we discussed here, which can easily lead to misconfiguration.

Our suggestion is to add a new public static Builder withNoRolePrefix() method. This is nice because the application is either always supplying fully-qualified authorities or always supplying roles, making it easier to infer at a glance when the code is read later on.

What do you think?

@nielsbasjes
Copy link
Contributor Author

Hi @nielsbasjes. I was talking to @jzheaux about this feature and we come to a conclusion that we should refrain from adding the .authority(...) method to the builder.

Unexpected but you're the committers here.

The main reason is because that we can cause more confusion with methods with a not so predictable behavior, like we discussed here, which can easily lead to misconfiguration.

Where do you see "not so predictable behavior"?

  • In .role("X") X is always a role (i.e. with prefix).
  • In .implies("X") X is always a role (i.e. with prefix).
  • In .authority("X") X is always an authority (i.e. without prefix).

From where I'm standing the confusion (which is there) stems mainly from the difference between authority and role being quite unclear (at least to me when reading the documentation) and being stored in the same structures with a hidden prefix to determine the difference.

My code and explanation (which as I said I'm willing to put into the documentation if so desired) make it clearer and reliably usable ... at least to me it does.

Our suggestion is to add a new public static Builder withNoRolePrefix() method. This is nice because the application is either always supplying fully-qualified authorities or always supplying roles, making it easier to infer at a glance when the code is read later on.

That would be a completely silly change because it is 100% the same as simply doing .withRolePrefix(""). I do not see that as a valid code improvement because that makes the distinction between authorities and roles even harder to understand.

In my applications I do not want an external authority to accidentally match a role ever; I have even created this last week to make that even stronger to ensure no problems if someone creates an externally provided authority "ROLE_X":

@Bean
fun grantedAuthorityDefaults(): GrantedAuthorityDefaults = GrantedAuthorityDefaults(UUID.randomUUID().toString())

What do you think?

I think it would really help if you document what you see as the distinction between authority and role because from this discussion it seems you have a totally different idea than I have.

@marcusdacoregio
Copy link
Contributor

marcusdacoregio commented Jun 27, 2024

I don't think I clearly follow the following statement:

My mindset when writing this was that

  • the authority is a "label" that is provided externally provided (via authentication).
  • the role is a "label" that is internal to use to actually give access to things.

In my opinion, this is probably mostly related to your business needs than how the framework sees it. Both authorities and roles are the same thing, sometimes it makes sense to group users into roles and sometimes you need a more granular control so you can opt-in for authorities. But that is just a matter of naming, when performing any authorization logic the framework does not differ whether it is an authority or a role.

That would be a completely silly change because it is 100% the same as simply doing .withRolePrefix(""). I do not see that as a valid code improvement because that makes the distinction between authorities and roles even harder to understand.

That's the point, there is no difference between those two in general. The example that you gave earlier from my point of view should have a different arrangement. Roles are often used to group authorities and simplify access control logic:

RoleHierarchyImpl.withDefaultRolePrefix()
  .role("USER_WRITE")        .implies("USER_READ")
  .role("USER_DELETE")       .implies("USER_WRITE")
  .authority("User")         .implies("USER_READ") 
  .authority("Manager")      .implies("USER_WRITE")
  .authority("Administrator").implies("USER_DELETE", "SHOW_DASHBOARD")

should be:

RoleHierarchyImpl.withNoRolePrefix()
  .role("ROLE_USER")             .implies("USER_READ")
  .role("ROLE_MANAGER")          .implies("USER_WRITE")
  .role("ROLE_ADMINISTRATOR")    .implies("USER_DELETE", "SHOW_DASHBOARD")
  .role("USER_WRITE")       .implies("USER_READ")
  .role("USER_DELETE")      .implies("USER_WRITE")

In the example above, there is three roles and based on the role that the user has authorities are given to them. It sounds like it's the other way around than your understanding, but authorities/roles can be given to users directly, with no hard rule on whether it should be external/internal. This is also (briefly) mentioned in the documentation.

I think it would really help if you document what you see as the distinction between authority and role because from this discussion it seems you have a totally different idea than I have.

Absolutely, we can include in the reference docs some guidance on the difference between those two.

I apologize for the back and forth on this, but since this is aimed for 6.4 we have a bit of time and we should take advantage of it to try to refine as much as possible.

@marcusdacoregio marcusdacoregio modified the milestones: 6.4.0-M1, 6.4.0-M2 Jul 15, 2024
@nielsbasjes
Copy link
Contributor Author

In my opinion, this is probably mostly related to your business needs than how the framework sees it.

Yes, so that seems to be the big problem for me here; The framework has two flags that are technically identical and also do not have any logical distinction between them.
So duplicate APIs that do the same, have the same implementation and only differ because one prefixes everything with a prefix.

That makes the current situation very confusing for me.

I'm confronted with authorities which are of external origin (i.e. usergroups from the IAM/IGA solution) which I need to handle in an as clean as possible way within the application.

My need is to map these external values (without any prefix; stored as authorities) into Roles in a clean way, that is what I'm trying to achieve here.

Key question: What would you like to do with this? How do you see this? I'm willing to change it as long as I understand how you want it.

For now: I have taken the code I put here as a starting point and created a company internal variant that meets our business needs: Mapping authorities and roles into new roles.

Note: I had to copy some of the code (instead of inherit) because parts of it were private instead of protected (just making the constructor with parameters protected would have fixed it for me).

@marcusdacoregio marcusdacoregio removed this from the 6.4.0-M2 milestone Aug 7, 2024
@marcusdacoregio
Copy link
Contributor

Hi @nielsbasjes. The reference docs have a good explanation about authorities and where the ROLE_ prefix comes from.

With that in mind, I still think that adding that new method to the builder will cause some confusion, and therefore, I don't think we should merge it as is without more feedback from the community. For such scenarios, users can always use the constructor that accepts a role hierarchy as a String.

I'd still love a PR to fix #15264 (comment).

With that said, I'll close this as declined for now, but we can always reopen it as soon as we get more community feedback.

@marcusdacoregio marcusdacoregio added the status: declined A suggestion or change that we don't feel we should currently apply label Aug 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core An issue in spring-security-core status: declined A suggestion or change that we don't feel we should currently apply type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants