[KEYCLOAK-19859] - Patching request filter to properly end responses
This commit is contained in:
parent
783eecf612
commit
7bef534392
5 changed files with 85 additions and 61 deletions
|
@ -17,12 +17,17 @@
|
||||||
|
|
||||||
package org.keycloak.quarkus.runtime.integration.web;
|
package org.keycloak.quarkus.runtime.integration.web;
|
||||||
|
|
||||||
|
import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
|
||||||
|
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
|
import org.keycloak.common.util.Resteasy;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.services.filters.AbstractRequestFilter;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.KeycloakTransactionManager;
|
||||||
|
|
||||||
import io.vertx.core.AsyncResult;
|
import io.vertx.core.AsyncResult;
|
||||||
import io.vertx.core.Handler;
|
import io.vertx.core.Handler;
|
||||||
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.core.http.HttpServerRequest;
|
import io.vertx.core.http.HttpServerRequest;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
|
||||||
|
@ -33,7 +38,7 @@ import io.vertx.ext.web.RoutingContext;
|
||||||
* <p>The filter itself runs in a event loop and should delegate to worker threads any blocking code (for now, all requests are handled
|
* <p>The filter itself runs in a event loop and should delegate to worker threads any blocking code (for now, all requests are handled
|
||||||
* as blocking).
|
* as blocking).
|
||||||
*/
|
*/
|
||||||
public class QuarkusRequestFilter extends AbstractRequestFilter implements Handler<RoutingContext> {
|
public class QuarkusRequestFilter implements Handler<RoutingContext> {
|
||||||
|
|
||||||
private static final Handler<AsyncResult<Object>> EMPTY_RESULT = result -> {
|
private static final Handler<AsyncResult<Object>> EMPTY_RESULT = result -> {
|
||||||
// we don't really care about the result because any exception thrown should be handled by the parent class
|
// we don't really care about the result because any exception thrown should be handled by the parent class
|
||||||
|
@ -43,49 +48,71 @@ public class QuarkusRequestFilter extends AbstractRequestFilter implements Handl
|
||||||
public void handle(RoutingContext context) {
|
public void handle(RoutingContext context) {
|
||||||
// our code should always be run as blocking until we don't provide a better support for running non-blocking code
|
// our code should always be run as blocking until we don't provide a better support for running non-blocking code
|
||||||
// in the event loop
|
// in the event loop
|
||||||
context.vertx().executeBlocking(promise -> {
|
context.vertx().executeBlocking(createBlockingHandler(context), false, EMPTY_RESULT);
|
||||||
ClientConnection connection = createClientConnection(context.request());
|
|
||||||
|
|
||||||
filter(connection, session -> {
|
|
||||||
try {
|
|
||||||
configureContextualData(context, connection, session);
|
|
||||||
|
|
||||||
// we need to close the session before response is sent to the client, otherwise subsequent requests could
|
|
||||||
// not get the latest state because the session from the previous request is still being closed
|
|
||||||
// other methods from Vert.x to add a handler to the response works asynchronously
|
|
||||||
context.addHeadersEndHandler(createEndHandler(context, promise, session));
|
|
||||||
context.next();
|
|
||||||
} catch (Throwable cause) {
|
|
||||||
context.fail(cause);
|
|
||||||
// re-throw so that the any exception is handled from parent
|
|
||||||
throw new RuntimeException(cause);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, false, EMPTY_RESULT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Handler<Void> createEndHandler(RoutingContext context, io.vertx.core.Promise<Object> promise,
|
private Handler<Promise<Object>> createBlockingHandler(RoutingContext context) {
|
||||||
KeycloakSession session) {
|
return promise -> {
|
||||||
return event -> {
|
KeycloakSessionFactory sessionFactory = getSessionFactory();
|
||||||
|
KeycloakSession session = sessionFactory.create();
|
||||||
|
|
||||||
|
configureContextualData(context, createClientConnection(context.request()), session);
|
||||||
|
configureEndHandler(context, promise, session);
|
||||||
|
|
||||||
|
KeycloakTransactionManager tx = session.getTransactionManager();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
close(session);
|
tx.begin();
|
||||||
|
context.next();
|
||||||
promise.complete();
|
promise.complete();
|
||||||
} catch (Throwable cause) {
|
} catch (Throwable cause) {
|
||||||
context.fail(cause);
|
promise.fail(cause);
|
||||||
|
// re-throw so that the any exception is handled from parent
|
||||||
|
throw new RuntimeException(cause);
|
||||||
|
} finally {
|
||||||
|
if (!context.response().headWritten()) {
|
||||||
|
// make sure the session is closed in case the handler is not called
|
||||||
|
// it might happen that, for whatever reason, downstream handlers do not end the response or
|
||||||
|
// no data was written to the response
|
||||||
|
close(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureContextualData(RoutingContext context, ClientConnection connection, KeycloakSession session) {
|
/**
|
||||||
// quarkus-resteasy changed and clears the context map before dispatching
|
* Creates a handler to close the {@link KeycloakSession} before the response is written to response but after Resteasy
|
||||||
// need to push keycloak contextual objects into the routing context for retrieving it later
|
* is done with processing its output.
|
||||||
context.data().put(KeycloakSession.class.getName(), session);
|
*/
|
||||||
context.data().put(ClientConnection.class.getName(), connection);
|
private void configureEndHandler(RoutingContext context, Promise<Object> promise, KeycloakSession session) {
|
||||||
|
context.addHeadersEndHandler(event -> {
|
||||||
|
try {
|
||||||
|
close(session);
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
promise.fail(cause);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void configureContextualData(RoutingContext context, ClientConnection connection, KeycloakSession session) {
|
||||||
protected boolean isAutoClose() {
|
Resteasy.pushContext(ClientConnection.class, connection);
|
||||||
return false;
|
Resteasy.pushContext(KeycloakSession.class, session);
|
||||||
|
// quarkus-resteasy changed and clears the context map before dispatching
|
||||||
|
// need to push keycloak contextual objects into the routing context for retrieving it later
|
||||||
|
context.put(KeycloakSession.class.getName(), session);
|
||||||
|
context.put(ClientConnection.class.getName(), connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void close(KeycloakSession session) {
|
||||||
|
KeycloakTransactionManager tx = session.getTransactionManager();
|
||||||
|
if (tx.isActive()) {
|
||||||
|
if (tx.getRollbackOnly()) {
|
||||||
|
tx.rollback();
|
||||||
|
} else {
|
||||||
|
tx.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientConnection createClientConnection(HttpServerRequest request) {
|
private ClientConnection createClientConnection(HttpServerRequest request) {
|
||||||
|
|
|
@ -33,4 +33,6 @@ public @interface SetDefaultProvider {
|
||||||
* the default provider is only set after enabling a feature.
|
* the default provider is only set after enabling a feature.
|
||||||
*/
|
*/
|
||||||
boolean beforeEnableFeature() default true;
|
boolean beforeEnableFeature() default true;
|
||||||
|
|
||||||
|
String defaultProvider() default "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,12 +176,13 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
commands.add("--auto-build");
|
commands.add("--auto-build");
|
||||||
commands.add("--http-enabled=true");
|
commands.add("--http-enabled=true");
|
||||||
|
|
||||||
if (configuration.getDebugPort() > 0) {
|
if (Boolean.parseBoolean(System.getProperty("auth.server.debug", "false"))) {
|
||||||
commands.add("--debug");
|
commands.add("--debug");
|
||||||
commands.add(Integer.toString(configuration.getDebugPort()));
|
if (configuration.getDebugPort() > 0) {
|
||||||
} else if (Boolean.parseBoolean(System.getProperty("auth.server.debug", "false"))) {
|
commands.add(Integer.toString(configuration.getDebugPort()));
|
||||||
commands.add("--debug");
|
} else {
|
||||||
commands.add(System.getProperty("auth.server.debug.port", "5005"));
|
commands.add(System.getProperty("auth.server.debug.port", "5005"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands.add("--http-port=" + configuration.getBindHttpPort());
|
commands.add("--http-port=" + configuration.getBindHttpPort());
|
||||||
|
@ -194,18 +195,9 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
commands.add("--cluster=" + System.getProperty("auth.server.quarkus.cluster.config", "local"));
|
commands.add("--cluster=" + System.getProperty("auth.server.quarkus.cluster.config", "local"));
|
||||||
commands.addAll(getAdditionalBuildArgs());
|
commands.addAll(getAdditionalBuildArgs());
|
||||||
|
|
||||||
if (!containsUserProfileSpiConfiguration(commands)) {
|
|
||||||
// ensure that at least one user profile provider is configured
|
|
||||||
commands.add("--spi-user-profile-provider=declarative-user-profile");
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands.toArray(new String[0]);
|
return commands.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean containsUserProfileSpiConfiguration(List<String> commands) {
|
|
||||||
return commands.stream().anyMatch(s -> s.startsWith("--spi-user-profile-provider="));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitForReadiness() throws MalformedURLException, LifecycleException {
|
private void waitForReadiness() throws MalformedURLException, LifecycleException {
|
||||||
SuiteContext suiteContext = this.suiteContext.get();
|
SuiteContext suiteContext = this.suiteContext.get();
|
||||||
//TODO: not sure if the best endpoint but it makes sure that everything is properly initialized. Once we have
|
//TODO: not sure if the best endpoint but it makes sure that everything is properly initialized. Once we have
|
||||||
|
|
|
@ -7,6 +7,7 @@ import org.keycloak.testsuite.arquillian.ContainerInfo;
|
||||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||||
import org.keycloak.testsuite.arquillian.containers.KeycloakQuarkusServerDeployableContainer;
|
import org.keycloak.testsuite.arquillian.containers.KeycloakQuarkusServerDeployableContainer;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
import org.wildfly.extras.creaper.core.online.CliException;
|
import org.wildfly.extras.creaper.core.online.CliException;
|
||||||
import org.wildfly.extras.creaper.core.online.ModelNodeResult;
|
import org.wildfly.extras.creaper.core.online.ModelNodeResult;
|
||||||
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
||||||
|
@ -27,7 +28,8 @@ public class SpiProvidersSwitchingUtils {
|
||||||
private enum SpiSwitcher {
|
private enum SpiSwitcher {
|
||||||
UNDERTOW {
|
UNDERTOW {
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> getCurrentDefaultProvider(Container container, String spiName) {
|
public Optional<String> getCurrentDefaultProvider(Container container, String spiName,
|
||||||
|
SetDefaultProvider annotation) {
|
||||||
return Optional.ofNullable(System.getProperty(getProviderPropertyName(spiName)));
|
return Optional.ofNullable(System.getProperty(getProviderPropertyName(spiName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +50,8 @@ public class SpiProvidersSwitchingUtils {
|
||||||
WILDFLY {
|
WILDFLY {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<String> getCurrentDefaultProvider(Container container, String spiName) {
|
public Optional<String> getCurrentDefaultProvider(Container container, String spiName,
|
||||||
|
SetDefaultProvider annotation) {
|
||||||
String cliCmd = SUBSYSTEM_KEYCLOAK_SERVER_SPI + spiName + ":read-attribute(name=default-provider)";
|
String cliCmd = SUBSYSTEM_KEYCLOAK_SERVER_SPI + spiName + ":read-attribute(name=default-provider)";
|
||||||
return runInCli(cliCmd).filter(ModelNodeResult::isSuccess)
|
return runInCli(cliCmd).filter(ModelNodeResult::isSuccess)
|
||||||
.map(n -> n.get("result").asString());
|
.map(n -> n.get("result").asString());
|
||||||
|
@ -88,12 +91,6 @@ public class SpiProvidersSwitchingUtils {
|
||||||
},
|
},
|
||||||
QUARKUS {
|
QUARKUS {
|
||||||
|
|
||||||
@Override
|
|
||||||
public Optional<String> getCurrentDefaultProvider(Container container, String spiName) {
|
|
||||||
return Optional.ofNullable(
|
|
||||||
getQuarkusContainer(container).getCurrentlyConfiguredSpiProviderFor(toDashCase(spiName)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDefaultProvider(Container container, String spiName, String providerId) {
|
public void setDefaultProvider(Container container, String spiName, String providerId) {
|
||||||
getQuarkusContainer(container).setAdditionalBuildArgs(Collections
|
getQuarkusContainer(container).setAdditionalBuildArgs(Collections
|
||||||
|
@ -102,8 +99,7 @@ public class SpiProvidersSwitchingUtils {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeProviderConfig(Container container, String spiName) {
|
public void removeProviderConfig(Container container, String spiName) {
|
||||||
getQuarkusContainer(container).setAdditionalBuildArgs(Collections
|
getQuarkusContainer(container).setAdditionalBuildArgs(Collections.emptyList());
|
||||||
.singletonList(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-provider=default"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private KeycloakQuarkusServerDeployableContainer getQuarkusContainer(Container container) {
|
private KeycloakQuarkusServerDeployableContainer getQuarkusContainer(Container container) {
|
||||||
|
@ -136,7 +132,14 @@ public class SpiProvidersSwitchingUtils {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public abstract Optional<String> getCurrentDefaultProvider(Container container, String spiName);
|
public Optional<String> getCurrentDefaultProvider(Container container, String spiName,
|
||||||
|
SetDefaultProvider annotation) {
|
||||||
|
String defaultProvider = annotation.defaultProvider();
|
||||||
|
if (StringUtil.isNotBlank(defaultProvider)) {
|
||||||
|
return Optional.of(defaultProvider);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void setDefaultProvider(Container container, String spiName, String providerId);
|
public abstract void setDefaultProvider(Container container, String spiName, String providerId);
|
||||||
|
|
||||||
|
@ -172,7 +175,7 @@ public class SpiProvidersSwitchingUtils {
|
||||||
log.infof("Setting default provider for %s to %s", spi, annotation.providerId());
|
log.infof("Setting default provider for %s to %s", spi, annotation.providerId());
|
||||||
|
|
||||||
if (annotation.onlyUpdateDefault()) {
|
if (annotation.onlyUpdateDefault()) {
|
||||||
spiSwitcher.getCurrentDefaultProvider(container, spi).ifPresent(v -> originalSettingsBackup.put(spi, v));
|
spiSwitcher.getCurrentDefaultProvider(container, spi, annotation).ifPresent(v -> originalSettingsBackup.put(spi, v));
|
||||||
spiSwitcher.updateDefaultProvider(container, spi, annotation.providerId());
|
spiSwitcher.updateDefaultProvider(container, spi, annotation.providerId());
|
||||||
} else {
|
} else {
|
||||||
spiSwitcher.setDefaultProvider(container, spi, annotation.providerId());
|
spiSwitcher.setDefaultProvider(container, spi, annotation.providerId());
|
||||||
|
|
|
@ -43,7 +43,7 @@ import java.util.Map;
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:joerg.matysiak@bosch.io">Jörg Matysiak</a>
|
* @author <a href="mailto:joerg.matysiak@bosch.io">Jörg Matysiak</a>
|
||||||
*/
|
*/
|
||||||
@SetDefaultProvider(spi="userProfile", providerId="custom-user-profile", onlyUpdateDefault = true)
|
@SetDefaultProvider(spi="userProfile", providerId="custom-user-profile", defaultProvider="declarative-user-profile", onlyUpdateDefault = true)
|
||||||
public class CustomUserProfileTest extends AbstractUserProfileTest {
|
public class CustomUserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue