Skip to content

Commit

Permalink
Support @permissionchecker CDI bean methods
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Oct 13, 2024
1 parent 342e6f0 commit 903b9f0
Show file tree
Hide file tree
Showing 54 changed files with 3,000 additions and 269 deletions.
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@
<quarkus-spring-boot-api.version>3.2</quarkus-spring-boot-api.version>
<mockito.version>5.14.1</mockito.version>
<jna.version>5.8.0</jna.version><!-- should satisfy both testcontainers and mongodb -->
<quarkus-security.version>2.1.0</quarkus-security.version>
<quarkus-security.version>2.1.1-SNAPSHOT</quarkus-security.version>
<keycloak.version>25.0.6</keycloak.version>
<logstash-gelf.version>1.15.1</logstash-gelf.version>
<checker-qual.version>3.48.1</checker-qual.version>
Expand Down
124 changes: 91 additions & 33 deletions docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1055,8 +1055,83 @@ Because `MediaLibrary` is the `TvLibrary` class parent, a user with the `admin`

CAUTION: Annotation-based permissions do not work with custom xref:security-customization.adoc#jaxrs-security-context[Jakarta REST SecurityContexts] because there are no permissions in `jakarta.ws.rs.core.SecurityContext`.

[[permission-checker]]
==== Create permission checkers

Previous examples required that the `SecurityIdentity` possessed required permissions.
We added these permissions in the `SecurityIdentityAugmentor` and used role-to-permission mapping.
Another way to check permissions required by the `@PermissionsAllowed` security annotation is a permission checker.
The permission checkers are CDI bean methods annotated with a `@PermissionChecker`.
For example, a permission checker can be created like this:

[source,java]
----
package org.acme.security.rest.resource;
import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.jboss.resteasy.reactive.RestQuery;
@Path("/hello")
public class HelloResource {
@PermissionsAllowed("say-hello") <1>
@GET
public String sayHello(@RestQuery String toWhom) {
return "Hello " + toWhom;
}
@PermissionChecker("say-hello") <2>
boolean canSayHello(SecurityIdentity identity) { <3>
var principalName = identity.getPrincipal().getName();
return "alice".equals(principalName);
}
}
----
<1> Permission required to access the `HelloResource#sayHello` is the `say-hello` permission.
<2> The `HelloResource#canSayHello` method authorize access to the `HelloResource#sayHello` endpoint.
<3> The `SecurityIdentity` instance can be injected into any permission checker method.

The permission checker above requires the `SecurityIdentity` instance to authorize the `sayHello` endpoint.
Instead of declaring the `say-hello` permission checker directly on the resource, you can declare it on any CDI bean.
Sometimes it might be useful to check secured method parameters.
For example, a permission checker that accepts the `toWhom` argument can be created like this:

[source,java]
----
package org.acme.security.rest.resource;
import io.quarkus.security.PermissionChecker;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class HelloPermissionChecker {
@Inject
RoutingContext routingContext; <1>
@PermissionChecker("say-hello")
boolean canSayHello(String toWhom) {
// replace following lines with your business logic
boolean sayHelloToBob = "bob".equals(toWhom);
return !sayHelloToBob;
}
}
----
<1> You can inject another CDI beans and use them inside a checker method.

TIP: Permission checks are run on event loops whenever possible.
Annotate a permission checker method with the `io.smallrye.common.annotation.Blocking` annotation if you want to run the check on a worker thread.

[[permission-meta-annotation]]
=== Create permission meta-annotations
==== Create permission meta-annotations

`@PermissionsAllowed` can also be used in meta-annotations.
For example, a new `@CanWrite` security annotation can be created like this:
Expand All @@ -1080,7 +1155,7 @@ public @interface CanWrite {
<1> Any method or class annotated with the `@CanWrite` annotation is secured with this `@PermissionsAllowed` annotation instance.

[[permission-bean-params]]
=== Pass `@BeanParam` parameters into a custom permission
==== Pass `@BeanParam` parameters into a custom permission

Quarkus can map fields of a secured method parameters to a custom permission constructor parameters.
You can use this feature to pass `jakarta.ws.rs.BeanParam` parameters into your custom permission.
Expand All @@ -1096,20 +1171,19 @@ import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/hello")
public class SimpleResource {
public class HelloResource {
@PermissionsAllowed(value = "say:hello", permission = BeanParamPermission.class,
params = "beanParam.securityContext.userPrincipal.name") <1>
@PermissionsAllowed(value = "say-hello", params = "beanParam.securityContext.userPrincipal.name") <1>
@GET
public String sayHello(@BeanParam SimpleBeanParam beanParam) {
return "Hello from " + beanParam.uriInfo.getPath();
}
}
----
<1> The `params` annotation attribute specifies that user principal name should be passed to the `BeanParamPermission` constructor.
Other `BeanParamPermission` constructor parameters like `customAuthorizationHeader` and `query` are matched automatically.
Quarkus identifies the `BeanParamPermission` constructor parameters among `beanParam` fields and their public accessors.
<1> The `params` annotation attribute specifies that user principal name should be passed to the `BeanParamPermissionChecker#canSayHello` method.
Other `BeanParamPermissionChecker#canSayHello` method parameters like `customAuthorizationHeader` and `query` are matched automatically.
Quarkus identifies the `BeanParamPermissionChecker#canSayHello` method parameters among `beanParam` fields and their public accessors.
To avoid ambiguous resolution, automatic detection only works for the `beanParam` fields.
For that reason, we had to specify path to the user principal name explicitly.

Expand Down Expand Up @@ -1155,47 +1229,31 @@ public class SimpleBeanParam {
<3> The `customAuthorizationHeader` field is not public, therefore Quarkus access this field with the `customAuthorizationHeader` accessor.
That is particularly useful with Java records, where generated accessors are not prefixed with `get`.

Here is an example of the `BeanParamPermission` permission that checks user principal, custom header and query parameter:
Here is an example of a `@PermissionChecker` method that checks `say-hello` permission based on a user principal, custom header and query parameter:

[source,java]
----
package org.acme.security.permission;
import java.security.Permission;
public class BeanParamPermission extends Permission {
private final String actions;
public BeanParamPermission(String permissionName, String customAuthorizationHeader, String name, String query) {
super(permissionName);
this.actions = computeActions(customAuthorizationHeader, name, query);
}
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.security.PermissionChecker;
@Override
public boolean implies(Permission p) {
boolean nameMatches = getName().equals(p.getName());
boolean actionMatches = actions.equals(p.getActions());
return nameMatches && actionMatches;
}
@ApplicationScoped
public class BeanParamPermissionChecker {
private static String computeActions(String customAuthorizationHeader, String name, String query) {
@PermissionChecker("say-hello")
boolean canSayHello(String customAuthorizationHeader, String name, String query) {
boolean queryParamAllowedForPermissionName = checkQueryParams(query);
boolean usernameWhitelisted = isUserNameWhitelisted(name);
boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorizationHeader);
var isAuthorized = queryParamAllowedForPermissionName && usernameWhitelisted && customAuthorizationMatches;
if (isAuthorized) {
return "hello";
} else {
return "goodbye";
}
return queryParamAllowedForPermissionName && usernameWhitelisted && customAuthorizationMatches;
}
...
}
----

NOTE: You can pass `@BeanParam` directly into a custom permission constructor and access its fields programmatically in the constructor instead.
NOTE: You can pass `@BeanParam` directly into a `@PermissionChecker` method and access its fields programmatically.
Ability to reference `@BeanParam` fields with the `@PermissionsAllowed#params` attribute is useful when you have multiple differently structured `@BeanParam` classes.

== References
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public abstract class AbstractPermissionsAllowedTestCase {
@BeforeAll
public static void setupUsers() {
TestIdentityController.resetRoles()
.add("admin", "admin")
.add("admin", "admin", "admin")
.add("user", "user")
.add("viewer", "viewer");
}
Expand Down Expand Up @@ -190,6 +190,40 @@ public void testCustomPermissionWithAdditionalArgs_MetaAnnotation() {
.statusCode(403);
}

@Test
public void testPermissionCheckerDeclaredInsideResource() {
reqPermChecker("checker-inside-resource", null, false).statusCode(401);
reqPermChecker("checker-inside-resource", "user", false).statusCode(403);
reqPermChecker("checker-inside-resource", "admin", false).statusCode(200).body(Matchers.is("admin"));
}

@Test
public void testPermissionRunOnCorrectThread() {
testPermissionRunOnCorrectThread("worker-thread");
testPermissionRunOnCorrectThread("io-thread");
testPermissionRunOnCorrectThread("io-thread-uni");
testPermissionRunOnCorrectThread("worker-thread-method-args");
testPermissionRunOnCorrectThread("io-thread-method-args");
}

private static void testPermissionRunOnCorrectThread(String subPath) {
reqPermChecker(subPath, "user", false).statusCode(403);
reqPermChecker(subPath, "admin", false).statusCode(200).body(Matchers.is("admin"));
reqPermChecker(subPath, "admin", true).statusCode(403);
}

private static ValidatableResponse reqPermChecker(String path, String user, boolean addFailHeader) {
var req = RestAssured.given();
if (user != null) {
req.auth().basic(user, user);
}
if (addFailHeader) {
// this "fail" header is about checking that we have RoutingContext available
req.header("fail", "true");
}
return req.get("/permission-checkers/" + path).then();
}

private static ValidatableResponse reqAutodetectedExtraArgs(String user, String place) {
return RestAssured.given()
.auth().basic(user, user)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.resteasy.reactive.server.test.security;

import jakarta.enterprise.context.RequestScoped;

import io.quarkus.security.PermissionChecker;

@RequestScoped
public class BeanParamPermissionChecker {

@PermissionChecker("say-hello")
boolean canSayHello(String customAuthorizationHeader, String name, String query) {
boolean queryParamAllowedForPermissionName = checkQueryParams(query);
boolean usernameWhitelisted = isUserNameWhitelisted(name);
boolean customAuthorizationMatches = checkCustomAuthorization(customAuthorizationHeader);
return queryParamAllowedForPermissionName && usernameWhitelisted && customAuthorizationMatches;
}

static boolean checkCustomAuthorization(String customAuthorization) {
return "customAuthorization".equals(customAuthorization);
}

static boolean isUserNameWhitelisted(String userName) {
return "admin".equals(userName);
}

static boolean checkQueryParams(String queryParam) {
return "myQueryParam".equals(queryParam);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public class LazyAuthPermissionsAllowedTestCase extends AbstractPermissionsAllow
.addClasses(PermissionsAllowedResource.class, TestIdentityProvider.class, TestIdentityController.class,
NonBlockingPermissionsAllowedResource.class, CustomPermission.class,
PermissionsIdentityAugmentor.class, CustomPermissionWithExtraArgs.class,
StringPermissionsAllowedMetaAnnotation.class, CreateOrUpdate.class)
StringPermissionsAllowedMetaAnnotation.class, CreateOrUpdate.class, PermissionCheckers.class,
PermissionCheckersResource.class)
.addAsResource(new StringAsset("quarkus.http.auth.proactive=false\n"),
"application.properties"));

Expand Down

This file was deleted.

Loading

0 comments on commit 903b9f0

Please sign in to comment.