Skip to content

Commit

Permalink
Merge pull request #39920 from michalvavrik/feature/refactor-jax-rs-h…
Browse files Browse the repository at this point in the history
…ttp-security-policies

Allow to inject JAX-RS ResourceInfo into custom HTTP Security Policy
  • Loading branch information
sberyozkin authored Apr 6, 2024
2 parents c24cfd7 + 0f0df56 commit 86ba00e
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 23 deletions.
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 @@ -1504,35 +1504,33 @@ 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 withDefaultSecurityCheck = securityConfig.denyJaxRs()
|| !securityConfig.defaultRolesAllowed().orElse(List.of()).isEmpty();
var index = indexBuildItem.getComputingIndex();
return new MethodScannerBuildItem(new MethodScanner() {
@Override
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
Map<String, Object> methodContext) {
if (applySecurityInterceptors && interceptedMethods.contains(method)) {
// EagerSecurityHandler needs to be present whenever the method requires eager interceptor
// because JAX-RS specific HTTP Security policies are defined by runtime config properties
// for example: when you annotate resource method with @Tenant("hr") you select OIDC tenant,
// so we can't authenticate before the tenant is selected, only after then HTTP perms can be checked
return List.of(EagerSecurityInterceptorHandler.Customizer.newInstance(),
EagerSecurityHandler.Customizer.newInstance());
EagerSecurityHandler.Customizer.newInstance(false));
} else {
if (denyJaxRs || hasDefaultJaxRsRolesAllowed) {
return List.of(EagerSecurityHandler.Customizer.newInstance());
} else {
return Objects
.requireNonNullElse(
consumeStandardSecurityAnnotations(method, actualEndpointClass, index,
(c) -> List.of(EagerSecurityHandler.Customizer.newInstance())),
Collections.emptyList());
}
return List.of(newEagerSecurityHandlerCustomizerInstance(method, actualEndpointClass, index,
withDefaultSecurityCheck));
}
}
});
}

private HandlerChainCustomizer newEagerSecurityHandlerCustomizerInstance(MethodInfo method, ClassInfo actualEndpointClass,
IndexView index, boolean withDefaultSecurityCheck) {
if (withDefaultSecurityCheck
|| consumeStandardSecurityAnnotations(method, actualEndpointClass, index, (c) -> c) != null) {
return EagerSecurityHandler.Customizer.newInstance(false);
}
return EagerSecurityHandler.Customizer.newInstance(true);
}

/**
* This results in adding {@link AllWriteableMarker} to user provided {@link MessageBodyWriter} classes
* that handle every class
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,55 @@
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
"""),
"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 @@ -40,8 +41,13 @@ public void apply(SecurityIdentity identity, MethodDescription method, Object[]

}
};
private final boolean onlyCheckForHttpPermissions;
private volatile SecurityCheck check;

public EagerSecurityHandler(boolean onlyCheckForHttpPermissions) {
this.onlyCheckForHttpPermissions = onlyCheckForHttpPermissions;
}

@Override
public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
if (!EagerSecurityContext.instance.authorizationController.isAuthorizationEnabled()) {
Expand All @@ -56,7 +62,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 Expand Up @@ -96,7 +107,7 @@ public void onFailure(Throwable failure) {
}

private Function<SecurityIdentity, Uni<?>> getSecurityCheck(ResteasyReactiveRequestContext requestContext) {
if (this.check == NULL_SENTINEL) {
if (this.onlyCheckForHttpPermissions || this.check == NULL_SENTINEL) {
return null;
}
SecurityCheck check = this.check;
Expand Down Expand Up @@ -210,20 +221,39 @@ private static boolean isRequestAlreadyChecked(ResteasyReactiveRequestContext re
return requestContext.getProperty(STANDARD_SECURITY_CHECK_INTERCEPTOR) != null;
}

public static class Customizer implements HandlerChainCustomizer {
public static abstract class Customizer implements HandlerChainCustomizer {

public static HandlerChainCustomizer newInstance() {
return new Customizer();
public static HandlerChainCustomizer newInstance(boolean onlyCheckForHttpPermissions) {
return onlyCheckForHttpPermissions ? new HttpPermissionsOnlyCustomizer()
: new HttpPermissionsAndSecurityChecksCustomizer();
}

@Override
public List<ServerRestHandler> handlers(Phase phase, ResourceClass resourceClass,
ServerResourceMethod serverResourceMethod) {
if (phase == Phase.AFTER_MATCH) {
return Collections.singletonList(new EagerSecurityHandler());
return Collections.singletonList(new EagerSecurityHandler(onlyCheckForHttpPermissions()));
}
return Collections.emptyList();
}

protected abstract boolean onlyCheckForHttpPermissions();

public static final class HttpPermissionsOnlyCustomizer extends Customizer {

@Override
protected boolean onlyCheckForHttpPermissions() {
return true;
}
}

public static final class HttpPermissionsAndSecurityChecksCustomizer extends Customizer {

@Override
protected boolean onlyCheckForHttpPermissions() {
return false;
}
}

}
}
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

0 comments on commit 86ba00e

Please sign in to comment.