Skip to content

Commit

Permalink
RESTEasy Classic - apply ex. mappers for auth failures before processing
Browse files Browse the repository at this point in the history
(cherry picked from commit 8bfa76e)
  • Loading branch information
michalvavrik authored and gsmet committed Nov 15, 2022
1 parent 7f3f666 commit a0f316d
Show file tree
Hide file tree
Showing 8 changed files with 273 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.util.Optional;
import java.util.function.Consumer;

import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem;
import io.quarkus.deployment.builditem.ExecutorBuildItem;
Expand All @@ -22,11 +24,13 @@
import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentBuildItem;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.DefaultRouteBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;
import io.vertx.core.Handler;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.RoutingContext;

public class ResteasyStandaloneBuildStep {
Expand Down Expand Up @@ -86,11 +90,17 @@ public void boot(ShutdownContextBuildItem shutdown,
// Routes use the order VertxHttpRecorder.DEFAULT_ROUTE_ORDER + 1 to ensure the default route is called before the resteasy one
Handler<RoutingContext> handler = recorder.vertxRequestHandler(vertx.getVertx(),
executorBuildItem.getExecutorProxy(), resteasyVertxConfig);

// failure handler for auth failures that occurred before the handler defined right above started processing the request
final Consumer<Route> addFailureHandler = recorder.addVertxFailureHandler(vertx.getVertx(),
executorBuildItem.getExecutorProxy(), resteasyVertxConfig);

// Exact match for resources matched to the root path
routes.produce(
RouteBuildItem.builder()
.orderedRoute(standalone.deploymentRootPath,
VertxHttpRecorder.AFTER_DEFAULT_ROUTE_ORDER_MARK + REST_ROUTE_ORDER_OFFSET)
VertxHttpRecorder.AFTER_DEFAULT_ROUTE_ORDER_MARK + REST_ROUTE_ORDER_OFFSET,
addFailureHandler)
.handler(handler).build());
String matchPath = standalone.deploymentRootPath;
if (matchPath.endsWith("/")) {
Expand All @@ -106,4 +116,10 @@ public void boot(ShutdownContextBuildItem shutdown,
recorder.start(shutdown, requireVirtual.isPresent());
}

@BuildStep
@Record(value = ExecutionTime.STATIC_INIT)
public FilterBuildItem addDefaultAuthFailureHandler(ResteasyStandaloneRecorder recorder) {
// replace default auth failure handler added by vertx-http so that our exception mappers can customize response
return new FilterBuildItem(recorder.defaultAuthFailureHandler(), FilterBuildItem.AUTHENTICATION - 1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.quarkus.resteasy.test.security;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

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.AuthenticationFailedException;
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 HttpPolicyAuthFailureExceptionMapperTest {

private static final String EXPECTED_RESPONSE = "expect response";

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(TestIdentityProvider.class, TestIdentityController.class)
.addAsResource(
new StringAsset("quarkus.http.auth.proactive=false\n" +
"quarkus.http.auth.permission.basic.paths=/*\n" +
"quarkus.http.auth.permission.basic.policy=authenticated\n"),
"application.properties"));

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

@Test
public void testAuthFailedExceptionMapper() {
RestAssured
.given()
.auth().basic("user", "unknown-pwd")
.get("/")
.then()
.statusCode(401)
.body(Matchers.equalTo(EXPECTED_RESPONSE));
}

@Provider
public static class AuthFailedExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {

@Override
public Response toResponse(AuthenticationFailedException exception) {
return Response.status(401).entity(EXPECTED_RESPONSE).build();
}
}

@Path("hello")
public static final class HelloResource {

@GET
public String hello() {
return "hello world";
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.resteasy.runtime.standalone;

import java.io.IOException;
import java.util.function.Consumer;

import org.jboss.logging.Logger;
import org.jboss.resteasy.core.ResteasyContext;
Expand Down Expand Up @@ -54,7 +55,7 @@ public ResteasyProviderFactory getProviderFactory() {
public void service(Context context,
HttpServerRequest req,
HttpServerResponse resp,
HttpRequest vertxReq, HttpResponse vertxResp, boolean handleNotFound) throws IOException {
HttpRequest vertxReq, HttpResponse vertxResp, boolean handleNotFound, Throwable throwable) throws IOException {

ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Expand All @@ -69,7 +70,15 @@ public void service(Context context,
ResteasyContext.pushContext(HttpServerRequest.class, req);
ResteasyContext.pushContext(HttpServerResponse.class, resp);
ResteasyContext.pushContext(Vertx.class, context.owner());
if (handleNotFound) {
if (throwable != null) {
dispatcher.pushContextObjects(vertxReq, vertxResp);
dispatcher.writeException(vertxReq, vertxResp, throwable, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) {

}
});
} else if (handleNotFound) {
dispatcher.invoke(vertxReq, vertxResp);
} else {
dispatcher.invokePropagateNotFound(vertxReq, vertxResp);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package io.quarkus.resteasy.runtime.standalone;

import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.jboss.resteasy.specimpl.ResteasyUriInfo;
import org.jboss.resteasy.spi.ResteasyConfiguration;
import org.jboss.resteasy.spi.ResteasyDeployment;

Expand All @@ -12,9 +15,15 @@
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.AuthenticationRedirectException;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Route;
import io.vertx.ext.web.RoutingContext;

/**
Expand All @@ -23,7 +32,7 @@
@Recorder
public class ResteasyStandaloneRecorder {

public static final String META_INF_RESOURCES = "META-INF/resources";
static final String RESTEASY_URI_INFO = ResteasyUriInfo.class.getName();

private static boolean useDirect = true;

Expand Down Expand Up @@ -68,6 +77,80 @@ public Handler<RoutingContext> vertxRequestHandler(Supplier<Vertx> vertx, Execut
return null;
}

public Consumer<Route> addVertxFailureHandler(Supplier<Vertx> vertx, Executor executor, ResteasyVertxConfig config) {
if (deployment == null) {
return null;
} else {
return new Consumer<Route>() {
@Override
public void accept(Route route) {
// allow customization of auth failures with exception mappers; this failure handler is only
// used when auth failed before RESTEasy Classic began processing the request
route.failureHandler(new VertxRequestHandler(vertx.get(), deployment, contextPath,
new ResteasyVertxAllocator(config.responseBufferSize), executor,
readTimeout.getValue().readTimeout.toMillis()) {

@Override
public void handle(RoutingContext request) {
if (request.failure() instanceof AuthenticationFailedException
|| request.failure() instanceof AuthenticationCompletionException
|| request.failure() instanceof AuthenticationRedirectException) {
super.handle(request);
} else {
request.next();
}
}

@Override
protected void setCurrentIdentityAssociation(RoutingContext routingContext) {
// security identity is not available as authentication failed
}
});
}
};
}
}

public Handler<RoutingContext> defaultAuthFailureHandler() {
return new Handler<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
if (event.get(QuarkusHttpUser.AUTH_FAILURE_HANDLER) instanceof DefaultAuthFailureHandler) {

// only replace default auth failure handler if we can extract URI info
// as org.jboss.resteasy.plugins.server.BaseHttpRequest requires it;
// we need to extract URI info here as if auth failure will happen further upstream
// we want to return 401 and correct headers rather than 400 (malformed input) and so on
try {
event.put(RESTEASY_URI_INFO, VertxUtil.extractUriInfo(event.request(), contextPath));
} catch (Exception e) {
// URI could be malformed or there has been internal error when extracting URI info
// keep default behavior (don't fail event, let default auth failure handler to handle this)
event.next();
return;
}

// fail event rather than end it, so that exception mappers can customize response
event.put(QuarkusHttpUser.AUTH_FAILURE_HANDLER, new BiConsumer<RoutingContext, Throwable>() {

@Override
public void accept(RoutingContext event, Throwable throwable) {
if (event.failed()) {
//auth failure handler should never get called from route failure handlers
//but if we get to this point bad things have happened,
//so it is better to send a response than to hang
event.end();
} else {
event.fail(throwable);
}
}
});
}
event.next();
}
};
}

private static class ResteasyVertxAllocator implements BufferAllocator {

private final int bufferSize;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.resteasy.runtime.standalone;

import static io.quarkus.resteasy.runtime.standalone.ResteasyStandaloneRecorder.RESTEASY_URI_INFO;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -99,33 +101,27 @@ public void run() {
}

private void dispatch(RoutingContext routingContext, InputStream is, VertxOutput output) {
ResteasyUriInfo uriInfo;
try {
uriInfo = VertxUtil.extractUriInfo(routingContext.request(), rootPath);
} catch (Exception e) {
if (e.getCause() instanceof MalformedInputException) {
log.debug(e.getCause());
routingContext.response().setStatusCode(400);
} else {
log.debug(e);
routingContext.response().setStatusCode(500);
ResteasyUriInfo uriInfo = routingContext.get(RESTEASY_URI_INFO);
if (uriInfo == null) {
try {
uriInfo = VertxUtil.extractUriInfo(routingContext.request(), rootPath);
} catch (Exception e) {
if (e.getCause() instanceof MalformedInputException) {
log.debug(e.getCause());
routingContext.response().setStatusCode(400);
} else {
log.debug(e);
routingContext.response().setStatusCode(500);
}
routingContext.response().end();
return;
}
routingContext.response().end();
return;
}

ManagedContext requestContext = Arc.container().requestContext();
requestContext.activate();
routingContext.remove(QuarkusHttpUser.AUTH_FAILURE_HANDLER);
if (association != null) {
QuarkusHttpUser existing = (QuarkusHttpUser) routingContext.user();
if (existing != null) {
SecurityIdentity identity = existing.getSecurityIdentity();
association.setIdentity(identity);
} else {
association.setIdentity(QuarkusHttpUser.getSecurityIdentity(routingContext, null));
}
}
setCurrentIdentityAssociation(routingContext);
currentVertxRequest.setCurrent(routingContext);
try {
Context ctx = vertx.getOrCreateContext();
Expand All @@ -148,7 +144,7 @@ private void dispatch(RoutingContext routingContext, InputStream is, VertxOutput
map.put(RoutingContext.class, routingContext);
try (ResteasyContext.CloseableContext restCtx = ResteasyContext.addCloseableContextDataLevel(map)) {
ContextUtil.pushContext(routingContext);
dispatcher.service(ctx, request, response, vertxRequest, vertxResponse, true);
dispatcher.service(ctx, request, response, vertxRequest, vertxResponse, true, routingContext.failure());
} catch (Failure e1) {
vertxResponse.setStatus(e1.getErrorCode());
if (e1.isLoggable()) {
Expand Down Expand Up @@ -187,4 +183,16 @@ private void dispatch(RoutingContext routingContext, InputStream is, VertxOutput
}
}

protected void setCurrentIdentityAssociation(RoutingContext routingContext) {
if (association != null) {
QuarkusHttpUser existing = (QuarkusHttpUser) routingContext.user();
if (existing != null) {
SecurityIdentity identity = existing.getSecurityIdentity();
association.setIdentity(identity);
} else {
association.setIdentity(QuarkusHttpUser.getSecurityIdentity(routingContext, null));
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.it.resteasy.elytron;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import io.quarkus.security.AuthenticationFailedException;

@Provider
public class AuthFailedExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {

static final String EXPECTED_RESPONSE = "expected response";

@Override
public Response toResponse(AuthenticationFailedException exception) {
return Response.status(401).entity(EXPECTED_RESPONSE).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ quarkus.security.users.embedded.roles.mary=managers
quarkus.security.users.embedded.users.poul=poul
quarkus.security.users.embedded.roles.poul=interns
quarkus.security.users.embedded.plain-text=true
quarkus.http.auth.basic=true
quarkus.http.auth.basic=true

%auth-failed-ex-mapper.quarkus.http.auth.permission.basic.paths=/*
%auth-failed-ex-mapper.quarkus.http.auth.permission.basic.policy=authenticated
%auth-failed-ex-mapper.quarkus.http.auth.proactive=false
Loading

0 comments on commit a0f316d

Please sign in to comment.