Redirect to relative-path from the root path (#32868)

Closes #32863

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš 2024-09-18 15:37:28 +01:00 committed by GitHub
parent 9e42e8013d
commit 84564f080a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 97 additions and 9 deletions

View file

@ -124,6 +124,15 @@ The support is in preview mode, and we would be happy to obtain any feedback.
For more information, see the link:{tracingguide_link}[{tracingguide_name}] guide.
= Automatic redirect from root to relative path
User is automatically redirected to the path where {project_name} is hosted when the `http-relative-path` property is specified.
It means when the relative path is set to `/auth`, and the user access `localhost:8080/`, the page is redirected to `localhost:8080/auth`.
The same applies to the management interface when the `http-management-relative-path` or `http-relative-path` property is specified.
It improves user experience as users no longer need to set the relative path to the URL explicitly.
= Removal of legacy cookies
Keycloak no longer sends `_LEGACY` cookies, which where introduced as a work-around to older browsers not supporting

View file

@ -36,6 +36,15 @@ to perform any cleanup or custom processing when a realm, and their groups, are
The `GroupProvider` interface is also updated with a new `preRemove(RealmModel)` method to force implementations to properly
handle the removal of groups when a realm is removed.
= Automatic redirect from root to relative path
User is automatically redirected to the path where {project_name} is hosted when the `http-relative-path` property is specified.
It means when the relative path is set to `/auth`, and the user access `localhost:8080/`, the page is redirected to `localhost:8080/auth`.
The same applies to the management interface when the `http-management-relative-path` or `http-relative-path` property is specified.
It improves user experience as users no longer need to set the relative path to the URL explicitly.
= Operator scheduling defaults
Keycloak Pods will now have default affinities to prevent multiple instances from the same CR from being deployed on the same node, and all Pods from the same CR will prefer to be in the same zone to prevent stretch cache clusters.

View file

@ -62,6 +62,9 @@ By default, the new Quarkus distribution removes `/auth` from the context-path.
bin/kc.[sh|bat] start-dev --http-relative-path /auth
----
When the relative path is specified, it is still possible to be redirected from the root to the relative path.
Specifically, when the user access `localhost:8080/`, the page is redirected to the `localhost:8080/auth`.
== Migrating custom providers
Similarly to the WildFly distribution custom providers are deployed to Keycloak by copying them to a deployment directory. In the new distribution you should copy your providers to the `providers` directory instead of `standalone/deployments`, which no longer exists. Additional dependencies are also copied to the `providers` directory.

View file

@ -16,19 +16,25 @@ The most significant advantage might be seen in Kubernetes environments as the s
The management interface is turned on when something is exposed on it.
Management endpoints such as `/metrics` and `/health` are exposed on the default management port `9000` when metrics and health are enabled.
The management interface provides a set of options and is fully configurable.
In order to change the port for the management interface, you can use the {project_name} option `http-management-port`.
NOTE: If management interface properties are not explicitly set, their values are automatically inherited from the default HTTP server.
=== Port
In order to change the port for the management interface, you can use the {project_name} option `http-management-port`.
=== Relative path
You can change the relative path of the management interface, as the prefix path for the management endpoints can be different.
You can achieve it via the {project_name} option `http-management-relative-path`.
For instance, if you set the CLI option `--http-management-relative-path=/management`, the metrics, and health endpoints will be accessed on the `/management/metrics` and `/management/health` paths.
User is automatically *redirected* to the path where {project_name} is hosted when the relative path is specified.
It means when the relative path is set to `/management`, and the user access `localhost:9000/`, the page is redirected to `localhost:9000/management`.
NOTE: If you do not explicitly set the value for it, the value from the `http-relative-path` property is used. For instance,
if you set the CLI option `--http-relative-path=/auth`, these endpoints are accessible on the `/auth/metrics` and `/auth/health` paths.
== TLS support
=== TLS support
When the TLS is set for the default {project_name} server, the management interface will be accessible through HTTPS as well.
The management interface can run only either on HTTP or HTTPS, not both as for the main server.
@ -36,7 +42,7 @@ The management interface can run only either on HTTP or HTTPS, not both as for t
Specific {project_name} management interface options with the prefix `https-management-*` were provided for setting different TLS parameters for the management HTTP server. Their function is similar to their counterparts for the main HTTP server, for details see <@links.server id="enabletls" />.
When these options are not explicitly set, the TLS parameters are inherited from the default HTTP server.
== Disable Management interface
=== Disable Management interface
The management interface is automatically turned off when nothing is exposed on it.
Currently, only health checks and metrics are exposed on the management interface regardless.

View file

@ -25,7 +25,6 @@ import io.quarkus.agroal.spi.JdbcDriverBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BuildTimeConditionBuildItem;
import io.quarkus.bootstrap.logging.InitialConfigurator;
import io.quarkus.builder.item.EmptyBuildItem;
import io.quarkus.datasource.deployment.spi.DevServicesDatasourceResultBuildItem;
import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig;
import io.quarkus.deployment.IsDevelopment;
@ -48,6 +47,7 @@ import io.quarkus.hibernate.orm.deployment.spi.AdditionalJpaModelBuildItem;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.smallrye.config.ConfigValue;
@ -77,6 +77,7 @@ import org.keycloak.common.util.MultiSiteUtils;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.config.DatabaseOptions;
import org.keycloak.config.HealthOptions;
import org.keycloak.config.HttpOptions;
import org.keycloak.config.ManagementOptions;
import org.keycloak.config.MetricsOptions;
import org.keycloak.config.SecurityOptions;
@ -123,6 +124,7 @@ import org.keycloak.theme.ThemeResourceSpi;
import org.keycloak.transaction.JBossJtaTransactionManagerLookup;
import org.keycloak.userprofile.config.UPConfigUtils;
import org.keycloak.util.JsonSerialization;
import org.keycloak.utils.StringUtil;
import org.keycloak.vault.FilesKeystoreVaultProviderFactory;
import org.keycloak.vault.FilesPlainTextVaultProviderFactory;
@ -234,17 +236,43 @@ class KeycloakProcessor {
recorder.configureProfile(profile.getName(), profile.getFeatures());
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Consume(ConfigBuildItem.class)
void configureRedirectForRootPath(BuildProducer<RouteBuildItem> routes,
HttpRootPathBuildItem httpRootPathBuildItem,
KeycloakRecorder recorder) {
Configuration.getOptionalKcValue(HttpOptions.HTTP_RELATIVE_PATH)
.filter(StringUtil::isNotBlank)
.filter(f -> !f.equals("/"))
.ifPresent(relativePath ->
routes.produce(httpRootPathBuildItem.routeBuilder()
.route("/")
.handler(recorder.getRedirectHandler(relativePath))
.build())
);
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep(onlyIf = IsManagementEnabled.class)
@Consume(ConfigBuildItem.class)
void configureManagementInterface(BuildProducer<RouteBuildItem> routes,
NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem,
KeycloakRecorder recorder) {
final var path = Configuration.getOptionalKcValue(ManagementOptions.HTTP_MANAGEMENT_RELATIVE_PATH.getKey()).orElse("/");
final var relativePath = Configuration.getOptionalKcValue(ManagementOptions.HTTP_MANAGEMENT_RELATIVE_PATH).orElse("/");
if (StringUtil.isNotBlank(relativePath) && !relativePath.equals("/")) {
// redirect from / to the relativePath
routes.produce(nonApplicationRootPathBuildItem.routeBuilder()
.management()
.route("/")
.handler(recorder.getRedirectHandler(relativePath))
.build());
}
routes.produce(nonApplicationRootPathBuildItem.routeBuilder()
.management()
.route(path)
.route(relativePath)
.handler(recorder.getManagementHandler())
.build());
}

View file

@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
class KeycloakPathConfigurationTest {
@ -98,11 +99,16 @@ class KeycloakPathConfigurationTest {
}
@Test
void testRootUnavailable() {
void testRootRedirect() {
given().basePath("/").redirects().follow(false)
.when().get("")
.then()
.statusCode(302)
.header("Location", is("/auth"));
given().basePath("/")
.when().get("")
.then()
// application root is configured to /auth, so we expect 404 on /
.statusCode(404);
.statusCode(200);
}
}

View file

@ -80,6 +80,11 @@ public class KeycloakRecorder {
Profile.init(profileName, features);
}
// default handler for redirecting to specific path
public Handler<RoutingContext> getRedirectHandler(String redirectPath) {
return routingContext -> routingContext.redirect(redirectPath);
}
// default handler for the management interface
public Handler<RoutingContext> getManagementHandler() {
return routingContext -> routingContext.response().end("Keycloak Management Interface");

View file

@ -29,7 +29,9 @@ import org.keycloak.it.utils.KeycloakDistribution;
import java.io.IOException;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ -120,10 +122,30 @@ public class ManagementDistTest {
assertRelativePath(result, "/management");
}
@Test
@Launch({"start-dev", "--http-relative-path=/auth", "--http-management-relative-path=/management"})
void testManagementRootRedirects(LaunchResult result, KeycloakDistribution distribution) {
assertRelativePath(result, "/management");
distribution.setRequestPort(8080);
given().redirects().follow(false).when().get("/").then().statusCode(302).header("Location", is("/auth"));
when().get("/").then().statusCode(200).body(containsString("Welcome to Keycloak"));
when().get("/auth").then().statusCode(200).body(containsString("Welcome to Keycloak"));
}
private void assertRelativePath(LaunchResult result, String relativePath) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Management interface listening on http://0.0.0.0:9000");
given().redirects().follow(false).when().get("/").then()
.statusCode(302)
.and()
.header("Location", is(relativePath));
when().get("/").then()
.statusCode(200)
.and()
.body(is("Keycloak Management Interface"));
when().get(relativePath).then()
.statusCode(200)
.and()