From ef387cd7cf02372c379f1829717ce999661455cd Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Tue, 24 Oct 2023 11:44:13 +0200 Subject: [PATCH 1/3] Move KeycloakContainer to keycloak-server test-framework Create realm by posting to Keycloak admin API --- .../src/main/resources/application.properties | 5 +- .../it/kafka/KafkaKeycloakTestResource.java | 47 +++++++-- .../kafka/containers/KeycloakContainer.java | 42 -------- .../src/test/resources/kafkaServer.properties | 15 +-- .../keycloak/client/KeycloakTestClient.java | 37 +++++++ .../keycloak/server/KeycloakContainer.java | 98 +++++++++++++++++++ .../KeycloakTestResourceLifecycleManager.java | 53 +++------- 7 files changed, 188 insertions(+), 109 deletions(-) delete mode 100644 integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/containers/KeycloakContainer.java create mode 100644 test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakContainer.java diff --git a/integration-tests/kafka-oauth-keycloak/src/main/resources/application.properties b/integration-tests/kafka-oauth-keycloak/src/main/resources/application.properties index 44480d97cdda9..ba681b7c83819 100644 --- a/integration-tests/kafka-oauth-keycloak/src/main/resources/application.properties +++ b/integration-tests/kafka-oauth-keycloak/src/main/resources/application.properties @@ -4,10 +4,7 @@ quarkus.log.category.\"org.apache.zookeeper\".level=WARN mp.messaging.connector.smallrye-kafka.security.protocol=SASL_PLAINTEXT mp.messaging.connector.smallrye-kafka.sasl.mechanism=OAUTHBEARER -mp.messaging.connector.smallrye-kafka.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ - oauth.client.id="kafka-client" \ - oauth.client.secret="kafka-client-secret" \ - oauth.token.endpoint.uri="http://keycloak:8080/auth/realms/kafka-authz/protocol/openid-connect/token" ; +mp.messaging.connector.smallrye-kafka.sasl.jaas.config=set_by_test mp.messaging.connector.smallrye-kafka.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler mp.messaging.outgoing.out.connector=smallrye-kafka diff --git a/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java b/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java index bfac502d54c94..e3319c8914171 100644 --- a/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java +++ b/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/KafkaKeycloakTestResource.java @@ -5,45 +5,76 @@ import java.util.HashMap; import java.util.Map; -import org.jboss.logging.Logger; import org.testcontainers.utility.MountableFile; -import io.quarkus.it.kafka.containers.KeycloakContainer; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.quarkus.test.keycloak.client.KeycloakTestClient; +import io.quarkus.test.keycloak.server.KeycloakContainer; import io.strimzi.test.container.StrimziKafkaContainer; public class KafkaKeycloakTestResource implements QuarkusTestResourceLifecycleManager { - private static final Logger log = Logger.getLogger(KafkaKeycloakTestResource.class); private StrimziKafkaContainer kafka; private KeycloakContainer keycloak; + private static final String KEYCLOAK_REALM_JSON = System.getProperty("keycloak.realm.json"); @Override public Map start() { - Map properties = new HashMap<>(); //Start keycloak container keycloak = new KeycloakContainer(); keycloak.start(); - log.info(keycloak.getLogs()); - keycloak.createHostsFile(); + KeycloakTestClient client = new KeycloakTestClient(keycloak.getServerUrl()); + client.createRealmFromPath(KEYCLOAK_REALM_JSON); //Start kafka container this.kafka = new StrimziKafkaContainer("quay.io/strimzi/kafka:latest-kafka-3.0.0") .withBrokerId(1) - .withKafkaConfigurationMap(Map.of("listener.security.protocol.map", "JWT:SASL_PLAINTEXT,BROKER1:PLAINTEXT")) + .withKafkaConfigurationMap(Map.of("listener.security.protocol.map", + "JWT:SASL_PLAINTEXT,BROKER1:PLAINTEXT", + "listener.name.jwt.oauthbearer.sasl.jaas.config", + getOauthSaslJaasConfig(keycloak.getInternalUrl(), keycloak.getServerUrl()), + "listener.name.jwt.plain.sasl.jaas.config", + getPlainSaslJaasConfig(keycloak.getInternalUrl(), keycloak.getServerUrl()))) .withNetworkAliases("kafka") .withServerProperties(MountableFile.forClasspathResource("kafkaServer.properties")) .withBootstrapServers( c -> String.format("JWT://%s:%s", c.getHost(), c.getMappedPort(KAFKA_PORT))); this.kafka.start(); - log.info(this.kafka.getLogs()); properties.put("kafka.bootstrap.servers", this.kafka.getBootstrapServers()); + properties.put("mp.messaging.connector.smallrye-kafka.sasl.jaas.config", + getClientSaslJaasConfig(keycloak.getServerUrl())); return properties; } + private String getClientSaslJaasConfig(String keycloakServerUrl) { + return "org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required" + + " oauth.client.id=\"kafka-client\"" + + " oauth.client.secret=\"kafka-client-secret\"" + + " oauth.token.endpoint.uri=\"" + keycloakServerUrl + "/realms/kafka-authz/protocol/openid-connect/token\";"; + } + + private String getPlainSaslJaasConfig(String keycloakInternalUrl, String keycloakServerUrl) { + return "'org.apache.kafka.common.security.plain.PlainLoginModule required " + + "oauth.jwks.endpoint.uri=\"" + keycloakInternalUrl + "/realms/kafka-authz/protocol/openid-connect/certs\" " + + "oauth.valid.issuer.uri=\"" + keycloakServerUrl + "/realms/kafka-authz\" " + + "oauth.token.endpoint.uri=\"" + keycloakInternalUrl + "/realms/kafka-authz/protocol/openid-connect/token\" " + + "oauth.client.id=\"kafka\" " + + "oauth.client.secret=\"kafka-secret\" " + + "unsecuredLoginStringClaim_sub=\"admin\";'"; + } + + private String getOauthSaslJaasConfig(String keycloakInternalUrl, String keycloakServerUrl) { + return "'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required " + + "oauth.jwks.endpoint.uri=\"" + keycloakInternalUrl + "/realms/kafka-authz/protocol/openid-connect/certs\" " + + "oauth.valid.issuer.uri=\"" + keycloakServerUrl + "/realms/kafka-authz\" " + + "oauth.token.endpoint.uri=\"" + keycloakInternalUrl + "/realms/kafka-authz/protocol/openid-connect/token\" " + + "oauth.client.id=\"kafka\" " + + "oauth.client.secret=\"kafka-secret\";'"; + } + @Override public void stop() { if (kafka != null) { diff --git a/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/containers/KeycloakContainer.java b/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/containers/KeycloakContainer.java deleted file mode 100644 index d3f46a1d3e5a9..0000000000000 --- a/integration-tests/kafka-oauth-keycloak/src/test/java/io/quarkus/it/kafka/containers/KeycloakContainer.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.quarkus.it.kafka.containers; - -import java.io.FileWriter; - -import org.testcontainers.containers.FixedHostPortGenericContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.MountableFile; - -public class KeycloakContainer extends FixedHostPortGenericContainer { - - public KeycloakContainer() { - super("quay.io/keycloak/keycloak:16.1.1"); - withExposedPorts(8443); - withFixedExposedPort(8080, 8080); - withEnv("KEYCLOAK_USER", "admin"); - withEnv("KEYCLOAK_PASSWORD", "admin"); - withEnv("KEYCLOAK_HTTPS_PORT", "8443"); - withEnv("PROXY_ADDRESS_FORWARDING", "true"); - withEnv("KEYCLOAK_IMPORT", "/opt/jboss/keycloak/realms/kafka-authz-realm.json"); - waitingFor(Wait.forLogMessage(".*WFLYSRV0025.*", 1)); - withNetwork(Network.SHARED); - withNetworkAliases("keycloak"); - withCopyFileToContainer(MountableFile.forClasspathResource("keycloak/realms/kafka-authz-realm.json"), - "/opt/jboss/keycloak/realms/kafka-authz-realm.json"); - withCommand("-Dkeycloak.profile.feature.upload_scripts=enabled", "-b", "0.0.0.0"); - } - - public void createHostsFile() { - try (FileWriter fileWriter = new FileWriter("target/hosts")) { - String dockerHost = this.getHost(); - if ("localhost".equals(dockerHost)) { - fileWriter.write("127.0.0.1 keycloak"); - } else { - fileWriter.write(dockerHost + " keycloak"); - } - fileWriter.flush(); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/integration-tests/kafka-oauth-keycloak/src/test/resources/kafkaServer.properties b/integration-tests/kafka-oauth-keycloak/src/test/resources/kafkaServer.properties index 9e0bd573cf46c..d148fcc18242f 100644 --- a/integration-tests/kafka-oauth-keycloak/src/test/resources/kafkaServer.properties +++ b/integration-tests/kafka-oauth-keycloak/src/test/resources/kafkaServer.properties @@ -74,22 +74,11 @@ oauth.username.claim=preferred_username principal.builder.class=io.strimzi.kafka.oauth.server.OAuthKafkaPrincipalBuilder listener.name.jwt.sasl.enabled.mechanisms=OAUTHBEARER,PLAIN -listener.name.jwt.oauthbearer.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ - oauth.jwks.endpoint.uri="http://keycloak:8080/auth/realms/kafka-authz/protocol/openid-connect/certs" \ - oauth.valid.issuer.uri="http://keycloak:8080/auth/realms/kafka-authz" \ - oauth.token.endpoint.uri="http://keycloak:8080/auth/realms/kafka-authz/protocol/openid-connect/token" \ - oauth.client.id="kafka" \ - oauth.client.secret="kafka-secret"; +listener.name.jwt.oauthbearer.sasl.jaas.config=set_by_test listener.name.jwt.oauthbearer.sasl.server.callback.handler.class=io.strimzi.kafka.oauth.server.JaasServerOauthValidatorCallbackHandler listener.name.jwt.oauthbearer.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler -listener.name.jwt.plain.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \ - oauth.jwks.endpoint.uri="http://keycloak:8080/auth/realms/kafka-authz/protocol/openid-connect/certs" \ - oauth.valid.issuer.uri="http://keycloak:8080/auth/realms/kafka-authz" \ - oauth.token.endpoint.uri="http://keycloak:8080/auth/realms/kafka-authz/protocol/openid-connect/token" \ - oauth.client.id="kafka" \ - oauth.client.secret="kafka-secret" \ - unsecuredLoginStringClaim_sub="admin"; +#listener.name.jwt.plain.sasl.jaas.config=set_by_test listener.name.jwt.plain.sasl.server.callback.handler.class=io.strimzi.kafka.oauth.server.plain.JaasServerOauthOverPlainValidatorCallbackHandler diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java index 83ec3a7109367..ba0f7171fc8f6 100644 --- a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/client/KeycloakTestClient.java @@ -1,10 +1,14 @@ package io.quarkus.test.keycloak.client; import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.List; import org.eclipse.microprofile.config.ConfigProvider; @@ -30,8 +34,14 @@ public class KeycloakTestClient implements DevServicesContext.ContextAware { private DevServicesContext testContext; + private final String authServerUrl; + public KeycloakTestClient() { + this(null); + } + public KeycloakTestClient(String authServerUrl) { + this.authServerUrl = authServerUrl; } /** @@ -268,6 +278,9 @@ public String getAuthServerBaseUrl() { * For example: 'http://localhost:8081/auth/realms/quarkus'. */ public String getAuthServerUrl() { + if (this.authServerUrl != null) { + return this.authServerUrl; + } String authServerUrl = getPropertyValue(CLIENT_AUTH_SERVER_URL_PROP, null); if (authServerUrl == null) { authServerUrl = getPropertyValue(AUTH_SERVER_URL_PROP, null); @@ -316,6 +329,30 @@ public void deleteRealm(RealmRepresentation realm) { deleteRealm(realm.getRealm()); } + public void createRealmFromPath(String path) { + RealmRepresentation representation = readRealmFile(path); + createRealm(representation); + } + + public RealmRepresentation readRealmFile(String realmPath) { + try { + return readRealmFile(Path.of(realmPath).toUri().toURL(), realmPath); + } catch (MalformedURLException ex) { + // Will not happen as this method is called only when it is confirmed the file exists + throw new RuntimeException(ex); + } + } + + public RealmRepresentation readRealmFile(URL url, String realmPath) { + try { + try (InputStream is = url.openStream()) { + return JsonSerialization.readValue(is, RealmRepresentation.class); + } + } catch (IOException ex) { + throw new RuntimeException("Realm " + realmPath + " resource can not be opened", ex); + } + } + private String getPropertyValue(String prop, String defaultValue) { return ConfigProvider.getConfig().getOptionalValue(prop, String.class) .orElseGet(() -> getDevProperty(prop, defaultValue)); diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakContainer.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakContainer.java new file mode 100644 index 0000000000000..c9fcc852d835c --- /dev/null +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakContainer.java @@ -0,0 +1,98 @@ +package io.quarkus.test.keycloak.server; + +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import io.quarkus.runtime.configuration.ConfigurationException; +import io.restassured.RestAssured; + +public class KeycloakContainer extends GenericContainer { + + private static final String KEYCLOAK_VERSION = System.getProperty("keycloak.version"); + private static final String KEYCLOAK_DOCKER_IMAGE = System.getProperty("keycloak.docker.image"); + + private boolean useHttps; + private Integer fixedPort; + private final boolean legacy; + + private static String getKeycloakImageName() { + if (KEYCLOAK_DOCKER_IMAGE != null) { + return KEYCLOAK_DOCKER_IMAGE; + } else if (KEYCLOAK_VERSION != null) { + return "quay.io/keycloak/keycloak:" + KEYCLOAK_VERSION; + } else { + throw new ConfigurationException("Please set either 'keycloak.docker.image' or 'keycloak.version' system property"); + } + } + + public KeycloakContainer() { + this(DockerImageName.parse(getKeycloakImageName())); + } + + public KeycloakContainer(DockerImageName keycloakImageName) { + super(keycloakImageName); + this.legacy = keycloakImageName.getVersionPart().endsWith("-legacy"); + withExposedPorts(8080, 8443); + // Keycloak env vars + withEnv("KC_HTTP_ENABLED", "true"); + withEnv("KEYCLOAK_ADMIN", "admin"); + withEnv("KEYCLOAK_ADMIN_PASSWORD", "admin"); + withEnv("KC_HOSTNAME_STRICT", "false"); + withEnv("KC_HOSTNAME_STRICT_HTTPS", "false"); + withEnv("KC_STORAGE", "chm"); + // Keycloak legacy env vars + withEnv("DB_VENDOR", "H2"); + withEnv("KEYCLOAK_USER", "admin"); + withEnv("KEYCLOAK_PASSWORD", "admin"); + withEnv("KEYCLOAK_HTTPS_PORT", "8443"); + withNetwork(Network.SHARED); + withNetworkAliases("keycloak"); + } + + public KeycloakContainer withUseHttps(boolean useHttps) { + this.useHttps = useHttps; + return this; + } + + public KeycloakContainer withFixedPort(int fixedPort) { + this.fixedPort = fixedPort; + return this; + } + + @Override + protected void doStart() { + if (!legacy && getCommandParts().length == 0) { + withCommand("start-dev"); + } + if (fixedPort != null) { + addFixedExposedPort(fixedPort, getPort()); + } + if (useHttps) { + RestAssured.useRelaxedHTTPSValidation(); + waitingFor(Wait.forHttps(getAuthPath()).forPort(8443).allowInsecure()); + } else { + waitingFor(Wait.forHttp(getAuthPath()).forPort(8080)); + } + super.doStart(); + } + + public int getPort() { + return useHttps ? 8443 : 8080; + } + + private String getAuthPath() { + return legacy ? "/auth" : ""; + } + + public String getServerUrl() { + return String.format("%s://%s:%d" + getAuthPath(), + useHttps ? "https" : "http", this.getHost(), this.getMappedPort(getPort())); + } + + public String getInternalUrl() { + return String.format("%s://keycloak:%d" + getAuthPath(), + useHttps ? "https" : "http", getPort()); + } +} diff --git a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java index 4b1d8b4676950..f83b34dc53171 100644 --- a/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java +++ b/test-framework/keycloak-server/src/main/java/io/quarkus/test/keycloak/server/KeycloakTestResourceLifecycleManager.java @@ -15,67 +15,36 @@ import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.util.JsonSerialization; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; public class KeycloakTestResourceLifecycleManager implements QuarkusTestResourceLifecycleManager { - private GenericContainer keycloak; - private static String KEYCLOAK_SERVER_URL; + private static KeycloakContainer keycloak; + private static final String KEYCLOAK_REALM = System.getProperty("keycloak.realm", "quarkus"); private static final String KEYCLOAK_SERVICE_CLIENT = System.getProperty("keycloak.service.client", "quarkus-service-app"); private static final String KEYCLOAK_WEB_APP_CLIENT = System.getProperty("keycloak.web-app.client", "quarkus-web-app"); private static final Boolean KEYCLOAK_USE_HTTPS = Boolean.valueOf(System.getProperty("keycloak.use.https", "true")); - private static final String KEYCLOAK_VERSION = System.getProperty("keycloak.version"); - private static final String KEYCLOAK_DOCKER_IMAGE = System.getProperty("keycloak.docker.image"); private static final String TOKEN_USER_ROLES = System.getProperty("keycloak.token.user-roles", "user"); private static final String TOKEN_ADMIN_ROLES = System.getProperty("keycloak.token.admin-roles", "user,admin"); - static { - if (KEYCLOAK_USE_HTTPS) { - RestAssured.useRelaxedHTTPSValidation(); - } - } - @SuppressWarnings("resource") @Override public Map start() { - String keycloakDockerImage; - if (KEYCLOAK_DOCKER_IMAGE != null) { - keycloakDockerImage = KEYCLOAK_DOCKER_IMAGE; - } else if (KEYCLOAK_VERSION != null) { - keycloakDockerImage = "quay.io/keycloak/keycloak:" + KEYCLOAK_VERSION; - } else { - throw new ConfigurationException("Please set either 'keycloak.docker.image' or 'keycloak.version' system property"); - } - - keycloak = new GenericContainer<>(keycloakDockerImage) - .withExposedPorts(8080, 8443) - .withEnv("DB_VENDOR", "H2") - .withEnv("KEYCLOAK_USER", "admin") - .withEnv("KEYCLOAK_PASSWORD", "admin") - .waitingFor(Wait.forHttp("/auth").forPort(8080)); - + keycloak = new KeycloakContainer() + .withUseHttps(KEYCLOAK_USE_HTTPS); keycloak.start(); - if (KEYCLOAK_USE_HTTPS) { - KEYCLOAK_SERVER_URL = "https://localhost:" + keycloak.getMappedPort(8443) + "/auth"; - } else { - KEYCLOAK_SERVER_URL = "http://localhost:" + keycloak.getMappedPort(8080) + "/auth"; - } - RealmRepresentation realm = createRealm(KEYCLOAK_REALM); postRealm(realm); Map conf = new HashMap<>(); - conf.put("keycloak.url", KEYCLOAK_SERVER_URL); - conf.put("quarkus.oidc.auth-server-url", KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM); + conf.put("keycloak.url", keycloak.getServerUrl()); + conf.put("quarkus.oidc.auth-server-url", keycloak.getServerUrl() + "/realms/" + KEYCLOAK_REALM); return conf; } @@ -86,7 +55,7 @@ private static void postRealm(RealmRepresentation realm) { .contentType("application/json") .body(JsonSerialization.writeValueAsBytes(realm)) .when() - .post(KEYCLOAK_SERVER_URL + "/admin/realms").then() + .post(keycloak.getServerUrl() + "/admin/realms").then() .statusCode(201); } catch (IOException e) { throw new RuntimeException(e); @@ -130,7 +99,7 @@ private static String getAdminAccessToken() { .param("password", "admin") .param("client_id", "admin-cli") .when() - .post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token") + .post(keycloak.getServerUrl() + "/realms/master/protocol/openid-connect/token") .as(AccessTokenResponse.class).getToken(); } @@ -186,7 +155,7 @@ public static String getAccessToken(String userName) { .param("client_id", KEYCLOAK_SERVICE_CLIENT) .param("client_secret", "secret") .when() - .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") + .post(keycloak.getServerUrl() + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") .as(AccessTokenResponse.class).getToken(); } @@ -197,7 +166,7 @@ public static String getRefreshToken(String userName) { .param("client_id", KEYCLOAK_SERVICE_CLIENT) .param("client_secret", "secret") .when() - .post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") + .post(keycloak.getServerUrl() + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token") .as(AccessTokenResponse.class).getRefreshToken(); } @@ -205,7 +174,7 @@ public static String getRefreshToken(String userName) { public void stop() { createRequestSpec().auth().oauth2(getAdminAccessToken()) .when() - .delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); + .delete(keycloak.getServerUrl() + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204); keycloak.stop(); } From 81d9000cf5cd1c6a6c1b2c076c7d8f35485580a8 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Tue, 24 Oct 2023 23:00:50 +0200 Subject: [PATCH 2/3] Bump io.strimzi:kafka-oauth-client from 0.12.0 to 0.14.0 --- bom/application/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 47c9e6f40f370..e1d0d7083e187 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -218,7 +218,7 @@ 2.4.0 6.6.1.202309021850-r - 0.12.0 + 0.14.0 9.34 0.0.6 0.1.3 From abdfa22776aff0664c2cb5eaa68888da5e4eb7aa Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Tue, 24 Oct 2023 11:03:42 +0200 Subject: [PATCH 3/3] Strimzi OAuth substitutions --- bom/application/pom.xml | 5 ++ docs/src/main/asciidoc/kafka.adoc | 7 ++ .../client/deployment/KafkaProcessor.java | 26 ------ .../deployment/StrimziOAuthProcessor.java | 38 ++++++++ extensions/kafka-client/runtime/pom.xml | 5 ++ .../kafka/graal/StrimziSubstitutions.java | 89 +++++++++++++++++++ .../kafka-oauth-keycloak/pom.xml | 16 +++- 7 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/StrimziOAuthProcessor.java create mode 100644 extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index e1d0d7083e187..b3224a6b21d75 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -4256,6 +4256,11 @@ kafka-oauth-client ${strimzi-oauth.version} + + io.strimzi + kafka-oauth-common + ${strimzi-oauth.version} + io.strimzi strimzi-test-container diff --git a/docs/src/main/asciidoc/kafka.adoc b/docs/src/main/asciidoc/kafka.adoc index e3782c07e51ae..819d70d996dbe 100644 --- a/docs/src/main/asciidoc/kafka.adoc +++ b/docs/src/main/asciidoc/kafka.adoc @@ -1971,12 +1971,19 @@ First, add the following dependency to your application: io.strimzi kafka-oauth-client + + + io.strimzi + kafka-oauth-common + ---- [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] .build.gradle ---- implementation("io.strimzi:kafka-oauth-client") +// if compiling to native you'd need also the following dependency +implementation("io.strimzi:kafka-oauth-common") ---- This dependency provides the callback handler required to handle the OAuth workflow. diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java index 625b9da04fced..3cbffb9edac47 100644 --- a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/KafkaProcessor.java @@ -277,7 +277,6 @@ public void build( handleAvro(reflectiveClass, proxies, serviceProviders, sslNativeSupport, capabilities); handleOpenTracing(reflectiveClass, capabilities); - handleStrimziOAuth(curateOutcomeBuildItem, reflectiveClass); } @@ -330,31 +329,6 @@ private void handleOpenTracing(BuildProducer reflectiv .build()); } - private void handleStrimziOAuth(CurateOutcomeBuildItem curateOutcomeBuildItem, - BuildProducer reflectiveClass) { - if (!QuarkusClassLoader.isClassPresentAtRuntime("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler")) { - return; - } - - reflectiveClass - .produce(ReflectiveClassBuildItem.builder("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler") - .methods().fields().build()); - - if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().anyMatch( - x -> x.getGroupId().equals("org.keycloak") && x.getArtifactId().equals("keycloak-core"))) { - reflectiveClass.produce(ReflectiveClassBuildItem.builder("org.keycloak.jose.jws.JWSHeader", - "org.keycloak.representations.AccessToken", - "org.keycloak.representations.AccessToken$Access", - "org.keycloak.representations.AccessTokenResponse", - "org.keycloak.representations.IDToken", - "org.keycloak.representations.JsonWebToken", - "org.keycloak.jose.jwk.JSONWebKeySet", - "org.keycloak.jose.jwk.JWK", - "org.keycloak.json.StringOrArrayDeserializer", - "org.keycloak.json.StringListMapDeserializer").methods().fields().build()); - } - } - private void handleAvro(BuildProducer reflectiveClass, BuildProducer proxies, BuildProducer serviceProviders, diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/StrimziOAuthProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/StrimziOAuthProcessor.java new file mode 100644 index 0000000000000..ad5638a9c813b --- /dev/null +++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/StrimziOAuthProcessor.java @@ -0,0 +1,38 @@ +package io.quarkus.kafka.client.deployment; + +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; + +public class StrimziOAuthProcessor { + + @BuildStep + public void handleStrimziOAuth(CurateOutcomeBuildItem curateOutcomeBuildItem, + BuildProducer reflectiveClass) { + if (!QuarkusClassLoader.isClassPresentAtRuntime("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler")) { + return; + } + + reflectiveClass + .produce(ReflectiveClassBuildItem.builder( + "io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler") + .methods().fields().build()); + + if (curateOutcomeBuildItem.getApplicationModel().getDependencies().stream().anyMatch( + x -> x.getGroupId().equals("org.keycloak") && x.getArtifactId().equals("keycloak-core"))) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder("org.keycloak.jose.jws.JWSHeader", + "org.keycloak.representations.AccessToken", + "org.keycloak.representations.AccessToken$Access", + "org.keycloak.representations.AccessTokenResponse", + "org.keycloak.representations.IDToken", + "org.keycloak.representations.JsonWebToken", + "org.keycloak.jose.jwk.JSONWebKeySet", + "org.keycloak.jose.jwk.JWK", + "org.keycloak.json.StringOrArrayDeserializer", + "org.keycloak.json.StringListMapDeserializer").methods().fields().build()); + } + } + +} diff --git a/extensions/kafka-client/runtime/pom.xml b/extensions/kafka-client/runtime/pom.xml index ab884b3a9d3a3..547de810ee1e3 100644 --- a/extensions/kafka-client/runtime/pom.xml +++ b/extensions/kafka-client/runtime/pom.xml @@ -37,6 +37,11 @@ quarkus-kubernetes-service-binding true + + io.strimzi + kafka-oauth-common + provided + io.quarkus diff --git a/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java b/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java new file mode 100644 index 0000000000000..e556ed94f2aa2 --- /dev/null +++ b/extensions/kafka-client/runtime/src/main/java/io/smallrye/reactive/kafka/graal/StrimziSubstitutions.java @@ -0,0 +1,89 @@ +package io.smallrye.reactive.kafka.graal; + +import java.util.function.BooleanSupplier; + +import com.jayway.jsonpath.Predicate; +import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import com.jayway.jsonpath.spi.mapper.MappingProvider; +import com.oracle.svm.core.annotate.Alias; +import com.oracle.svm.core.annotate.RecomputeFieldValue; +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; + +final class HasStrimzi implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + try { + KafkaSubstitutions.class.getClassLoader() + .loadClass("io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler"); + return true; + } catch (Exception e) { + return false; + } + } +} + +@TargetClass(className = "com.jayway.jsonpath.internal.filter.ValueNodes", innerClass = "JsonNode", onlyWith = HasStrimzi.class) +final class Target_com_jayway_jsonpath_internal_filter_ValueNodes_JsonNode { + @Alias + private Object json; + @Alias + private boolean parsed; + + @Substitute + public Object parse(Predicate.PredicateContext ctx) { + try { + return parsed ? json : new JacksonJsonProvider().parse(json.toString()); + } catch (Throwable e) { + throw new IllegalArgumentException(e); + } + } +} + +@TargetClass(className = "com.jayway.jsonpath.internal.filter.ValueNode", onlyWith = HasStrimzi.class) +final class Target_com_jayway_jsonpath_internal_filter_ValueNode { + + @Substitute + private static boolean isJson(Object o) { + if (o == null || !(o instanceof String)) { + return false; + } + String str = o.toString().trim(); + if (str.length() <= 1) { + return false; + } + char c0 = str.charAt(0); + char c1 = str.charAt(str.length() - 1); + if ((c0 == '[' && c1 == ']') || (c0 == '{' && c1 == '}')) { + try { + new JacksonJsonProvider().parse(str); + return true; + } catch (Exception e) { + return false; + } + } + return false; + } +} + +@TargetClass(className = "com.jayway.jsonpath.internal.DefaultsImpl", onlyWith = HasStrimzi.class) +final class Target_com_jayway_jsonpath_internal_DefaultsImpl { + + @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FromAlias) + @Alias + public static Target_com_jayway_jsonpath_internal_DefaultsImpl INSTANCE = new Target_com_jayway_jsonpath_internal_DefaultsImpl(); + + @Substitute + public JsonProvider jsonProvider() { + return new JacksonJsonNodeJsonProvider(); + } + + @Substitute + public MappingProvider mappingProvider() { + return new JacksonMappingProvider(); + } +} diff --git a/integration-tests/kafka-oauth-keycloak/pom.xml b/integration-tests/kafka-oauth-keycloak/pom.xml index ca50ff4070d1a..9e3fd77b760b4 100644 --- a/integration-tests/kafka-oauth-keycloak/pom.xml +++ b/integration-tests/kafka-oauth-keycloak/pom.xml @@ -44,6 +44,11 @@ io.strimzi kafka-oauth-client + + + io.strimzi + kafka-oauth-common + @@ -71,6 +76,11 @@ awaitility test + + io.quarkus + quarkus-test-keycloak-server + test + @@ -174,7 +184,8 @@ false - target/hosts + ${keycloak.docker.image} + ${project.basedir}/src/test/resources/keycloak/realms/kafka-authz-realm.json @@ -183,7 +194,8 @@ false - -Djdk.net.hosts.file=${basedir}/target/hosts + ${keycloak.docker.image} + ${project.basedir}/src/test/resources/keycloak/realms/kafka-authz-realm.json