Skip to content
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

Support injection of the @BeanParam parameters into a custom permission referenced in the @PermissionAllowed security annotation #43353

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

michalvavrik
Copy link
Member

@michalvavrik michalvavrik commented Sep 17, 2024

This PR allows to pass into custom java.security.Permission only parameters you need from a @BeanParam. I can imagine you have different types of @BeanParams that share a header or query param etc. that you want to pass. It's alternative, but not only option. Instead of this, users could introduce level of abstraction (shared interface) they will inject into the java.security.Permission instead.

IMO what this can be convenient in very simple scenarios, like when you want to pass beanParam.field into your custom java.security.Permission.

Permission constructor parameters are newly matched automatically based on secured method parameter names.

@quarkus-bot quarkus-bot bot added area/docstyle issues related for manual docstyle review area/documentation area/rest area/security labels Sep 17, 2024
Copy link

github-actions bot commented Sep 18, 2024

🎊 PR Preview 79099ce has been successfully built and deployed to https://quarkus-pr-main-43353-preview.surge.sh/version/main/guides/

  • Images of blog posts older than 3 months are not available.
  • Newsletters older than 3 months are not available.

@quarkus-bot

This comment has been minimized.

@quarkus-bot

This comment has been minimized.

@geoand geoand requested a review from FroMage September 18, 2024 05:45
@michalvavrik
Copy link
Member Author

This doesn't have support, so let's close this. Thanks to reviewers that spend time on this. I appreciate it.

@quarkus-bot quarkus-bot bot added the triage/invalid This doesn't seem right label Sep 18, 2024
@sberyozkin
Copy link
Member

@michalvavrik Michal, you don't have to close just because I thought it was unnecessary, let me re-open for a bit longer, in case someone else would like to comment. I actually came here with another comment but it was already closed, so let me add it

@sberyozkin sberyozkin reopened this Sep 18, 2024
@quarkus-bot quarkus-bot bot removed the triage/invalid This doesn't seem right label Sep 18, 2024
@sberyozkin
Copy link
Member

@michalvavrik I wonder can we avoid typing bean.paramName in @PermissionAllowed and instead check if the custom permission constructor has matching names and add those bean param named automatically to @PermissionAllowed ?

@michalvavrik
Copy link
Member Author

@michalvavrik Michal, you don't have to close just because I thought it was unnecessary, let me re-open for a bit longer, in case someone else would like to comment.

I am not defeatist, I just don't have better (new) arguments :-) Let's hear from @FroMage if he manages to find a time later, I may have misunderstood this comment #43231 (comment).

I actually came here with another comment but it was already closed, so let me add it

I am ready to provide examples and answers why I implemented this. we can always close this later in case there is not an agreement, I am not worried.

@michalvavrik
Copy link
Member Author

@michalvavrik I wonder can we avoid typing bean.paramName in @PermissionAllowed and instead check if the custom permission constructor has matching names and add those bean param named automatically to @PermissionAllowed ?

We can, and it is already done for not nested params (when there is no dot). I didn't implement it for cases where there is a dot. In general, only reason for params annotation attribute is there can be ambiguity (like nested fields with a same name). While bean.paramName makes it unambiguious.

@sberyozkin
Copy link
Member

@michalvavrik, yeah sure, the last comment in the issue made it clearer for me, so this PR is alive :-).

So what do you think about this case: it is a BeanParam, but the permission constructor does not accept it, so try to deduce required bean param names ?

@michalvavrik
Copy link
Member Author

michalvavrik commented Sep 18, 2024

So what do you think about this case: it is a BeanParam, but the permission constructor does not accept it, so try to deduce required bean param names ?

I think that's a right way to do. I can implement it. I can also rewrite docs to accent simple auto detected cases. Is it acceptable to keep what there is now, like beanParam.nested? It is useful for ambiguous cases, to explicitly declare what you want to map.

@quarkus-bot

This comment has been minimized.

@sberyozkin
Copy link
Member

Sure, I guess it is a small amount of code to support beanName.paramName expressions, but indeed, it is worth highlighting how nicely it can be auto-mapped, if it proves possible to implement

@michalvavrik
Copy link
Member Author

Sure, I guess it is a small amount of code to support beanName.paramName expressions, but indeed, it is worth highlighting how nicely it can be auto-mapped, if it proves possible to implement

I didn't realize it at first, but how it works ATM is explained here https://github.com/quarkusio/quarkus-security/blob/main/src/main/java/io/quarkus/security/PermissionsAllowed.java#L68C10-L68C11 TL;DR; autodetected are parameters based on data type. We can also add autodetection based on construct <-> secured method parameter name matches, I can implement that, but it can't be done by default otherwise it will be a breaking change. I'll have deeper look in next 2 days and see what I can do to make it easy.

Copy link
Member

@FroMage FroMage left a comment

Choose a reason for hiding this comment

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

LGTM, but docs need a fix.

@Override
public boolean implies(Permission permission) {
if (permission instanceof MyPermission myPermission) {
return myPermission.authorization != null && "query1".equals(myPermission.queryParam);
Copy link
Member

Choose a reason for hiding this comment

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

Once again a permission where this. members are never used. I don't understand this model.

Copy link
Member Author

Choose a reason for hiding this comment

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

The model comes from the https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/security/Permission.html#implies(java.security.Permission). In Quarkus terms, the identity has permission checker that can check required permissions. Here it is:

So technically, you only need one required permission and one checker. You can do requiredPermission.implies(null) or whatever, but it feels very hacky, so I always expect users will declare both requiredPermission and possessedPermission and do: possessedPermission.implies(requiredPermission).

I expect this can be simplified with your @RequiredPermission or @PermissionChecker or whatever later.

BTW this particular MyPermission is here to test that bean parameters were passed correctly, I don't think it needs to use this for that purpose. I wouldn't write this to the docs.

Copy link
Member

Choose a reason for hiding this comment

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

I can understand implies as it's documented, which is about figuring out if, given an already granted permission, another one is implied by it. For example, given that the current user has the admin permission, is the read permission implied? Probably, yes.

But this is not what we're describing here with MyPermission. I see no relation.

SecurityIdentity.checkPermission(Permission) I also understand: does the current user have the right to do X? Fine. That makes sense.

But again, this does not look like what MyPermission.implies is doing. Is this something related to the current PR, or the general security model in Quarkus?

Copy link
Member Author

@michalvavrik michalvavrik Sep 30, 2024

Choose a reason for hiding this comment

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

But this is not what we're describing here with MyPermission. I see no relation.

ad describing: it's a test, not docs; but sure, I'll answer

SecurityIdentity.checkPermission(Permission) I also understand: does the current user have the right to do X? Fine. That makes sense.

When you specify @PermissionsAllowed(permission=CustomPermission) then CustomPermission instance is created and passed to the SecurityIdentity.checkPermission(Permission) because it is fact checkPermission(requiredPermission).

It depends on how user writes SecurityIdentityAugmentor whether this is used. I am using one and the same permission in the all tests for making it easy, but you can also do this (meta code ...):

@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
    return QuarkusSecurityIdentity.builder(identity).addPermissionChecker(requiredPermission -> {
        if (requiredPermission.implies(new StringPermission("read"))) {
            return Uni.createFrom().item(Boolean.TRUE);
        }
        return Uni.createFrom().item(Boolean.FALSE);
    }).build();
}

and this augmentor part can be simplified with #43238.

I cannot simplify this model in this PR.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, so wait, users have to implement a SecurityIdentityAugmentor to make this work?

Copy link
Member Author

@michalvavrik michalvavrik Sep 30, 2024

Choose a reason for hiding this comment

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

It is one option. There are other ways to add permission checker on your security identity. For example we map token claims as StringPermission. Or you can remap SecurityIdentity roles like this:

map identity role 'user' to String permission 'read'
quarkus.http.auth.policy.role-policy2.permissions.user=read  
quarkus.http.auth.permission.roles2.paths=*
quarkus.http.auth.permission.roles2.policy=role-policy2

Which gives you var possessed = new StringPermission("read")

Problem with this is that we add permission check like possessedPermisison.implies(requiredPermission). It felt right, but I suppose for purpose of this particular situation, requiredPermisison.implies(possessedPermission) is equivalent. Though it is not from POV of formal logic.

So ATM you do need augmentor, but this part could be changed in this PR if it is requested.

Copy link
Member Author

Choose a reason for hiding this comment

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

It would be yet another breaking change though.

Copy link
Member Author

Choose a reason for hiding this comment

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

And if you wonder, if user doesn't specify any permission checker but @PermissionsAllowed is applied, the QuarkusSecurityIdentity responds with false by default

so you don't have the permission unless you posses at least one that complies it. I think we shouldn't change it in this PR. I'll continue updating the docs with the augmentor.

@quarkus-bot

This comment has been minimized.

@FroMage
Copy link
Member

FroMage commented Sep 19, 2024

Sure, I guess it is a small amount of code to support beanName.paramName expressions, but indeed, it is worth highlighting how nicely it can be auto-mapped, if it proves possible to implement

I didn't realize it at first, but how it works ATM is explained here https://github.com/quarkusio/quarkus-security/blob/main/src/main/java/io/quarkus/security/PermissionsAllowed.java#L68C10-L68C11 TL;DR; autodetected are parameters based on data type. We can also add autodetection based on construct <-> secured method parameter name matches, I can implement that, but it can't be done by default otherwise it will be a breaking change. I'll have deeper look in next 2 days and see what I can do to make it easy.

Oh, I did not realise you did the matching based on type by default. Given a lot of form or path parameters are typed String or Long this will lead to duplicates fairly frequently. Isn't it more intuitive and useful to do the matching based on the parameter names (constructor param names matching endpoint method parameter names)?

Then indeed, we could also match member names of bean params when there's no ambiguity, or resort to explicit parameter names with dot prefixes (such as this PR supports) when users want to resolve any ambiguity.

@michalvavrik
Copy link
Member Author

michalvavrik commented Sep 19, 2024

Oh, I did not realise you did the matching based on type by default. Given a lot of form or path parameters are typed String or Long this will lead to duplicates fairly frequently. Isn't it more intuitive and useful to do the matching based on the parameter names (constructor param names matching endpoint method parameter names)?

Then indeed, we could also match member names of bean params when there's no ambiguity, or resort to explicit parameter names with dot prefixes (such as this PR supports) when users want to resolve any ambiguity.

Yes, it would be better. I don't remember why I decided to use param types by default. Only positive thing about it is that it doesn't depend on concrete parameter names in permission/secured method. I think it should be changed to param name matching by default as for what I have seen, it avoid typing any PermissionsAllowed#params almost always. Original behavior (matching based on type) should be possible to activate by constant on PermissionsAllowed#autodetectUsingTypes that could be assigned to PermissionsAllowed#params.

It will be breaking change but I think @sberyozkin also suggested it in this PR, so I'll propose it in Quarkus Security API project.

@sberyozkin
Copy link
Member

My proposal is just do the name based matching as proposed by Steph @FroMage without worrying about supporting a type only match or thinking we may be breaking something, it can't be properly supported without the name match.
If we do not explicitly suggest in the docs that users can have same parameters having different names in JAX-RS methods and custom permission constructors, then we should just fix as proposed by Steph and merge

@michalvavrik michalvavrik force-pushed the feature/permissions-allowed-bean-param branch from 48e28a4 to 05fa0bb Compare September 29, 2024 15:30
@michalvavrik
Copy link
Member Author

Hello @FroMage @sberyozkin ,

I have pushed updated version. It contains everything I am aware of that we agreed on. I tried to address @FroMage comments regarding the docs and hope that the new documentation example is more meaningful. We can iterate over docs as much as necessary.

Thanks

@quarkus-bot

This comment has been minimized.

@quarkus-bot

This comment has been minimized.

public class SimpleResource {
@PermissionsAllowed(value = "read", permission = BeanParamPermission.class,
params = "beanParam.securityContext.userPrincipal.name") <1>
Copy link
Member

Choose a reason for hiding this comment

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

It looks like you are supporting unlimited unwrapping of BeanParam params, but does that make it too complex ? I thought the plan was only to allow to refer to individual BeanParam params.

Copy link
Member Author

Choose a reason for hiding this comment

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

Implementation wise, it was just difference few recursive calls, so I thought it was useful. I can drop it either from documentation or from implementation itself. Personally, I don't see an appeal to give ability to reference beanParam.query and not beanParam.nestedBeanParam.query. For example here

I checked out tests, saw how @ BeanParam are tested and tried to support that. My personal opinion is that it is cool that I can just reference principal directly.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah agreed. But getting awfull close to EL 😱

private final String userName;
private final String queryParam;
public BeanParamPermission(String permissionName, String customAuthorizationHeader, String name, String query) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little bit confused. Oh, I see, you have unwrapped the user name from the security context.
Hmm, it is kind of nice and neat, but very complex at the same time. I'm not sure we need to support such combinations, the security identity for example should be accessible directly from the permission class

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, it is kind of nice and neat, but very complex at the same time.

implementation wise, it is few recursive calls ( I won't check code atm, but I guess it was either one or two)

I'm not sure we need to support such combinations, the security identity for example should be accessible directly from the permission class

I will remove it if you insist, but first, I'd like to finish discussion about nested @BeanParams in the other comment thread because essentially, it's same ability.

it is kind of nice and neat

hope you mean it, because that's how I feel about it

Copy link
Member

Choose a reason for hiding this comment

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

I doubt typing a string beanParam.securityContext.userPrincipal.name is better than using Arc to get SecurityIdentity (as opposed to JAX-RS security context, which we recommend to bind to SecurityIdentity), it feels like we can pass on this one, for now at least

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure we need to support such combinations, the security identity for example should be accessible directly from the permission class

you can do that, but some people prefer JAX-RS specific concepts like here

Copy link
Member Author

Choose a reason for hiding this comment

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

I doubt typing a string beanParam.securityContext.userPrincipal.name is better than using Arc to get SecurityIdentity (as opposed to JAX-RS security context, which we recommend to bind to SecurityIdentity), it feels like we can pass on this one, for now at least

I'll remove it from docs example. Please comment if you want more.

Copy link
Member Author

Choose a reason for hiding this comment

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

@sberyozkin will need bit more input here, do you have alternative proposals? I can provide replace this beanParam.nestedBeanParam.query but I think you will have same argument. It's legit concern, but we shouldn't assume that users prefer your way. So let's suggest something and I'll do it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd keep it.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, keep it, I'd say

Copy link
Member

@FroMage FroMage left a comment

Choose a reason for hiding this comment

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

Look good, except minor language suggestions, and the unresolved question of how implies works which I find very confusing.

boolean usernameWhitelisted = isUserNameWhitelisted(that.userName);
boolean customAuthorizationMatches = checkCustomAuthorization(that.customAuthorization);
return permissionNameMatches && queryParamAllowedForPermissionName
&& usernameWhitelisted && customAuthorizationMatches;
Copy link
Member

Choose a reason for hiding this comment

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

I still find this model of permission super confusing. Apparently there are two instances. The first one (this, here) has fields that are not used. The second (that, here) have fields that we look at.

This is beyond my ability to understand.

Copy link
Member Author

@michalvavrik michalvavrik Sep 30, 2024

Choose a reason for hiding this comment

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

yes, I understand. I propose that I'll change this example to use augmentor that I drafter above in your another comment thread and this way, you know it will use this always. I'll rewrite it now.

Copy link
Member Author

Choose a reason for hiding this comment

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

replaced, now there is no that

@Override
public boolean implies(Permission permission) {
if (permission instanceof MyPermission myPermission) {
return myPermission.authorization != null && "query1".equals(myPermission.queryParam);
Copy link
Member

Choose a reason for hiding this comment

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

I can understand implies as it's documented, which is about figuring out if, given an already granted permission, another one is implied by it. For example, given that the current user has the admin permission, is the read permission implied? Probably, yes.

But this is not what we're describing here with MyPermission. I see no relation.

SecurityIdentity.checkPermission(Permission) I also understand: does the current user have the right to do X? Fine. That makes sense.

But again, this does not look like what MyPermission.implies is doing. Is this something related to the current PR, or the general security model in Quarkus?

@michalvavrik michalvavrik force-pushed the feature/permissions-allowed-bean-param branch from 05fa0bb to 03afb70 Compare September 30, 2024 13:22
@quarkus-bot
Copy link

quarkus-bot bot commented Sep 30, 2024

Status for workflow Quarkus CI

This is the status report for running Quarkus CI on commit 03afb70.

✅ The latest workflow run for the pull request has completed successfully.

It should be safe to merge provided you have a look at the other checks in the summary.

You can consult the Develocity build scans.

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.

Support @PermissionAllowed for @BeanParam parameter
4 participants