diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d709d6ea2d3..2b4bdb1f826b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -814,6 +814,41 @@ jobs: with: job-id: clustering-integration-tests + clustering-integration-tests-mtls: + name: Clustering IT (mTLS) + needs: build + runs-on: ubuntu-latest + timeout-minutes: 35 + env: + MAVEN_OPTS: -Xmx1536m + steps: + - uses: actions/checkout@v4 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + - name: Run cluster tests with mtls + run: | + ./mvnw test ${{ env.SUREFIRE_RETRY }} -Pauth-server-cluster-quarkus,db-postgres "-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" -Dsession.cache.owners=2 -Dtest=RealmInvalidationClusterTest -Dauth.server.jgroups.mtls=true -pl testsuite/integration-arquillian/tests/base + + - name: Upload JVM Heapdumps + if: always() + uses: ./.github/actions/upload-heapdumps + + - uses: ./.github/actions/upload-flaky-tests + name: Upload flaky tests + env: + GH_TOKEN: ${{ github.token }} + with: + job-name: Clustering IT (mTLS) + + - name: Surefire reports + if: always() + uses: ./.github/actions/archive-surefire-reports + with: + job-id: clustering-integration-tests-mtls + fips-unit-tests: name: FIPS UT runs-on: ubuntu-latest diff --git a/docs/guides/server/caching.adoc b/docs/guides/server/caching.adoc index b90d40004458..c2a7b93df532 100644 --- a/docs/guides/server/caching.adoc +++ b/docs/guides/server/caching.adoc @@ -304,6 +304,18 @@ It requires a keystore with the certificate to use: `cache-embedded-mtls-key-sto The truststore contains the valid certificates to accept connection from, and it can be configured with `cache-embedded-mtls-trust-store-file` (path to the truststore), and `cache-embedded-mtls-trust-store-password` (password to decrypt it). To restrict unauthorized access, use a self-signed certificate for each {project_name} deployment. +[NOTE] +==== +**Zero Configuration Encryption** + +{project_name} offers a zero-configuration approach to encrypting network communication between nodes. +This feature automatically generates self-signed certificates, eliminating the need for manual certificate creation and management. +The generated certificate and associated keys are stored within the database of each {project_name} instance. + +To enable zero-configuration TLS encryption, set the `cache-embedded-mtls-enabled` option to true. +No other `cache-embedded-mtls-*` must be set to enable the zero-configuration mode. +==== + For JGroups stacks with `UDP` or `TCP_NIO2`, see the http://jgroups.org/manual5/index.html#ENCRYPT[JGroups Encryption documentation] on how to set up the protocol stack. For more information about securing cache communication, see the {infinispan_embedding_docs}#secure-cluster-transport[Encrypting cluster transport] documentation. diff --git a/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/JpaServerConfigStorageProvider.java b/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/JpaServerConfigStorageProvider.java new file mode 100644 index 000000000000..3d1b51470c78 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/JpaServerConfigStorageProvider.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.storage.configuration.jpa; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.LockModeType; +import org.keycloak.storage.configuration.ServerConfigStorageProvider; +import org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity; + +/** + * A {@link ServerConfigStorageProvider} that stores its data in the database, using the {@link EntityManager}. + */ +public class JpaServerConfigStorageProvider implements ServerConfigStorageProvider { + + private final EntityManager entityManager; + + public JpaServerConfigStorageProvider(EntityManager entityManager) { + this.entityManager = Objects.requireNonNull(entityManager); + } + + @Override + public Optional find(String key) { + return Optional.ofNullable(getEntity(key, LockModeType.READ)) + .map(ServerConfigEntity::getValue); + } + + @Override + public void store(String key, String value) { + var entity = getEntity(key, LockModeType.WRITE); + if (entity == null) { + entity = new ServerConfigEntity(); + entity.setKey(Objects.requireNonNull(key)); + entity.setValue(Objects.requireNonNull(value)); + entityManager.persist(entity); + return; + } + entity.setValue(Objects.requireNonNull(value)); + entityManager.merge(entity); + } + + @Override + public void remove(String key) { + var entity = getEntity(key, LockModeType.WRITE); + if (entity != null) { + entityManager.remove(entity); + } + } + + @Override + public String loadOrCreate(String key, Supplier valueGenerator) { + var entity = getEntity(key, LockModeType.WRITE); + if (entity != null) { + return entity.getValue(); + } + var value = Objects.requireNonNull(valueGenerator.get()); + entity = new ServerConfigEntity(); + entity.setKey(Objects.requireNonNull(key)); + entity.setValue(value); + entityManager.persist(entity); + return value; + } + + @Override + public void close() { + //no-op + } + + private ServerConfigEntity getEntity(String key, LockModeType lockModeType) { + return entityManager.find(ServerConfigEntity.class, Objects.requireNonNull(key), lockModeType); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/JpaServerConfigStorageProviderFactory.java b/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/JpaServerConfigStorageProviderFactory.java new file mode 100644 index 000000000000..94435399aa51 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/JpaServerConfigStorageProviderFactory.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.storage.configuration.jpa; + +import java.util.Set; + +import jakarta.persistence.EntityManager; +import org.keycloak.Config; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.Provider; +import org.keycloak.storage.configuration.ServerConfigStorageProviderFactory; + +/** + * A {@link ServerConfigStorageProviderFactory} that instantiates {@link JpaServerConfigStorageProvider}. + */ +public class JpaServerConfigStorageProviderFactory implements ServerConfigStorageProviderFactory { + + @Override + public JpaServerConfigStorageProvider create(KeycloakSession session) { + return new JpaServerConfigStorageProvider(getEntityManager(session)); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "jpa"; + } + + @Override + public Set> dependsOn() { + return Set.of(JpaConnectionProvider.class); + } + + private static EntityManager getEntityManager(KeycloakSession session) { + return session.getProvider(JpaConnectionProvider.class).getEntityManager(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/entity/ServerConfigEntity.java b/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/entity/ServerConfigEntity.java new file mode 100644 index 000000000000..83433b40e03b --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/storage/configuration/jpa/entity/ServerConfigEntity.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.storage.configuration.jpa.entity; + +import java.util.Objects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +/** + * A JPA entity to store the key-value configuration. + */ +@SuppressWarnings("unused") +@Table(name = "SERVER_CONFIG") +@Entity +public class ServerConfigEntity { + + @Id + @Column(name = "SERVER_CONFIG_KEY") + private String key; + + @Column(name = "VALUE") + private String value; + + @Version + @Column(name = "VERSION") + private int version; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + ServerConfigEntity that = (ServerConfigEntity) o; + return version == that.version && Objects.equals(key, that.key) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(key); + result = 31 * result + Objects.hashCode(value); + result = 31 * result + version; + return result; + } +} diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.2.0.xml new file mode 100644 index 000000000000..85d0d49b9e56 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.2.0.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 159c233af05a..6f29bfe1d48a 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -85,5 +85,6 @@ + diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.storage.configuration.ServerConfigStorageProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.storage.configuration.ServerConfigStorageProviderFactory new file mode 100644 index 000000000000..3be985df64e4 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.storage.configuration.ServerConfigStorageProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2025 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.keycloak.storage.configuration.jpa.JpaServerConfigStorageProviderFactory diff --git a/model/jpa/src/main/resources/default-persistence.xml b/model/jpa/src/main/resources/default-persistence.xml index a1fcb1ae664a..40ff6d60a2c3 100644 --- a/model/jpa/src/main/resources/default-persistence.xml +++ b/model/jpa/src/main/resources/default-persistence.xml @@ -89,6 +89,9 @@ org.keycloak.models.jpa.entities.OrganizationEntity org.keycloak.models.jpa.entities.OrganizationDomainEntity + + org.keycloak.storage.configuration.jpa.entity.ServerConfigEntity + true diff --git a/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigStorageProvider.java b/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigStorageProvider.java new file mode 100644 index 000000000000..eeb0d84eb213 --- /dev/null +++ b/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigStorageProvider.java @@ -0,0 +1,72 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.storage.configuration; + +import java.util.Optional; +import java.util.function.Supplier; + +import org.keycloak.provider.Provider; + +/** + * A {@link Provider} to store server configuration to be shared between the Keycloak instances. + *

+ * This provider is a key-value store where both keys and values are {@link String}. + */ +public interface ServerConfigStorageProvider extends Provider { + + /** + * Returns the value to which the specified {@code key}. + * + * @param key The {@code key} whose associated value is to be returned. + * @return The value from the specified {@code key}. + * @throws NullPointerException if the specified {@code key} is {@code null}. + */ + Optional find(String key); + + /** + * Stores the specified {@code value} with the specified {@code key}. + *

+ * If the {@code key} exists, its value is updated. + * + * @param key The {@code key} with which the specified {@code value} is to be stored. + * @param value The {@code value} to be associated with the specified {@code key}. + * @throws NullPointerException if the specified {@code key} or {@code value} is {@code null}. + */ + void store(String key, String value); + + /** + * Removes the {@code value} specified by the {@code key}. + * + * @param key The {@code key} whose value is to be removed. + * @throws NullPointerException if the specified {@code key} is {@code null}. + */ + void remove(String key); + + /** + * Returns the value to which the specified {@code key} or, if not found, stores the value returned by the + * {@code valueGenerator}. + * + * @param key The {@code key} whose associated value is to be returned or stored. + * @param valueGenerator The {@link Supplier} to generate the value if it is not found. + * @return The {value stored by the {@code key}, or the value generated by the {@link Supplier}. + * @throws NullPointerException if the specified {@code key}, {@code valueGenerator} or {@link Supplier} return + * value is {@code null}. + */ + String loadOrCreate(String key, Supplier valueGenerator); + +} diff --git a/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigStorageProviderFactory.java b/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigStorageProviderFactory.java new file mode 100644 index 000000000000..e8a907fbc446 --- /dev/null +++ b/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigStorageProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.storage.configuration; + +import org.keycloak.provider.ProviderFactory; + +/** + * A {@link ProviderFactory} to create instances of {@link ServerConfigStorageProvider} + */ +public interface ServerConfigStorageProviderFactory extends ProviderFactory { +} diff --git a/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigurationStorageProviderSpi.java b/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigurationStorageProviderSpi.java new file mode 100644 index 000000000000..1b7af725cdf1 --- /dev/null +++ b/model/storage/src/main/java/org/keycloak/storage/configuration/ServerConfigurationStorageProviderSpi.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.storage.configuration; + +import org.keycloak.provider.Spi; + +/** + * The {@link Spi} implementation of {@link ServerConfigStorageProvider}. + */ +public class ServerConfigurationStorageProviderSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "serverConfig"; + } + + @Override + public Class getProviderClass() { + return ServerConfigStorageProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ServerConfigStorageProviderFactory.class; + } +} diff --git a/model/storage/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/storage/src/main/resources/META-INF/services/org.keycloak.provider.Spi index ce5ebe63eba6..6310544f2b50 100644 --- a/model/storage/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/storage/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -17,3 +17,4 @@ org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.federated.UserFederatedStorageProviderSpi +org.keycloak.storage.configuration.ServerConfigurationStorageProviderSpi \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java index aea9e4936c32..ed8282976e4d 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java @@ -14,6 +14,7 @@ import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.configuration.Configuration; import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; @@ -56,17 +57,25 @@ public static PropertyMapper[] getClusteringPropertyMappers() { .build(), fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE.withRuntimeSpecificDefault(getDefaultKeystorePathValue())) .paramLabel("file") + .isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey())) + .validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD)) .build(), fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD) .paramLabel("password") .isMasked(true) + .isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey())) + .validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE)) .build(), fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE.withRuntimeSpecificDefault(getDefaultTruststorePathValue())) .paramLabel("file") + .isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey())) + .validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD)) .build(), fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD) .paramLabel("password") .isMasked(true) + .isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled.".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey())) + .validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE)) .build(), fromOption(CachingOptions.CACHE_REMOTE_HOST) .paramLabel("hostname") @@ -170,4 +179,11 @@ private static void validateCachingOptionIsPresent(Option optionSet, Option option, Option requiredOption) { + if (getOptionalKcValue(requiredOption).isPresent()) { + return; + } + throw new PropertyException("The option '%s' requires '%s' to be enabled.".formatted(option.getKey(), requiredOption.getKey())); + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java index 5e9c09046810..de4bf1c6395b 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java @@ -20,7 +20,6 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -28,15 +27,11 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; -import io.agroal.api.AgroalDataSource; import io.micrometer.core.instrument.Metrics; -import io.quarkus.arc.Arc; -import jakarta.persistence.EntityManager; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCacheManager; import org.infinispan.client.hotrod.RemoteCacheManagerAdmin; @@ -48,7 +43,6 @@ import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.cache.HashConfiguration; import org.infinispan.configuration.cache.PersistenceConfigurationBuilder; -import org.infinispan.configuration.global.GlobalConfiguration; import org.infinispan.configuration.global.ShutdownHookBehavior; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.configuration.parsing.ParserRegistry; @@ -59,23 +53,13 @@ import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; import org.infinispan.protostream.descriptors.FileDescriptor; import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants; -import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator; -import org.infinispan.remoting.transport.jgroups.JGroupsTransport; import org.jboss.logging.Logger; -import org.jgroups.conf.ProtocolConfiguration; -import org.jgroups.protocols.JDBC_PING2; -import org.jgroups.protocols.TCP_NIO2; -import org.jgroups.protocols.UDP; -import org.jgroups.util.TLS; -import org.jgroups.util.TLSClientAuth; import org.keycloak.common.Profile; import org.keycloak.common.util.MultiSiteUtils; import org.keycloak.config.CachingOptions; import org.keycloak.config.MetricsOptions; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanUtil; -import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.marshalling.KeycloakIndexSchemaUtil; import org.keycloak.marshalling.KeycloakModelSchema; @@ -86,15 +70,10 @@ import org.keycloak.models.sessions.infinispan.remote.RemoteInfinispanAuthenticationSessionProviderFactory; import org.keycloak.models.sessions.infinispan.remote.RemoteUserLoginFailureProviderFactory; import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsConfigurator; import javax.net.ssl.SSLContext; -import javax.sql.DataSource; -import static org.infinispan.configuration.global.TransportConfiguration.STACK; -import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY; -import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY; -import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY; -import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_HOST_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY; import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PORT_PROPERTY; @@ -114,26 +93,25 @@ public class CacheManagerFactory { - private static final Logger logger = Logger.getLogger(CacheManagerFactory.class); + public static final Logger logger = Logger.getLogger(CacheManagerFactory.class); // Map with the default cache configuration if the cache is not present in the XML. private static final Map> DEFAULT_CONFIGS = Map.of( CRL_CACHE_NAME, InfinispanUtil::getCrlCacheConfig ); private static final Supplier TO_NULL = () -> null; - private final CompletableFuture cacheManagerFuture; + private volatile CompletableFuture cacheManagerFuture; private final CompletableFuture remoteCacheManagerFuture; - private final Function jdbcCacheManagerFunction; - private volatile EmbeddedCacheManager cacheManager; + private final JGroupsConfigurator jGroupsConfigurator; public CacheManagerFactory(String config) { ConfigurationBuilderHolder builder = new ParserRegistry().parse(config); - if (!isJdbcPingRequired(builder)) { - cacheManagerFuture = CompletableFuture.supplyAsync(() -> startEmbeddedCacheManager(builder, null)); - jdbcCacheManagerFunction = null; - } else { + jGroupsConfigurator = JGroupsConfigurator.create(builder); + + if (jGroupsConfigurator.requiresKeycloakSession()) { cacheManagerFuture = null; - jdbcCacheManagerFunction = em -> startEmbeddedCacheManager(builder, em); + } else { + cacheManagerFuture = CompletableFuture.supplyAsync(() -> startEmbeddedCacheManager(null)); } if (InfinispanUtils.isRemoteInfinispan()) { @@ -145,35 +123,18 @@ public CacheManagerFactory(String config) { } } - private static boolean isJdbcPingRequired(ConfigurationBuilderHolder builder) { - if (InfinispanUtils.isRemoteInfinispan()) - return false; - - var transportConfig = builder.getGlobalConfigurationBuilder().transport(); - if (transportConfig.getTransport() == null) - return false; - - String transportStack = Configuration.getRawValue("kc.cache-stack"); - if (transportStack != null && !isJdbcPingStack(transportStack)) - return false; - - var stackXmlAttribute = transportConfig.defaultTransport().attributes().attribute(STACK); - return !stackXmlAttribute.isModified() || isJdbcPingStack(stackXmlAttribute.get()); - } - public EmbeddedCacheManager getOrCreateEmbeddedCacheManager(KeycloakSession keycloakSession) { if (cacheManagerFuture != null) return join(cacheManagerFuture); - if (cacheManager == null) { + if (cacheManagerFuture == null) { synchronized (this) { - if (cacheManager == null) { - EntityManager em = keycloakSession.getProvider(JpaConnectionProvider.class).getEntityManager(); - cacheManager = jdbcCacheManagerFunction.apply(em); + if (cacheManagerFuture == null) { + cacheManagerFuture = CompletableFuture.completedFuture(startEmbeddedCacheManager(keycloakSession)); } } } - return cacheManager; + return join(cacheManagerFuture); } public RemoteCacheManager getOrCreateRemoteCacheManager() { @@ -327,24 +288,14 @@ private static void updateSchemaAndReIndexCache(RemoteCacheManagerAdmin admin, S admin.reindexCache(cacheName); } - private EmbeddedCacheManager startEmbeddedCacheManager(ConfigurationBuilderHolder builder, EntityManager em) { + private EmbeddedCacheManager startEmbeddedCacheManager(KeycloakSession session) { logger.info("Starting Infinispan embedded cache manager"); + var builder = jGroupsConfigurator.holder(); // We must disable the Infinispan default ShutdownHook as we manage the EmbeddedCacheManager lifecycle explicitly // with #shutdown and multiple calls to EmbeddedCacheManager#stop can lead to Exceptions being thrown builder.getGlobalConfigurationBuilder().shutdown().hookBehavior(ShutdownHookBehavior.DONT_REGISTER); - if (Configuration.isTrue(MetricsOptions.METRICS_ENABLED)) { - builder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class); - builder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); - builder.getGlobalConfigurationBuilder().cacheContainer().statistics(true); - builder.getGlobalConfigurationBuilder().metrics().namesAsTags(true); - if (Configuration.isTrue(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)) { - builder.getGlobalConfigurationBuilder().metrics().histograms(true); - } - builder.getNamedConfigurationBuilders().forEach((s, configurationBuilder) -> configurationBuilder.statistics().enabled(true)); - } - Marshalling.configure(builder.getGlobalConfigurationBuilder()); assertAllCachesAreConfigured(builder, // skip revision caches, those are defined by DefaultInfinispanConnectionProviderFactory @@ -370,19 +321,36 @@ private EmbeddedCacheManager startEmbeddedCacheManager(ConfigurationBuilderHolde // embedded mode! assertAllCachesAreConfigured(builder, Arrays.stream(CLUSTERED_CACHE_NAMES)); if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) { - configureTransportStack(builder, em); + if (jGroupsConfigurator.isLocal()) { + throw new RuntimeException("Unable to use clustered cache with local mode."); + } configureRemoteStores(builder); } + jGroupsConfigurator.configure(session); configureCacheMaxCount(builder, CachingOptions.CLUSTERED_MAX_COUNT_CACHES); configureSessionsCaches(builder); validateWorkCacheConfiguration(builder); } configureCacheMaxCount(builder, CachingOptions.LOCAL_MAX_COUNT_CACHES); checkForRemoteStores(builder); + configureMetrics(builder); return new DefaultCacheManager(builder, isStartEagerly()); } + private static void configureMetrics(ConfigurationBuilderHolder holder) { + if (Configuration.isTrue(MetricsOptions.METRICS_ENABLED)) { + holder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class); + holder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); + holder.getGlobalConfigurationBuilder().cacheContainer().statistics(true); + holder.getGlobalConfigurationBuilder().metrics().namesAsTags(true); + if (Configuration.isTrue(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)) { + holder.getGlobalConfigurationBuilder().metrics().histograms(true); + } + holder.getNamedConfigurationBuilders().forEach((s, configurationBuilder) -> configurationBuilder.statistics().enabled(true)); + } + } + private static boolean isRemoteTLSEnabled() { return Configuration.isTrue(CachingOptions.CACHE_REMOTE_TLS_ENABLED); } @@ -416,102 +384,6 @@ private static int getStartTimeout() { return Integer.getInteger("kc.cache-ispn-start-timeout", 120); } - private static void configureTransportStack(ConfigurationBuilderHolder builder, EntityManager em) { - var transportConfig = builder.getGlobalConfigurationBuilder().transport(); - if (Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED)) { - validateTlsAvailable(transportConfig.build()); - var tls = new TLS() - .enabled(true) - .setKeystorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY)) - .setKeystorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY)) - .setKeystoreType("pkcs12") - .setTruststorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY)) - .setTruststorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY)) - .setTruststoreType("pkcs12") - .setClientAuth(TLSClientAuth.NEED) - .setProtocols(new String[]{"TLSv1.3"}); - transportConfig.addProperty(JGroupsTransport.SOCKET_FACTORY, tls.createSocketFactory()); - logger.info("MTLS enabled for communications for embedded caches"); - } - - String transportStack = Configuration.getRawValue("kc.cache-stack"); - if (transportStack != null && !transportStack.isBlank() && !isJdbcPingStack(transportStack)) { - warnDeprecatedStack(transportStack); - transportConfig.defaultTransport().stack(transportStack); - return; - } - - var stackXmlAttribute = transportConfig.defaultTransport().attributes().attribute(STACK); - // If the user has explicitly defined a transport stack that is not jdbc-ping or jdbc-ping-udp, return - if (stackXmlAttribute.isModified() && !isJdbcPingStack(stackXmlAttribute.get())) { - warnDeprecatedStack(stackXmlAttribute.get()); - return; - } - - var stackName = transportStack != null ? - transportStack : - stackXmlAttribute.isModified() ? stackXmlAttribute.get() : "jdbc-ping"; - warnDeprecatedStack(stackName); - - var udp = stackName.endsWith("udp"); - - var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em); - var attributes = Map.of( - // Leave initialize_sql blank as table is already created by Keycloak - "initialize_sql", "", - // Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default - // "cluster" cannot be used with Oracle DB as it's a reserved word. - "clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName), - "delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName), - "insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName), - "select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName), - "remove_all_data_on_view_change", "true", - "register_shutdown_hook", "false", - "stack.combine", "REPLACE", - "stack.position", udp ? "PING" : "MPING" - ); - var stack = List.of(new ProtocolConfiguration(JDBC_PING2.class.getSimpleName(), attributes)); - builder.addJGroupsStack(new EmbeddedJGroupsChannelConfigurator(stackName, stack, null), udp ? "udp" : "tcp"); - - Supplier dataSourceSupplier = Arc.container().select(AgroalDataSource.class)::get; - transportConfig.addProperty(JGroupsTransport.DATA_SOURCE, dataSourceSupplier); - transportConfig.defaultTransport().stack(stackName); - } - - private static void warnDeprecatedStack(String stackName) { - switch (stackName) { - case "jdbc-ping-udp": - case "tcp": - case "udp": - case "azure": - case "ec2": - case "google": - Logger.getLogger(CacheManagerFactory.class).warnf("Stack '%s' is deprecated. We recommend to use 'jdbc-ping' instead", stackName); - } - } - - private static boolean isJdbcPingStack(String stackName) { - return "jdbc-ping".equals(stackName) || "jdbc-ping-udp".equals(stackName); - } - - private static void validateTlsAvailable(GlobalConfiguration config) { - var stackName = config.transport().stack(); - if (stackName == null) { - // unable to validate - return; - } - for (var protocol : config.transport().jgroups().configurator(stackName).getProtocolStack()) { - var name = protocol.getProtocolName(); - if (name.equals(UDP.class.getSimpleName()) || - name.equals(UDP.class.getName()) || - name.equals(TCP_NIO2.class.getSimpleName()) || - name.equals(TCP_NIO2.class.getName())) { - throw new RuntimeException("Cache TLS is not available with protocol " + name); - } - } - - } - private static void configureRemoteStores(ConfigurationBuilderHolder builder) { //if one of remote store command line parameters is defined, some other are required, otherwise assume it'd configured via xml only if (Configuration.getOptionalKcValue(CACHE_REMOTE_HOST_PROPERTY).isPresent()) { @@ -655,7 +527,7 @@ private static void validateWorkCacheConfiguration(ConfigurationBuilderHolder bu } } - private static String requiredStringProperty(String propertyName) { + public static String requiredStringProperty(String propertyName) { return Configuration.getOptionalKcValue(propertyName).orElseThrow(() -> new RuntimeException("Property " + propertyName + " required but not specified")); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsConfigurator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsConfigurator.java new file mode 100644 index 000000000000..aca2b42323b5 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsConfigurator.java @@ -0,0 +1,131 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups; + +import java.util.ArrayList; +import java.util.List; + +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.keycloak.config.CachingOptions; +import org.keycloak.infinispan.util.InfinispanUtils; +import org.keycloak.models.KeycloakSession; +import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl.FileJGroupsTlsConfigurator; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl.JGroupsJdbcPingStackConfigurator; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl.JpaJGroupsTlsConfigurator; + +/** + * Configures the JGroups stacks before starting Infinispan. + */ +public class JGroupsConfigurator { + + private final ConfigurationBuilderHolder holder; + private final List stackConfiguratorList; + + private JGroupsConfigurator(ConfigurationBuilderHolder holder, List stackConfiguratorList) { + this.holder = holder; + this.stackConfiguratorList = stackConfiguratorList; + } + + private static void createJdbcPingConfigurator(ConfigurationBuilderHolder holder, List configurator) { + var stackXmlAttribute = JGroupsUtil.transportStackOf(holder); + if (stackXmlAttribute.isModified() && !isJdbcPingStack(stackXmlAttribute.get())) { + CacheManagerFactory.logger.debugf("Custom stack configured (%s). JDBC_PING discovery disabled.", stackXmlAttribute.get()); + return; + } + CacheManagerFactory.logger.debug("JDBC_PING discovery enabled."); + if (!stackXmlAttribute.isModified()) { + // defaults to jdbc-ping + JGroupsUtil.transportOf(holder).stack("jdbc-ping"); + } + configurator.add(JGroupsJdbcPingStackConfigurator.INSTANCE); + } + + private static boolean isJdbcPingStack(String stackName) { + return "jdbc-ping".equals(stackName) || "jdbc-ping-udp".equals(stackName); + } + + private static void createTlsConfigurator(List configurator) { + if (!Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED)) { + CacheManagerFactory.logger.debug("JGroups encryption disabled."); + return; + } + + if (Configuration.isBlank(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE) && Configuration.isBlank(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE)) { + CacheManagerFactory.logger.debug("JGroups encryption enabled. Neither KeyStore and Truststore present, using the certificates from database."); + configurator.add(JpaJGroupsTlsConfigurator.INSTANCE); + return; + } + CacheManagerFactory.logger.debug("JGroups encryption enabled. KeyStore or Truststore present."); + configurator.add(FileJGroupsTlsConfigurator.INSTANCE); + } + + private static boolean isLocal(ConfigurationBuilderHolder holder) { + return JGroupsUtil.transportOf(holder) == null; + } + + public static JGroupsConfigurator create(ConfigurationBuilderHolder holder) { + if (InfinispanUtils.isRemoteInfinispan() || isLocal(holder)) { + CacheManagerFactory.logger.debug("Multi Site or local mode. Skipping JGroups configuration."); + return new JGroupsConfigurator(holder, List.of()); + } + // Configure stack from CLI options to Global Configuration + Configuration.getOptionalKcValue(CachingOptions.CACHE_STACK).ifPresent(JGroupsUtil.transportOf(holder)::stack); + var configurator = new ArrayList(2); + createJdbcPingConfigurator(holder, configurator); + createTlsConfigurator(configurator); + return new JGroupsConfigurator(holder, List.copyOf(configurator)); + } + + /** + * @return The {@link ConfigurationBuilderHolder} with the current Infinispan configuration. + */ + public ConfigurationBuilderHolder holder() { + return holder; + } + + /** + * @return {@code true} if it requires a {@link KeycloakSession} to perform the stack configuration. + */ + public boolean requiresKeycloakSession() { + return stackConfiguratorList.stream().anyMatch(JGroupsStackConfigurator::requiresKeycloakSession); + } + + /** + * @return {@code true} if Keycloak is run in local mode (development mode for example) and JGroups won't be used. + */ + public boolean isLocal() { + return isLocal(holder); + } + + /** + * Configures the JGroups stack. + * + * @param session The {@link KeycloakSession}. It is {@code null} when {@link #requiresKeycloakSession()} returns + * {@code false}. + */ + public void configure(KeycloakSession session) { + if (InfinispanUtils.isRemoteInfinispan() || isLocal()) { + return; + } + stackConfiguratorList.forEach(jGroupsStackConfigurator -> jGroupsStackConfigurator.configure(holder, session)); + JGroupsUtil.warnDeprecatedStack(holder); + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsStackConfigurator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsStackConfigurator.java new file mode 100644 index 000000000000..2d2f6cb86ffe --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsStackConfigurator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups; + +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.keycloak.models.KeycloakSession; + +/** + * Interface to configure a JGroups Stack before Keycloak starts the embedded Infinispan. + */ +public interface JGroupsStackConfigurator { + + /** + * @return {@code true} if this configuration requires the sessions, for example, to access a database. + */ + boolean requiresKeycloakSession(); + + /** + * Configures the stack in {@code holder}. + *

+ * The {@code session} is not {@code null} when {@link #requiresKeycloakSession()} returns {@code true}. + * + * @param holder The Infinispan {@link ConfigurationBuilderHolder}. + * @param session The current {@link KeycloakSession}. It may be {@code null}; + */ + void configure(ConfigurationBuilderHolder holder, KeycloakSession session); + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsUtil.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsUtil.java new file mode 100644 index 000000000000..91533014a87f --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/JGroupsUtil.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups; + +import org.infinispan.commons.configuration.attributes.Attribute; +import org.infinispan.configuration.global.TransportConfigurationBuilder; +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.jgroups.protocols.TCP_NIO2; +import org.jgroups.protocols.UDP; +import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory; + +import static org.infinispan.configuration.global.TransportConfiguration.STACK; + +public final class JGroupsUtil { + + private JGroupsUtil() { + } + + public static TransportConfigurationBuilder transportOf(ConfigurationBuilderHolder holder) { + return holder.getGlobalConfigurationBuilder().transport(); + } + + public static Attribute transportStackOf(ConfigurationBuilderHolder holder) { + var transport = transportOf(holder); + assert transport != null; + return transport.attributes().attribute(STACK); + } + + public static void warnDeprecatedStack(ConfigurationBuilderHolder holder) { + var stackName = transportStackOf(holder).get(); + switch (stackName) { + case "jdbc-ping-udp": + case "tcp": + case "udp": + case "azure": + case "ec2": + case "google": + CacheManagerFactory.logger.warnf("Stack '%s' is deprecated. We recommend to use 'jdbc-ping' instead", stackName); + } + } + + public static void validateTlsAvailable(ConfigurationBuilderHolder holder) { + var stackName = transportStackOf(holder).get(); + if (stackName == null) { + // unable to validate + return; + } + var config = transportOf(holder).build(); + for (var protocol : config.transport().jgroups().configurator(stackName).getProtocolStack()) { + var name = protocol.getProtocolName(); + if (name.equals(UDP.class.getSimpleName()) || + name.equals(UDP.class.getName()) || + name.equals(TCP_NIO2.class.getSimpleName()) || + name.equals(TCP_NIO2.class.getName())) { + throw new RuntimeException("Cache TLS is not available with protocol " + name); + } + } + + } + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/BaseJGroupsTlsConfigurator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/BaseJGroupsTlsConfigurator.java new file mode 100644 index 000000000000..e5067cb45f23 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/BaseJGroupsTlsConfigurator.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl; + +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; +import org.jgroups.util.SocketFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsStackConfigurator; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsUtil; + +abstract class BaseJGroupsTlsConfigurator implements JGroupsStackConfigurator { + + @Override + public void configure(ConfigurationBuilderHolder holder, KeycloakSession session) { + var factory = createSocketFactory(session); + JGroupsUtil.transportOf(holder).addProperty(JGroupsTransport.SOCKET_FACTORY, factory); + JGroupsUtil.validateTlsAvailable(holder); + CacheManagerFactory.logger.info("JGroups Encryption enabled (mTLS)."); + } + + abstract SocketFactory createSocketFactory(KeycloakSession session); +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/CertificateEntity.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/CertificateEntity.java new file mode 100644 index 000000000000..fa8a10ef954b --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/CertificateEntity.java @@ -0,0 +1,118 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl; + +import java.security.KeyPair; +import java.security.cert.X509Certificate; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.common.util.PemUtils; + +/** + * JPA entity to store the {@link X509Certificate} and {@link KeyPair}. + */ +@SuppressWarnings("unused") +public class CertificateEntity { + + @JsonProperty("prvKey") + private String privateKeyPem; + + @JsonProperty("pubKey") + private String publicKeyPem; + + @JsonProperty("crt") + private String certificatePem; + + public CertificateEntity() { + } + + public CertificateEntity(String privateKeyPem, String publicKeyPem, String certificatePem) { + this.privateKeyPem = Objects.requireNonNull(privateKeyPem); + this.publicKeyPem = Objects.requireNonNull(publicKeyPem); + this.certificatePem = Objects.requireNonNull(certificatePem); + } + + public String getCertificatePem() { + return certificatePem; + } + + public void setCertificatePem(String certificatePem) { + this.certificatePem = certificatePem; + } + + public String getPrivateKeyPem() { + return privateKeyPem; + } + + public void setPrivateKeyPem(String privateKeyPem) { + this.privateKeyPem = privateKeyPem; + } + + public String getPublicKeyPem() { + return publicKeyPem; + } + + public void setPublicKeyPem(String publicKeyPem) { + this.publicKeyPem = publicKeyPem; + } + + @JsonIgnore + public void setCertificate(X509Certificate certificate) { + Objects.requireNonNull(certificate); + setCertificatePem(PemUtils.encodeCertificate(certificate)); + } + + @JsonIgnore + public void setKeyPair(KeyPair keyPair) { + Objects.requireNonNull(keyPair); + setPrivateKeyPem(PemUtils.encodeKey(keyPair.getPrivate())); + setPublicKeyPem(PemUtils.encodeKey(keyPair.getPublic())); + } + + @JsonIgnore + public X509Certificate getCertificate() { + return PemUtils.decodeCertificate(getCertificatePem()); + } + + @JsonIgnore + public KeyPair getKeyPair() { + var prv = PemUtils.decodePrivateKey(getPrivateKeyPem()); + var pub = PemUtils.decodePublicKey(getPublicKeyPem()); + return new KeyPair(pub, prv); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + CertificateEntity that = (CertificateEntity) o; + return Objects.equals(privateKeyPem, that.privateKeyPem) && + Objects.equals(publicKeyPem, that.publicKeyPem) && + Objects.equals(certificatePem, that.certificatePem); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(privateKeyPem); + result = 31 * result + Objects.hashCode(publicKeyPem); + result = 31 * result + Objects.hashCode(certificatePem); + return result; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/FileJGroupsTlsConfigurator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/FileJGroupsTlsConfigurator.java new file mode 100644 index 000000000000..ca62ab774a52 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/FileJGroupsTlsConfigurator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl; + +import org.jgroups.util.SocketFactory; +import org.jgroups.util.TLS; +import org.jgroups.util.TLSClientAuth; +import org.keycloak.models.KeycloakSession; + +import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY; +import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY; +import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY; +import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY; +import static org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory.requiredStringProperty; + +/** + * JGroups mTLS configuration using the provided KeyStore and TrustStore files. + */ +public class FileJGroupsTlsConfigurator extends BaseJGroupsTlsConfigurator { + + public static final FileJGroupsTlsConfigurator INSTANCE = new FileJGroupsTlsConfigurator(); + + @Override + SocketFactory createSocketFactory(KeycloakSession ignored) { + var tls = new TLS() + .enabled(true) + .setKeystorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY)) + .setTruststorePath(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY)) + .setKeystorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD_PROPERTY)) + .setTruststorePassword(requiredStringProperty(CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY)) + .setKeystoreType("pkcs12") + .setTruststoreType("pkcs12") + .setClientAuth(TLSClientAuth.NEED) + .setProtocols(new String[]{"TLSv1.3"}); + return tls.createSocketFactory(); + } + + @Override + public boolean requiresKeycloakSession() { + return false; + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/JGroupsJdbcPingStackConfigurator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/JGroupsJdbcPingStackConfigurator.java new file mode 100644 index 000000000000..238bf69cdb52 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/JGroupsJdbcPingStackConfigurator.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import io.agroal.api.AgroalDataSource; +import io.quarkus.arc.Arc; +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; +import org.jgroups.conf.ProtocolConfiguration; +import org.jgroups.protocols.JDBC_PING2; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.connections.jpa.util.JpaUtils; +import org.keycloak.models.KeycloakSession; +import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsStackConfigurator; +import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsUtil; + +import javax.sql.DataSource; + +/** + * JGroups discovery configuration using {@link JDBC_PING2}. + */ +public class JGroupsJdbcPingStackConfigurator implements JGroupsStackConfigurator { + + public static final JGroupsStackConfigurator INSTANCE = new JGroupsJdbcPingStackConfigurator(); + + private JGroupsJdbcPingStackConfigurator() {} + + @Override + public boolean requiresKeycloakSession() { + return true; + } + + @Override + public void configure(ConfigurationBuilderHolder holder, KeycloakSession session) { + var em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); + var stackName = JGroupsUtil.transportStackOf(holder).get(); + var isUdp = stackName.endsWith("udp"); + var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em); + var stack = getProtocolConfigurations(tableName, isUdp ? "PING" : "MPING"); + holder.addJGroupsStack(new EmbeddedJGroupsChannelConfigurator(stackName, stack, null), isUdp ? "udp" : "tcp"); + + Supplier dataSourceSupplier = Arc.container().select(AgroalDataSource.class)::get; + JGroupsUtil.transportOf(holder).addProperty(JGroupsTransport.DATA_SOURCE, dataSourceSupplier); + JGroupsUtil.transportOf(holder).stack(stackName); + CacheManagerFactory.logger.info("JGroups JDBC_PING discovery enabled."); + } + + private static List getProtocolConfigurations(String tableName, String discoveryProtocol) { + var attributes = Map.of( + // Leave initialize_sql blank as table is already created by Keycloak + "initialize_sql", "", + // Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default + // "cluster" cannot be used with Oracle DB as it's a reserved word. + "clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName), + "delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName), + "insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName), + "select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName), + "remove_all_data_on_view_change", "true", + "register_shutdown_hook", "false", + "stack.combine", "REPLACE", + "stack.position", discoveryProtocol + ); + return List.of(new ProtocolConfiguration(JDBC_PING2.class.getSimpleName(), attributes)); + } + + +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/JpaJGroupsTlsConfigurator.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/JpaJGroupsTlsConfigurator.java new file mode 100644 index 000000000000..f89479abe142 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/jgroups/impl/JpaJGroupsTlsConfigurator.java @@ -0,0 +1,150 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.infinispan.jgroups.impl; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.cert.X509Certificate; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.jgroups.util.DefaultSocketFactory; +import org.jgroups.util.SocketFactory; +import org.keycloak.common.crypto.CryptoIntegration; +import org.keycloak.common.util.CertificateUtils; +import org.keycloak.common.util.KeyUtils; +import org.keycloak.common.util.KeystoreUtil; +import org.keycloak.common.util.Retry; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.storage.configuration.ServerConfigStorageProvider; +import org.keycloak.util.JsonSerialization; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509ExtendedTrustManager; + +/** + * JGroups mTLS configuration using certificates stored by {@link ServerConfigStorageProvider}. + */ +public class JpaJGroupsTlsConfigurator extends BaseJGroupsTlsConfigurator { + + private static final char[] KEY_PASSWORD = "jgroups-password".toCharArray(); + private static final String CERTIFICATE_ID = "crt_jgroups"; + private static final String KEYSTORE_ALIAS = "jgroups"; + private static final String JGROUPS_SUBJECT = "jgroups"; + private static final String TLS_PROTOCOL_VERSION = "TLSv1.3"; + private static final String TLS_PROTOCOL = "TLS"; + private static final int STARTUP_RETRIES = 2; + private static final int STARTUP_RETRY_SLEEP_MILLIS = 10; + public static final JpaJGroupsTlsConfigurator INSTANCE = new JpaJGroupsTlsConfigurator(); + + @Override + public boolean requiresKeycloakSession() { + return true; + } + + @Override + SocketFactory createSocketFactory(KeycloakSession session) { + var factory = session.getKeycloakSessionFactory(); + return Retry.call(iteration -> KeycloakModelUtils.runJobInTransactionWithResult(factory, this::createSocketFactoryInTransaction), STARTUP_RETRIES, STARTUP_RETRY_SLEEP_MILLIS); + } + + private SocketFactory createSocketFactoryInTransaction(KeycloakSession session) { + try { + var storage = session.getProvider(ServerConfigStorageProvider.class); + var data = fromJson(storage.loadOrCreate(CERTIFICATE_ID, JpaJGroupsTlsConfigurator::generateSelfSignedCertificate)); + var km = createKeyManager(data.getKeyPair(), data.getCertificate()); + var tm = createTrustManager(data.getCertificate()); + var sslContext = SSLContext.getInstance(TLS_PROTOCOL); + sslContext.init(new KeyManager[]{km}, new TrustManager[]{tm}, null); + return createFromContext(sslContext); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + private X509ExtendedKeyManager createKeyManager(KeyPair keyPair, X509Certificate certificate) throws GeneralSecurityException, IOException { + var ks = CryptoIntegration.getProvider().getKeyStore(KeystoreUtil.KeystoreFormat.JKS); + ks.load(null, null); + ks.setKeyEntry(KEYSTORE_ALIAS, keyPair.getPrivate(), KEY_PASSWORD, new java.security.cert.Certificate[]{certificate}); + var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, KEY_PASSWORD); + for (KeyManager km : kmf.getKeyManagers()) { + if (km instanceof X509ExtendedKeyManager) { + return (X509ExtendedKeyManager) km; + } + } + throw new GeneralSecurityException("Could not obtain an X509ExtendedKeyManager"); + } + + private X509ExtendedTrustManager createTrustManager(X509Certificate certificate) throws GeneralSecurityException, IOException { + var ks = CryptoIntegration.getProvider().getKeyStore(KeystoreUtil.KeystoreFormat.JKS); + ks.load(null, null); + ks.setCertificateEntry(KEYSTORE_ALIAS, certificate); + var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509ExtendedTrustManager) { + return (X509ExtendedTrustManager) tm; + } + } + throw new GeneralSecurityException("Could not obtain an X509TrustManager"); + } + + private static String generateSelfSignedCertificate() { + var keyPair = KeyUtils.generateRsaKeyPair(2048); + var certificate = CertificateUtils.generateV1SelfSignedCertificate(keyPair, JGROUPS_SUBJECT); + var entity = new CertificateEntity(); + entity.setCertificate(certificate); + entity.setKeyPair(keyPair); + return toJson(entity); + } + + private static SocketFactory createFromContext(SSLContext context) { + DefaultSocketFactory socketFactory = new DefaultSocketFactory(context); + final SSLParameters serverParameters = new SSLParameters(); + serverParameters.setProtocols(new String[]{TLS_PROTOCOL_VERSION}); + serverParameters.setNeedClientAuth(true); + socketFactory.setServerSocketConfigurator(socket -> ((SSLServerSocket) socket).setSSLParameters(serverParameters)); + return socketFactory; + } + + private static String toJson(CertificateEntity entity) { + try { + return JsonSerialization.mapper.writeValueAsString(entity); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Should never happen!", e); + } + } + + private static CertificateEntity fromJson(String json) { + try { + return JsonSerialization.mapper.readValue(json, CertificateEntity.class); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Should never happen!", e); + } + } + +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CacheEmbeddedMtlsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CacheEmbeddedMtlsDistTest.java new file mode 100644 index 000000000000..380c522e98dc --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CacheEmbeddedMtlsDistTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.it.cli.dist; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.keycloak.config.CachingOptions; +import org.keycloak.config.Option; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.DryRun; +import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.utils.KeycloakDistribution; + +@DistributionTest +public class CacheEmbeddedMtlsDistTest { + + @DryRun + @Test + @RawDistOnly(reason = "Containers are immutable") + public void testCacheEmbeddedMtlsDisabled(KeycloakDistribution dist) { + for (var option : Arrays.asList( + CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE, + CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE, + CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD, + CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD + )) { + var result = dist.run("start-dev", "--cache=ispn", "--%s=a".formatted(option.getKey())); + result.assertError("Disabled option: '--%s'. Available only when property 'cache-embedded-mtls-enabled' is enabled.".formatted(option.getKey())); + } + } + + @DryRun + @Test + @RawDistOnly(reason = "Containers are immutable") + public void testCacheEmbeddedMtlsFileValidation(KeycloakDistribution dist) { + doFileAndPasswordValidation(dist, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD); + doFileAndPasswordValidation(dist, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD); + } + + @Test + @RawDistOnly(reason = "Containers are immutable") + public void testCacheEmbeddedMtlsEnabled(KeycloakDistribution dist) { + var result = dist.run("start-dev", "--cache=ispn", "--cache-embedded-mtls-enabled=true"); + result.assertMessage("JGroups JDBC_PING discovery enabled."); + result.assertMessage("JGroups Encryption enabled (mTLS)."); + } + + private void doFileAndPasswordValidation(KeycloakDistribution dist, Option fileOption, Option passwordOption) { + var result = dist.run("start-dev", "--cache=ispn", "--cache-embedded-mtls-enabled=true", "--%s=file".formatted(fileOption.getKey())); + result.assertError("The option '%s' requires '%s' to be enabled.".formatted(fileOption.getKey(), passwordOption.getKey())); + + result = dist.run("start-dev", "--cache=ispn", "--cache-embedded-mtls-enabled=true", "--%s=secret".formatted(passwordOption.getKey())); + result.assertError("The option '%s' requires '%s' to be enabled.".formatted(passwordOption.getKey(), fileOption.getKey())); + } + + +} diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/OptionsDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/OptionsDistTest.java index c0f4a4811f00..123ab9d2137d 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/OptionsDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/OptionsDistTest.java @@ -99,18 +99,11 @@ public void testExpressionsInConfigFile(KeycloakDistribution distribution) { result.assertMessage("- log-syslog-app-name: Available only when Syslog is activated."); } - @Test - @Order(7) - @Launch({"start", "--db=dev-file", "--cache-embedded-mtls-enabled=true", "--http-enabled=true", "--hostname-strict=false"}) - public void testCacheEmbeddedMtlsEnabled(LaunchResult result) { - assertTrue(result.getOutputStream().stream().anyMatch(s -> s.contains("Property cache-embedded-mtls-key-store-file required but not specified"))); - } - // Start-dev should be executed as last tests - build is done for development mode @DryRun @Test - @Order(8) + @Order(7) @Launch({"start-dev", "--test=invalid"}) public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResult result) { assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count()); @@ -118,7 +111,7 @@ public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResu @DryRun @Test - @Order(9) + @Order(8) @Launch({"start-dev", "--log=console", "--log-file-output=json"}) public void testServerDoesNotStartDevIfDisabledFileLogOption(LaunchResult result) { assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count()); @@ -127,7 +120,7 @@ public void testServerDoesNotStartDevIfDisabledFileLogOption(LaunchResult result @DryRun @Test - @Order(10) + @Order(9) @Launch({"start-dev", "--log=file", "--log-file-output=json", "--log-console-color=true"}) public void testServerStartDevIfEnabledFileLogOption(LaunchResult result) { assertEquals(0, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count()); diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt index 831865bd519d..20113471aa6b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt @@ -38,18 +38,6 @@ Cache: The maximum number of entries that can be stored in-memory by the keys cache. --cache-embedded-mtls-enabled Encrypts the network communication between Keycloak servers. Default: false. ---cache-embedded-mtls-key-store-file - The Keystore file path. The Keystore must contain the certificate to use by - the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. ---cache-embedded-mtls-key-store-password - The password to access the Keystore. ---cache-embedded-mtls-trust-store-file - The Truststore file path. It should contain the trusted certificates or the - Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. ---cache-embedded-mtls-trust-store-password - The password to access the Truststore. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt index b41839bfaaa5..4ec3601bf2ba 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt @@ -41,15 +41,19 @@ Cache: --cache-embedded-mtls-key-store-file The Keystore file path. The Keystore must contain the certificate to use by the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. + conf/ directory. Available only when property 'cache-embedded-mtls-enabled' + is enabled.. --cache-embedded-mtls-key-store-password - The password to access the Keystore. + The password to access the Keystore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-file The Truststore file path. It should contain the trusted certificates or the Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. + 'cache-mtls-truststore.p12' under conf/ directory. Available only when + property 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-password - The password to access the Truststore. + The password to access the Truststore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt index e16d37904728..58d9b86c1c01 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt @@ -39,18 +39,6 @@ Cache: The maximum number of entries that can be stored in-memory by the keys cache. --cache-embedded-mtls-enabled Encrypts the network communication between Keycloak servers. Default: false. ---cache-embedded-mtls-key-store-file - The Keystore file path. The Keystore must contain the certificate to use by - the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. ---cache-embedded-mtls-key-store-password - The password to access the Keystore. ---cache-embedded-mtls-trust-store-file - The Truststore file path. It should contain the trusted certificates or the - Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. ---cache-embedded-mtls-trust-store-password - The password to access the Truststore. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt index 1193bc49f9f9..f5507e23a05b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt @@ -42,15 +42,19 @@ Cache: --cache-embedded-mtls-key-store-file The Keystore file path. The Keystore must contain the certificate to use by the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. + conf/ directory. Available only when property 'cache-embedded-mtls-enabled' + is enabled.. --cache-embedded-mtls-key-store-password - The password to access the Keystore. + The password to access the Keystore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-file The Truststore file path. It should contain the trusted certificates or the Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. + 'cache-mtls-truststore.p12' under conf/ directory. Available only when + property 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-password - The password to access the Truststore. + The password to access the Truststore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt index f530a22255bf..85f916a6de4c 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt @@ -39,18 +39,6 @@ Cache: The maximum number of entries that can be stored in-memory by the keys cache. --cache-embedded-mtls-enabled Encrypts the network communication between Keycloak servers. Default: false. ---cache-embedded-mtls-key-store-file - The Keystore file path. The Keystore must contain the certificate to use by - the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. ---cache-embedded-mtls-key-store-password - The password to access the Keystore. ---cache-embedded-mtls-trust-store-file - The Truststore file path. It should contain the trusted certificates or the - Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. ---cache-embedded-mtls-trust-store-password - The password to access the Truststore. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt index cda1c92a3fae..7e907123128f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt @@ -42,15 +42,19 @@ Cache: --cache-embedded-mtls-key-store-file The Keystore file path. The Keystore must contain the certificate to use by the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. + conf/ directory. Available only when property 'cache-embedded-mtls-enabled' + is enabled.. --cache-embedded-mtls-key-store-password - The password to access the Keystore. + The password to access the Keystore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-file The Truststore file path. It should contain the trusted certificates or the Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. + 'cache-mtls-truststore.p12' under conf/ directory. Available only when + property 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-password - The password to access the Truststore. + The password to access the Truststore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt index 212d296d8837..95bfe8b6cf45 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt @@ -38,18 +38,6 @@ Cache: The maximum number of entries that can be stored in-memory by the keys cache. --cache-embedded-mtls-enabled Encrypts the network communication between Keycloak servers. Default: false. ---cache-embedded-mtls-key-store-file - The Keystore file path. The Keystore must contain the certificate to use by - the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. ---cache-embedded-mtls-key-store-password - The password to access the Keystore. ---cache-embedded-mtls-trust-store-file - The Truststore file path. It should contain the trusted certificates or the - Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. ---cache-embedded-mtls-trust-store-password - The password to access the Truststore. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt index 7497efd48002..de59831f13a9 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt @@ -41,15 +41,19 @@ Cache: --cache-embedded-mtls-key-store-file The Keystore file path. The Keystore must contain the certificate to use by the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. + conf/ directory. Available only when property 'cache-embedded-mtls-enabled' + is enabled.. --cache-embedded-mtls-key-store-password - The password to access the Keystore. + The password to access the Keystore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-file The Truststore file path. It should contain the trusted certificates or the Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. + 'cache-mtls-truststore.p12' under conf/ directory. Available only when + property 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-password - The password to access the Truststore. + The password to access the Truststore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt index 8d33913030e9..1af7a644b4b6 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt @@ -36,18 +36,6 @@ Cache: The maximum number of entries that can be stored in-memory by the keys cache. --cache-embedded-mtls-enabled Encrypts the network communication between Keycloak servers. Default: false. ---cache-embedded-mtls-key-store-file - The Keystore file path. The Keystore must contain the certificate to use by - the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. ---cache-embedded-mtls-key-store-password - The password to access the Keystore. ---cache-embedded-mtls-trust-store-file - The Truststore file path. It should contain the trusted certificates or the - Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. ---cache-embedded-mtls-trust-store-password - The password to access the Truststore. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt index 703645c4950d..268d22c11f4f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt @@ -39,15 +39,19 @@ Cache: --cache-embedded-mtls-key-store-file The Keystore file path. The Keystore must contain the certificate to use by the TLS protocol. By default, it lookup 'cache-mtls-keystore.p12' under - conf/ directory. + conf/ directory. Available only when property 'cache-embedded-mtls-enabled' + is enabled.. --cache-embedded-mtls-key-store-password - The password to access the Keystore. + The password to access the Keystore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-file The Truststore file path. It should contain the trusted certificates or the Certificate Authority that signed the certificates. By default, it lookup - 'cache-mtls-truststore.p12' under conf/ directory. + 'cache-mtls-truststore.p12' under conf/ directory. Available only when + property 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-mtls-trust-store-password - The password to access the Truststore. + The password to access the Truststore. Available only when property + 'cache-embedded-mtls-enabled' is enabled.. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java index 2e857b722fc6..60d241fbb0e3 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java @@ -80,7 +80,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo protected KeycloakQuarkusConfiguration configuration; protected List additionalBuildArgs = Collections.emptyList(); - protected Map> spis = new HashMap>(); + protected Map> spis = new HashMap<>(); @Override public Class getConfigurationClass() { @@ -237,6 +237,10 @@ protected List getArgs(Map env) { System.setProperty("kc.cache-remote-create-caches", "true"); } + if (configuration.isJgroupsMtls()) { + commands.add("--cache-embedded-mtls-enabled=true"); + } + return commands; } @@ -383,12 +387,7 @@ private URL getBaseUrl(SuiteContext suiteContext) throws MalformedURLException { } private HostnameVerifier createInsecureHostnameVerifier() { - return new HostnameVerifier() { - @Override - public boolean verify(String s, SSLSession sslSession) { - return true; - } - }; + return (s, sslSession) -> true; } private SSLSocketFactory createInsecureSslSocketFactory() throws IOException { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java index f3f0e3de2ed1..37360c75608d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusConfiguration.java @@ -1,17 +1,12 @@ package org.keycloak.testsuite.arquillian.containers; -import com.fasterxml.jackson.core.type.TypeReference; import org.jboss.arquillian.container.spi.ConfigurationException; import org.jboss.arquillian.container.spi.client.container.ContainerConfiguration; import org.jboss.logging.Logger; import org.keycloak.common.crypto.FipsMode; -import org.keycloak.util.JsonSerialization; -import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; /** * @author mhajas @@ -50,6 +45,8 @@ public class KeycloakQuarkusConfiguration implements ContainerConfiguration { private String enabledFeatures; private String disabledFeatures; + private boolean jgroupsMtls; + @Override public void validate() throws ConfigurationException { int basePort = getBindHttpPort(); @@ -235,4 +232,12 @@ public String getDisabledFeatures() { public void setDisabledFeatures(String disabledFeatures) { this.disabledFeatures = disabledFeatures; } + + public boolean isJgroupsMtls() { + return jgroupsMtls; + } + + public void setJgroupsMtls(boolean jgroupsMtls) { + this.jgroupsMtls = jgroupsMtls; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index a23238f9a04f..0642441e0ed3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -676,6 +676,7 @@ -Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true true + ${auth.server.jgroups.mtls} @@ -700,6 +701,7 @@ -Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true true + ${auth.server.jgroups.mtls} diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index a22feb05776f..487d09076c3f 100644 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -93,6 +93,7 @@ false false false + false @@ -452,6 +453,7 @@ ${auth.server.keystore.type} ${auth.server.java.security.file} ${auth.server.jvm.args.extra} + ${auth.server.jgroups.mtls} ${auth.server.profile} ${auth.server.feature}