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

Allow to inject JAX-RS ResourceInfo into custom HTTP Security Policy #39917

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.resteasy.test.security;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ResourceInfo;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomHttpSecurityPolicy implements HttpSecurityPolicy {

@Inject
ResourceInfo resourceInfo;

@Override
public Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> identity,
AuthorizationRequestContext requestContext) {
if ("CustomPolicyResource".equals(resourceInfo.getResourceClass().getSimpleName())
&& "isUserAdmin".equals(resourceInfo.getResourceMethod().getName())) {
return identity.onItem().ifNotNull().transform(i -> {
if (i.hasRole("user")) {
return new CheckResult(true, QuarkusSecurityIdentity.builder(i).addRole("admin").build());
}
return CheckResult.PERMIT;
});
}
return Uni.createFrom().item(CheckResult.PERMIT);
}

@Override
public String name() {
return "custom";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package io.quarkus.resteasy.test.security;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class CustomHttpSecurityWithJaxRsSecurityContextTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(CustomPolicyResource.class, TestIdentityProvider.class,
TestIdentityController.class, CustomHttpSecurityPolicy.class)
.addAsResource(new StringAsset("""
quarkus.http.auth.permission.custom-policy-1.paths=/custom-policy/is-admin
quarkus.http.auth.permission.custom-policy-1.policy=custom
quarkus.http.auth.permission.custom-policy-1.applies-to=JAXRS
"""),
"application.properties"));

@BeforeAll
public static void setupUsers() {
TestIdentityController.resetRoles()
.add("test", "test", "test")
.add("user", "user", "user");
}

@Test
public void testAugmentedIdentityInSecurityContext() {
// test that custom HTTP Security Policy is applied, it added 'admin' role to the 'user'
// and this new role is present in the JAX-RS SecurityContext
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.get("/custom-policy/is-admin")
.then()
.statusCode(200)
.body(Matchers.is("true"));
RestAssured
.given()
.auth().preemptive().basic("test", "test")
.get("/custom-policy/is-admin")
.then()
.statusCode(200)
.body(Matchers.is("false"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.resteasy.test.security;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("custom-policy")
public class CustomPolicyResource {

@Path("is-admin")
@GET
public boolean isUserAdmin(@Context SecurityContext context) {
return context.isUserInRole("admin");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.List;
import java.util.Optional;

import jakarta.ws.rs.container.ResourceInfo;

import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
Expand All @@ -27,4 +29,15 @@ public interface JaxRsSecurityConfig {
* annotation.
*/
Optional<List<String>> defaultRolesAllowed();

/**
* Allows to run custom HTTP Security Policies after your JAX-RS resource method has been matched
* with incoming HTTP request. Enable this option if you have custom HTTP Security Policy where you need
* to inject {@link ResourceInfo} and perform authorization with the knowledge of the invoked resource method.
* Please review documentation of the `quarkus.http.auth.permission."permissions".applies-to`
* configuration property before you enable this option.
*/
@WithName("enable-jaxrs-http-security-policies")
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
@WithDefault("false")
boolean enableJaxRsHttpSecurityPolicies();
}
Original file line number Diff line number Diff line change
Expand Up @@ -1504,8 +1504,9 @@ MethodScannerBuildItem integrateEagerSecurity(Capabilities capabilities, Combine

final boolean applySecurityInterceptors = !eagerSecurityInterceptors.isEmpty();
final var interceptedMethods = applySecurityInterceptors ? collectInterceptedMethods(eagerSecurityInterceptors) : null;
final boolean denyJaxRs = securityConfig.denyJaxRs();
final boolean hasDefaultJaxRsRolesAllowed = !securityConfig.defaultRolesAllowed().orElse(List.of()).isEmpty();
final boolean addEagerSecurityHandlerToEveryMethod = securityConfig.denyJaxRs()
|| !securityConfig.defaultRolesAllowed().orElse(List.of()).isEmpty()
|| securityConfig.enableJaxRsHttpSecurityPolicies();
var index = indexBuildItem.getComputingIndex();
return new MethodScannerBuildItem(new MethodScanner() {
@Override
Expand All @@ -1519,7 +1520,7 @@ public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndp
return List.of(EagerSecurityInterceptorHandler.Customizer.newInstance(),
EagerSecurityHandler.Customizer.newInstance());
} else {
if (denyJaxRs || hasDefaultJaxRsRolesAllowed) {
if (addEagerSecurityHandlerToEveryMethod) {
return List.of(EagerSecurityHandler.Customizer.newInstance());
} else {
return Objects
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package io.quarkus.resteasy.reactive.server.test.security;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ResourceInfo;

import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomHttpSecurityPolicy implements HttpSecurityPolicy {

@Inject
ResourceInfo resourceInfo;

@Override
public Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> identity,
AuthorizationRequestContext requestContext) {
if ("CustomPolicyResource".equals(resourceInfo.getResourceClass().getSimpleName())
&& "isUserAdmin".equals(resourceInfo.getResourceMethod().getName())) {
return identity.onItem().ifNotNull().transform(i -> {
if (i.hasRole("user")) {
return new CheckResult(true, QuarkusSecurityIdentity.builder(i).addRole("admin").build());
}
return CheckResult.PERMIT;
});
}
return Uni.createFrom().item(CheckResult.PERMIT);
}

@Override
public String name() {
return "custom";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.quarkus.resteasy.reactive.server.test.security;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.security.test.utils.TestIdentityController;
import io.quarkus.security.test.utils.TestIdentityProvider;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class CustomHttpSecurityWithJaxRsSecurityContextTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(CustomPolicyResource.class, TestIdentityProvider.class,
TestIdentityController.class, CustomHttpSecurityPolicy.class)
.addAsResource(new StringAsset("""
quarkus.http.auth.permission.custom-policy-1.paths=/custom-policy/is-admin
quarkus.http.auth.permission.custom-policy-1.policy=custom
quarkus.http.auth.permission.custom-policy-1.applies-to=JAXRS
quarkus.security.jaxrs.enable-jaxrs-http-security-policies=true
"""),
"application.properties"));

@BeforeAll
public static void setupUsers() {
TestIdentityController.resetRoles()
.add("test", "test", "test")
.add("user", "user", "user");
}

@Test
public void testAugmentedIdentityInSecurityContext() {
// test that custom HTTP Security Policy is applied, it added 'admin' role to the 'user'
// and this new role is present in the JAX-RS SecurityContext
RestAssured
.given()
.auth().preemptive().basic("user", "user")
.get("/custom-policy/is-admin")
.then()
.statusCode(200)
.body(Matchers.is("true"));
RestAssured
.given()
.auth().preemptive().basic("test", "test")
.get("/custom-policy/is-admin")
.then()
.statusCode(200)
.body(Matchers.is("false"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.resteasy.reactive.server.test.security;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.SecurityContext;

@Path("custom-policy")
public class CustomPolicyResource {

@Path("is-admin")
@GET
public boolean isUserAdmin(SecurityContext context) {
return context.isUserInRole("admin");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jboss.resteasy.reactive.common.model.ResourceClass;
import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
Expand Down Expand Up @@ -56,7 +57,12 @@ public void handle(ResteasyReactiveRequestContext requestContext) throws Excepti
return;
} else {
// only permission check
check = EagerSecurityContext.instance.getPermissionCheck(requestContext, null);
check = Uni.createFrom().deferred(new Supplier<Uni<?>>() {
@Override
public Uni<?> get() {
return EagerSecurityContext.instance.getPermissionCheck(requestContext, null);
}
});
}
} else {
if (EagerSecurityContext.instance.doNotRunPermissionSecurityCheck) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,38 @@ public enum AppliesTo {
* mechanism</li>
* <li>`io.quarkus.oidc.AuthorizationCodeFlow` which selects the OpenID Connect Code authentication mechanism</li>
* </ul>
* <p>
* You also need to set this option for custom HTTP Security Policies that needs to be run after your JAX-RS
* resource method has been matched with incoming HTTP request like in the example below.
* <p>
* Example of `the application.properties`:
* <code>
* quarkus.http.auth.permission.custom-policy-1.paths=/hello
* quarkus.http.auth.permission.custom-policy-1.policy=custom
* quarkus.http.auth.permission.custom-policy-1.applies-to=JAXRS
* # uncomment next line when you use the custom policy with Quarkus REST
* # quarkus.security.jaxrs.enable-jaxrs-http-security-policies=true
* </code>
* Example of the custom HTTP Security Policy invoked for matched `/hello` path.
* <code>
* &#064;ApplicationScoped
* public class CustomHttpSecurityPolicy implements HttpSecurityPolicy {
*
* &#064;Inject
* ResourceInfo resourceInfo;
*
* &#064;Override
* public Uni<CheckResult> checkPermission(RoutingContext request, Uni<SecurityIdentity> identity,
* AuthorizationRequestContext requestContext) {
* // use resourceInfo and authorize the HTTP request
* }
*
* &#064;Override
* public String name() {
* return "custom";
* }
* }
* </code>
*/
JAXRS
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public class AbstractPathMatchingHttpSecurityPolicy {
public AbstractPathMatchingHttpSecurityPolicy(Map<String, PolicyMappingConfig> permissions,
Map<String, PolicyConfig> rolePolicy, String rootPath, Instance<HttpSecurityPolicy> installedPolicies,
PolicyMappingConfig.AppliesTo appliesTo) {
boolean hasNoPermissions = permissions.isEmpty();
boolean hasNoPermissions = true;
var namedHttpSecurityPolicies = toNamedHttpSecPolicies(rolePolicy, installedPolicies);
List<ImmutablePathMatcher<List<HttpMatcher>>> sharedPermsMatchers = new ArrayList<>();
final var builder = ImmutablePathMatcher.<List<HttpMatcher>> builder().handlerAccumulator(List::addAll)
Expand Down
Loading