-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
base: main
Are you sure you want to change the base?
Conversation
🎊 PR Preview 79099ce has been successfully built and deployed to https://quarkus-pr-main-43353-preview.surge.sh/version/main/guides/
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
This doesn't have support, so let's close this. Thanks to reviewers that spend time on this. I appreciate it. |
@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 |
@michalvavrik I wonder can we avoid typing |
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 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. |
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 |
@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 |
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 |
This comment has been minimized.
This comment has been minimized.
Sure, I guess it is a small amount of code to support |
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. |
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.
LGTM, but docs need a fix.
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
@Override | ||
public boolean implies(Permission permission) { | ||
if (permission instanceof MyPermission myPermission) { | ||
return myPermission.authorization != null && "query1".equals(myPermission.queryParam); |
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.
Once again a permission where this.
members are never used. I don't understand this model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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 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?
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.
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.
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.
Oh, so wait, users have to implement a SecurityIdentityAugmentor
to make this work?
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 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.
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 would be yet another breaking change though.
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.
And if you wonder, if user doesn't specify any permission checker but @PermissionsAllowed
is applied, the QuarkusSecurityIdentity
responds with false by default
Line 84 in 1603d2c
return Uni.createFrom().item(false); |
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.
...curity/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java
Outdated
Show resolved
Hide resolved
.../test/java/io/quarkus/security/test/permissionsallowed/CustomPermissionWithMultipleArgs.java
Show resolved
Hide resolved
...curity/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/SecuredBean.java
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
Oh, I did not realise you did the matching based on type by default. Given a lot of form or path parameters are typed 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. |
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. |
48e28a4
to
05fa0bb
Compare
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 |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
public class SimpleResource { | ||
@PermissionsAllowed(value = "read", permission = BeanParamPermission.class, | ||
params = "beanParam.securityContext.userPrincipal.name") <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.
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.
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.
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
Line 71 in 96d8967
OtherBeanParamRecord obpr; |
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.
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.
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) { |
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'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
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.
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 @BeanParam
s 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
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 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
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'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
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 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.
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.
@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.
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'd keep it.
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.
Yeah, keep it, I'd say
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.
Look good, except minor language suggestions, and the unresolved question of how implies
works which I find very confusing.
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Outdated
Show resolved
Hide resolved
boolean usernameWhitelisted = isUserNameWhitelisted(that.userName); | ||
boolean customAuthorizationMatches = checkCustomAuthorization(that.customAuthorization); | ||
return permissionNameMatches && queryParamAllowedForPermissionName | ||
&& usernameWhitelisted && customAuthorizationMatches; |
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 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.
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.
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.
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.
replaced, now there is no that
...rc/test/java/io/quarkus/resteasy/reactive/server/test/security/OtherBeanParamPermission.java
Outdated
Show resolved
Hide resolved
@Override | ||
public boolean implies(Permission permission) { | ||
if (permission instanceof MyPermission myPermission) { | ||
return myPermission.authorization != null && "query1".equals(myPermission.queryParam); |
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 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?
05fa0bb
to
03afb70
Compare
Status for workflow
|
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@BeanParam
s 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 thejava.security.Permission
instead.IMO what this can be convenient in very simple scenarios, like when you want to pass
beanParam.field
into your customjava.security.Permission
.Permission constructor parameters are newly matched automatically based on secured method parameter names.