Support for EDB 17 (#42341)

Closes #42742
Closes #42293

Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>
This commit is contained in:
Václav Muzikář
2025-09-26 16:04:47 +02:00
committed by GitHub
parent 746a8211ff
commit b65a60e40d
32 changed files with 515 additions and 180 deletions

View File

@@ -0,0 +1,85 @@
name: Store IT
description: Run Store integration tests
inputs:
db:
description: The database to use for tests
required: true
runs:
using: composite
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- id: integration-test-setup
name: Integration test setup
uses: ./.github/actions/integration-test-setup
- name: Run new base tests
shell: bash
run: |
KC_TEST_DATABASE=${{ inputs.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=DatabaseTestSuite -Dkeycloak.distribution.start.timeout=360
- name: Database container port
shell: bash
run: |
# The Ryuk container process exists temporarily after the JVM terminates, wait for only the database container to remain
while [ "$(docker ps -q | wc -l)" -ne 1 ]; do
docker ps
sleep 10
done
DATABASE_PORT=$(docker ps -l --format '{{ .ID }}' | xargs docker port | cut -d ':' -f 2)
echo "DATABASE_PORT=$DATABASE_PORT" >> $GITHUB_ENV
- name: Run base tests
shell: bash
run: |
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh database`
echo "Tests: $TESTS"
./mvnw test ${{ env.SUREFIRE_RETRY }} \
-Pauth-server-quarkus -Pdb-${{ inputs.db }} \
"-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" \
-Dtest=$TESTS \
-Ddocker.database.skip=true \
-Ddocker.database.port=$DATABASE_PORT \
-Ddocker.container.testdb.ip=localhost \
-Dkeycloak.distribution.start.timeout=360 \
-pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
- name: Run cluster JDBC_PING2 UDP smoke test
shell: bash
run: |
./mvnw test ${{ env.SUREFIRE_RETRY }} \
-Pauth-server-cluster-quarkus \
-Pdb-${{ inputs.db }} \
-Dtest=RealmInvalidationClusterTest \
-Dsession.cache.owners=2 \
-Dauth.server.quarkus.cluster.stack=jdbc-ping-udp \
-Ddocker.database.skip=true \
-Ddocker.database.port=$DATABASE_PORT \
-Ddocker.container.testdb.ip=localhost \
-pl testsuite/integration-arquillian/tests/base \
2>&1 | misc/log/trimmer.sh
- name: Run cluster JDBC_PING2 TCP smoke test
shell: bash
run: |
./mvnw test ${{ env.SUREFIRE_RETRY }} \
-Pauth-server-cluster-quarkus \
-Pdb-${{ inputs.db }} \
-Dtest=RealmInvalidationClusterTest \
-Dsession.cache.owners=2 \
-Dauth.server.quarkus.cluster.stack=jdbc-ping \
-Ddocker.database.skip=true \
-Ddocker.database.port=$DATABASE_PORT \
-Ddocker.container.testdb.ip=localhost \
-pl testsuite/integration-arquillian/tests/base \
2>&1 | misc/log/trimmer.sh
- uses: ./.github/actions/upload-flaky-tests
name: Upload flaky tests
env:
GH_TOKEN: ${{ github.token }}
with:
job-name: Store IT

View File

@@ -38,6 +38,7 @@ jobs:
ci-webauthn: ${{ steps.conditional.outputs.ci-webauthn }}
ci-aurora: ${{ steps.auroradb-tests.outputs.run-aurora-tests }}
ci-compatibility-matrix: ${{ steps.version-compatibility.outputs.matrix }}
ci-additional-dbs: ${{ steps.additional-dbs-tests.outputs.run-additional-dbs-tests }}
permissions:
contents: read
pull-requests: read
@@ -58,6 +59,15 @@ jobs:
fi
echo "run-aurora-tests=$RUN_AURORADB_TESTS" >> $GITHUB_OUTPUT
- name: Additional DBs conditional check
id: additional-dbs-tests
run: |
RUN_ADDITIONAL_DBS_TESTS=false
if [[ $GITHUB_EVENT_NAME != "pull_request" && -n "${{ secrets.PRIVATE_DBS_QUAY_USERNAME }}" && -n "${{ secrets.PRIVATE_DBS_QUAY_TOKEN }}" ]]; then
RUN_ADDITIONAL_DBS_TESTS=true
fi
echo "run-additional-dbs-tests=$RUN_ADDITIONAL_DBS_TESTS" >> $GITHUB_OUTPUT
- name: Version Compatibility Matrix
id: version-compatibility
env:
@@ -571,72 +581,37 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- id: integration-test-setup
name: Integration test setup
uses: ./.github/actions/integration-test-setup
- name: Run new base tests
run: |
KC_TEST_DATABASE=${{ matrix.db }} KC_TEST_DATABASE_REUSE=true TESTCONTAINERS_REUSE_ENABLE=true ./mvnw package -f tests/pom.xml -Dtest=DatabaseTestSuite -Dkeycloak.distribution.start.timeout=360
- name: Database container port
run: |
# The Ryuk container process exists temporarily after the JVM terminates, wait for only the database container to remain
while [ "$(docker ps -q | wc -l)" -ne 1 ]; do
docker ps
sleep 10
done
DATABASE_PORT=$(docker ps -l --format '{{ .ID }}' | xargs docker port | cut -d ':' -f 2)
echo "DATABASE_PORT=$DATABASE_PORT" >> $GITHUB_ENV
- name: Run base tests
run: |
TESTS=`testsuite/integration-arquillian/tests/base/testsuites/suite.sh database`
echo "Tests: $TESTS"
./mvnw test ${{ env.SUREFIRE_RETRY }} \
-Pauth-server-quarkus -Pdb-${{ matrix.db }} \
"-Dwebdriver.chrome.driver=$CHROMEWEBDRIVER/chromedriver" \
-Dtest=$TESTS \
-Ddocker.database.skip=true \
-Ddocker.database.port=$DATABASE_PORT \
-Ddocker.container.testdb.ip=localhost \
-Dkeycloak.distribution.start.timeout=360 \
-pl testsuite/integration-arquillian/tests/base 2>&1 | misc/log/trimmer.sh
- name: Run cluster JDBC_PING2 UDP smoke test
run: |
./mvnw test ${{ env.SUREFIRE_RETRY }} \
-Pauth-server-cluster-quarkus \
-Pdb-${{ matrix.db }} \
-Dtest=RealmInvalidationClusterTest \
-Dsession.cache.owners=2 \
-Dauth.server.quarkus.cluster.stack=jdbc-ping-udp \
-Ddocker.database.skip=true \
-Ddocker.database.port=$DATABASE_PORT \
-Ddocker.container.testdb.ip=localhost \
-pl testsuite/integration-arquillian/tests/base \
2>&1 | misc/log/trimmer.sh
- name: Run cluster JDBC_PING2 TCP smoke test
run: |
./mvnw test ${{ env.SUREFIRE_RETRY }} \
-Pauth-server-cluster-quarkus \
-Pdb-${{ matrix.db }} \
-Dtest=RealmInvalidationClusterTest \
-Dsession.cache.owners=2 \
-Dauth.server.quarkus.cluster.stack=jdbc-ping \
-Ddocker.database.skip=true \
-Ddocker.database.port=$DATABASE_PORT \
-Ddocker.container.testdb.ip=localhost \
-pl testsuite/integration-arquillian/tests/base \
2>&1 | misc/log/trimmer.sh
- uses: ./.github/actions/upload-flaky-tests
name: Upload flaky tests
env:
GH_TOKEN: ${{ github.token }}
- id: run-store-tests
name: Run Store Integration Tests - ${{ matrix.db }}
uses: ./.github/actions/run-store-tests
with:
job-name: Store IT
db: ${{ matrix.db }}
store-integration-tests-additional:
name: Store IT (additional)
needs: build
runs-on: ubuntu-latest
timeout-minutes: 75
if: needs.conditional.outputs.ci-additional-dbs == 'true'
strategy:
matrix:
db: [edb]
fail-fast: false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Login to Quay.io
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ secrets.PRIVATE_DBS_QUAY_USERNAME }}
password: ${{ secrets.PRIVATE_DBS_QUAY_TOKEN }}
registry: quay.io
- id: run-store-tests
name: Run Store Integration Tests - ${{ matrix.db }}
uses: ./.github/actions/run-store-tests
with:
db: ${{ matrix.db }}
store-model-tests:
name: Store Model Tests

View File

@@ -168,4 +168,10 @@ Simply set the backup site name (e.g., availability zone) using the following op
--cache-remote-backup-sites=<name>
----
When this option is set, Infinispan will automatically replicate the cache data to the specified location.
When this option is set, Infinispan will automatically replicate the cache data to the specified location.¨
= Support for additional databases
With this release, we added support for the following new database vendors:
* EnterpriseDB (EDB) Advanced 17.6

View File

@@ -72,6 +72,7 @@
:developerguide_actiontoken_link: {developerguide_link}#_action_token_handler_spi
:developerguide_jsproviders_name: JavaScript Providers
:developerguide_jsproviders_link: {developerguide_link}#_script_providers
:importexportguide_link: https://www.keycloak.org/nightly/server/importExport
:gettingstarted_name: Getting Started Guide
:gettingstarted_name_short: Getting Started
:gettingstarted_link: https://www.keycloak.org/guides#getting-started

View File

@@ -12,6 +12,19 @@ This change enhances security by preventing unintended disclosure of authenticat
If you are relying on the `acr_values` parameter to be propagated to an identity provider, you must now explicitly set `acr_values` request parameter
to the `Forwarded query parameters` setting in the identity provider configuration.
=== Re-created indexes on the `CLIENT_ATTRIBUTES` and `GROUP_ATTRIBUTE` tables
In some previous versions of {project_name}, the EnterpriseDB (EDB) was considered unsupported. This has now changed and
EDB Advanced is supported starting with this release. If the EDB JDBC driver was used for connecting to EDB in previous versions,
some invalid schema changes were applied to the database. To mitigate this, some indexes are automatically re-created during
the schema migration to this version. **This affects you if you are using a Postgres database (including EDB), regardless if you
used EDB with previous releases.**
This affects indexes on the `CLIENT_ATTRIBUTES` and `GROUP_ATTRIBUTE` tables. If those tables contain more than 300000 entries,
{project_name} will skip the index creation by default during the automatic schema migration and instead log the SQL statement
on the console during migration to be applied manually after {project_name}'s startup.
See the link:{upgradingguide_link}[{upgradingguide_name}] for details on how to configure a different limit.
// ------------------------ Notable changes ------------------------ //
== Notable changes

View File

@@ -21,9 +21,10 @@ The server has built-in support for different databases. You can query the avail
|MariaDB Server | `mariadb` | ${properties["mariadb.version"]} | 11.8 (LTS), 11.4 (LTS), 10.11 (LTS), 10.6 (LTS)
|Microsoft SQL Server | `mssql` | ${properties["mssql.version"]} | 2022, 2019
|MySQL | `mysql` | ${properties["mysql.version"]} | 8.4 (LTS), 8.0 (LTS)
|MySQL | `mysql` | ${properties["mysql.version"]} | 8.4 (LTS), 8.0 (LTS)
|Oracle Database | `oracle` | ${properties["oracledb.version"]} | 23.x (i.e 23.5+), 19c (19.3+) (*Note: Oracle RAC is also supported if using the same database engine version, e.g 23.5+, 19.3+)
|PostgreSQL | `postgres` | ${properties["postgresql.version"]} | 17.x, 16.x, 15.x, 14.x, 13.x
|EnterpriseDB Advanced | `postgres` | ${properties["edb.version"]} | 17
|Amazon Aurora PostgreSQL | `postgres` | ${properties["aurora-postgresql.version"]} | 17.x, 16.x, 15.x
|===
@@ -49,6 +50,9 @@ this database
</@profile.ifCommunity>
or skip this section if you want to connect to a different database for which the database driver is already included.
NOTE: Overriding the built-in database drivers or supplying your own drivers is considered unsupported.
The only supported exceptions are explicitly documented in this guide, such as the Oracle Database driver.
=== Installing the Oracle Database driver
To install the Oracle Database driver for {project_name}:
@@ -184,19 +188,6 @@ The following is a sample command for a PostgreSQL database.
Be aware that you need to escape characters when invoking commands containing special shell characters such as `;` using the CLI, so you might want to set it in the configuration file instead.
== Overriding the default JDBC driver
The server uses a default JDBC driver accordingly to the database you chose.
To set a different driver you can set the `db-driver` with the fully qualified class name of the JDBC driver:
<@kc.start parameters="--db postgres --db-driver=my.Driver"/>
Regardless of the driver you set, the default driver is always available at runtime.
Only set this property if you really need to. For instance, when leveraging the capabilities from a JDBC Driver Wrapper for
a specific cloud database service.
== Configuring Unicode support for the database
Unicode support for all fields depends on whether the database allows VARCHAR and CHAR fields to use the Unicode character set.

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2016 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.connections.jpa.updater.liquibase;
import liquibase.Scope;
import liquibase.database.DatabaseConnection;
import liquibase.database.core.PostgresDatabase;
import liquibase.exception.DatabaseException;
import liquibase.executor.ExecutorService;
import liquibase.statement.core.RawSqlStatement;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PostgresPlusDatabase extends PostgresDatabase {
public static final String POSTGRESPLUS_PRODUCT_NAME = "EnterpriseDB";
@Override
public String getShortName() {
return "postgresplus";
}
@Override
protected String getDefaultDatabaseProductName() {
return POSTGRESPLUS_PRODUCT_NAME;
}
@Override
public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
return POSTGRESPLUS_PRODUCT_NAME.equalsIgnoreCase(conn.getDatabaseProductName());
}
@Override
public String getDefaultDriver(String url) {
String defaultDriver = super.getDefaultDriver(url);
if (defaultDriver == null) {
if (url.startsWith("jdbc:edb:")) {
defaultDriver = "com.edb.Driver";
}
}
return defaultDriver;
}
@Override
protected String getConnectionSchemaName() {
try {
return Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor(LiquibaseConstants.JDBC_EXECUTOR, this)
.queryForObject(new RawSqlStatement("select current_schema"), String.class);
} catch (Exception e) {
throw new RuntimeException("Failed to get current schema", e);
}
}
}

View File

@@ -18,11 +18,10 @@
package org.keycloak.connections.jpa.updater.liquibase.conn;
import liquibase.Scope;
import liquibase.ScopeManager;
import liquibase.ThreadLocalScopeManager;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.DatabaseConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.ClassLoaderResourceAccessor;
@@ -30,6 +29,7 @@ import liquibase.resource.ResourceAccessor;
import liquibase.ui.LoggerUIService;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.config.DatabaseOptions;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -49,6 +49,7 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
public static final String INDEX_CREATION_THRESHOLD_PARAM = "keycloak.indexCreationThreshold";
private int indexCreationThreshold;
private Class<? extends Database> liquibaseDatabaseClazz;
private static final AtomicBoolean INITIALIZATION = new AtomicBoolean(false);
@@ -92,10 +93,26 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
}
}
@SuppressWarnings("unchecked")
@Override
public void init(Config.Scope config) {
indexCreationThreshold = config.getInt("indexCreationThreshold", 300000);
logger.debugf("indexCreationThreshold is %d", indexCreationThreshold);
// We need to explicitly handle the default here as Config might not be MicroProfile and hence no actually server config exists
String dbAlias = config.root().get(DatabaseOptions.DB.getKey(), "dev-file");
logger.debugf("dbAlias is %s", dbAlias);
// We're not using the Liquibase logic to get the DB. That is because we already know which DB class we want to use
// for which DB vendor. We don't want to rely on auto-detection in Liquibase as it might make wrong assumptions (e.g. EDB).
String liquibaseType = org.keycloak.config.database.Database.getVendor(dbAlias).orElseThrow().getLiquibaseType();
logger.debugf("liquibaseType is %s", liquibaseType);
try {
liquibaseDatabaseClazz = (Class<? extends Database>) Class.forName(liquibaseType);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to load Liquibase Database class: " + liquibaseType, e);
}
}
@Override
@@ -114,7 +131,7 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
@Override
public KeycloakLiquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException {
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
Database database = getLiquibaseDatabase(connection);
if (defaultSchema != null) {
database.setDefaultSchemaName(defaultSchema);
}
@@ -130,7 +147,7 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
@Override
public KeycloakLiquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
Database database = getLiquibaseDatabase(connection);
if (defaultSchema != null) {
database.setDefaultSchemaName(defaultSchema);
}
@@ -143,4 +160,25 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
return new KeycloakLiquibase(changelogLocation, resourceAccessor, database);
}
// Similarly to Hibernate, we want to enforce Liquibase to use the same DB as configured in Keycloak
private Database getLiquibaseDatabase(Connection connection) {
Database liquibaseDatabase;
// Mimic what DatabaseFactory#findCorrectDatabaseImplementation does: create DB instance using reflections
try {
liquibaseDatabase = liquibaseDatabaseClazz.getConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to create instance of " + liquibaseDatabaseClazz.getName());
}
DatabaseConnection liquibaseConnection = new JdbcConnection(connection);
try {
logger.debugf("DB Product Name: %s", liquibaseConnection.getDatabaseProductName());
} catch (LiquibaseException e) {
logger.debug("Failed to detect DB Product Name", e);
}
liquibaseDatabase.setConnection(liquibaseConnection);
return liquibaseDatabase;
}
}

View File

@@ -64,6 +64,10 @@
<validCheckSum>7:fad08e83c77d0171ec166bc9bc5d390a</validCheckSum>
<validCheckSum>7:72553fac2d2281052acbbbb14aa22ccf</validCheckSum>
<validCheckSum>7:b558ad47ea0e4d3c3514225a49cc0d65</validCheckSum>
<!-- EDB previously did not run this change set, now it does -->
<validCheckSum>8:f43dfba07ba249d5d932dc489fd2b886</validCheckSum>
<validCheckSum>9:bd2bd0fc7768cf0845ac96a8786fa735</validCheckSum>
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<or>
<dbms type="mysql"/>

View File

@@ -18,6 +18,7 @@
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="20.0.0-12964-supported-dbs">
<validCheckSum>9:e5f243877199fd96bcc842f27a1656ac</validCheckSum>
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<or>
<dbms type="mysql"/>
@@ -38,6 +39,19 @@
</modifySql>
</changeSet>
<changeSet author="keycloak" id="20.0.0-12964-supported-dbs-edb-migration">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<dbms type="postgresql"/>
<changeSetExecuted id="20.0.0-12964-supported-dbs" author="keycloak" changeLogFile="META-INF/jpa-changelog-20.0.0.xml"/>
<indexExists tableName="GROUP_ATTRIBUTE" indexName="IDX_GROUP_ATT_BY_NAME_VALUE" />
</preConditions>
<dropIndex tableName="GROUP_ATTRIBUTE" indexName="IDX_GROUP_ATT_BY_NAME_VALUE" />
<createIndex tableName="GROUP_ATTRIBUTE" indexName="IDX_GROUP_ATT_BY_NAME_VALUE">
<column name="NAME" type="VARCHAR(255)"/>
<column name="(value::varchar(250))" valueComputed="(value::varchar(250))" />
</createIndex>
</changeSet>
<changeSet author="keycloak" id="20.0.0-12964-unsupported-dbs">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<not>

View File

@@ -83,4 +83,18 @@
</modifySql>
</changeSet>
<changeSet author="keycloak" id="24.0.0-26618-edb-migration">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<dbms type="postgresql"/>
<indexExists tableName="CLIENT_ATTRIBUTES" indexName="IDX_CLIENT_ATT_BY_NAME_VALUE" />
<changeSetExecuted id="24.0.0-26618-drop-index-if-present" author="keycloak" changeLogFile="META-INF/jpa-changelog-24.0.0.xml" />
<changeSetExecuted id="24.0.0-26618-reindex" author="keycloak" changeLogFile="META-INF/jpa-changelog-24.0.0.xml" />
</preConditions>
<dropIndex tableName="CLIENT_ATTRIBUTES" indexName="IDX_CLIENT_ATT_BY_NAME_VALUE"/>
<createIndex tableName="CLIENT_ATTRIBUTES" indexName="IDX_CLIENT_ATT_BY_NAME_VALUE">
<column name="NAME" type="VARCHAR(255)"/>
<column name="substr(VALUE,1,255)" valueComputed="substr(VALUE,1,255)" />
</createIndex>
</changeSet>
</databaseChangeLog>

View File

@@ -15,7 +15,7 @@
~ * See the License for the specific language governing permissions and
~ * limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet author="keycloak" id="25.0.0-28265-tables">
<addColumn tableName="OFFLINE_USER_SESSION">
@@ -144,6 +144,18 @@
<addUniqueConstraint columnNames="CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID" constraintName="UK_EXTERNAL_CONSENT" tableName="USER_CONSENT"/>
</changeSet>
<changeSet author="keycloak" id="unique-consentuser-edb-migration">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<dbms type="postgresql"/>
<changeSetExecuted id="unique-consentuser" author="keycloak" changeLogFile="META-INF/jpa-changelog-25.0.0.xml" />
<uniqueConstraintExists tableName="USER_CONSENT" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT"/>
</preConditions>
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate25_0_0_ConsentConstraints"/>
<dropUniqueConstraint tableName="USER_CONSENT" constraintName="UK_JKUWUVD56ONTGSUHOGM8UEWRT"/>
<addUniqueConstraint columnNames="CLIENT_ID, USER_ID" constraintName="UK_LOCAL_CONSENT" tableName="USER_CONSENT"/>
<addUniqueConstraint columnNames="CLIENT_STORAGE_PROVIDER, EXTERNAL_CLIENT_ID, USER_ID" constraintName="UK_EXTERNAL_CONSENT" tableName="USER_CONSENT"/>
</changeSet>
<changeSet author="keycloak" id="unique-consentuser-mysql">
<preConditions onSqlOutput="TEST" onFail="MARK_RAN">
<or>

View File

@@ -15,6 +15,5 @@
# limitations under the License.
#
org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase
org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase
org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase

View File

@@ -173,6 +173,9 @@
<oracledb.container>mirror.gcr.io/gvenzl/oracle-free:${oracledb.version}-slim-faststart</oracledb.container>
<!-- this is the oracle driver version also used in the Quarkus BOM -->
<oracle-jdbc.version>23.6.0.24.10</oracle-jdbc.version>
<!-- Custom image, lives in test-framework/db-edb/container -->
<edb.container>quay.io/keycloakqe/enterprisedb:${edb.version}</edb.container>
<edb.version>17</edb.version>
<!-- Infinispan Server Container -->
<infinispan.container>quay.io/infinispan/server:${infinispan.version}</infinispan.container>

View File

@@ -48,7 +48,7 @@ public final class Database {
public static boolean isLiquibaseDatabaseSupported(String databaseType, String dbKind) {
for (Vendor vendor : DATABASES.values()) {
if (vendor.liquibaseTypes.contains(databaseType) && vendor.isOfKind(dbKind)) {
if (vendor.liquibaseType.equals(databaseType) && vendor.isOfKind(dbKind)) {
return true;
}
}
@@ -177,7 +177,7 @@ public final class Database {
return addH2CloseOnExit(addH2NonKeywords(jdbcUrl));
}
},
asList("liquibase.database.core.H2Database"),
"liquibase.database.core.H2Database",
"dev-mem", "dev-file"
),
MYSQL("mysql",
@@ -190,7 +190,7 @@ public final class Database {
getProperty(DatabaseOptions.DB_URL_PORT, namedProperty, "3306"),
getProperty(DatabaseOptions.DB_URL_DATABASE, namedProperty, "keycloak"),
getProperty(DatabaseOptions.DB_URL_PROPERTIES, namedProperty)),
List.of("org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase")
"org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase"
),
TIDB("tidb",
"com.mysql.cj.jdbc.MysqlXADataSource",
@@ -202,7 +202,7 @@ public final class Database {
getProperty(DatabaseOptions.DB_URL_PORT, namedProperty, "3306"),
getProperty(DatabaseOptions.DB_URL_DATABASE, namedProperty, "keycloak"),
getProperty(DatabaseOptions.DB_URL_PROPERTIES, namedProperty)),
List.of("org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase")
"org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase"
),
MARIADB("mariadb",
"org.mariadb.jdbc.MariaDbDataSource",
@@ -214,7 +214,7 @@ public final class Database {
getProperty(DatabaseOptions.DB_URL_PORT, namedProperty, "3306"),
getProperty(DatabaseOptions.DB_URL_DATABASE, namedProperty, "keycloak"),
getProperty(DatabaseOptions.DB_URL_PROPERTIES, namedProperty)),
List.of("org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase")
"org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase"
),
POSTGRES("postgresql",
"org.postgresql.xa.PGXADataSource",
@@ -226,7 +226,7 @@ public final class Database {
getProperty(DatabaseOptions.DB_URL_PORT, namedProperty, "5432"),
getProperty(DatabaseOptions.DB_URL_DATABASE, namedProperty, "keycloak"),
getProperty(DatabaseOptions.DB_URL_PROPERTIES, namedProperty)),
asList("liquibase.database.core.PostgresDatabase", "org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase"),
"liquibase.database.core.PostgresDatabase",
"postgres"
),
MSSQL("mssql",
@@ -239,7 +239,7 @@ public final class Database {
getProperty(DatabaseOptions.DB_URL_PORT, namedProperty, "1433"),
getProperty(DatabaseOptions.DB_URL_DATABASE, namedProperty, "keycloak"),
getProperty(DatabaseOptions.DB_URL_PROPERTIES, namedProperty)),
List.of("org.keycloak.quarkus.runtime.storage.database.liquibase.database.CustomMSSQLDatabase"),
"org.keycloak.quarkus.runtime.storage.database.liquibase.database.CustomMSSQLDatabase",
"mssql"
),
ORACLE("oracle",
@@ -251,7 +251,7 @@ public final class Database {
getProperty(DatabaseOptions.DB_URL_HOST, namedProperty, "localhost"),
getProperty(DatabaseOptions.DB_URL_PORT, namedProperty, "1521"),
getProperty(DatabaseOptions.DB_URL_DATABASE, namedProperty, "keycloak")),
List.of("liquibase.database.core.OracleDatabase")
"liquibase.database.core.OracleDatabase"
);
final String databaseKind;
@@ -259,28 +259,23 @@ public final class Database {
final String nonXaDriver;
final Function<String, String> dialect;
final BiFunction<String, String, String> defaultUrl;
final List<String> liquibaseTypes;
final String liquibaseType;
final String[] aliases;
Vendor(String databaseKind, String xaDriver, String nonXaDriver, String dialect, String defaultUrl, List<String> liquibaseTypes,
String... aliases) {
this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, (namedProperty, alias) -> defaultUrl, liquibaseTypes, aliases);
}
Vendor(String databaseKind, String xaDriver, String nonXaDriver, String dialect, BiFunction<String, String, String> defaultUrl,
List<String> liquibaseTypes, String... aliases) {
this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, defaultUrl, liquibaseTypes, aliases);
String liquibaseType, String... aliases) {
this(databaseKind, xaDriver, nonXaDriver, alias -> dialect, defaultUrl, liquibaseType, aliases);
}
Vendor(String databaseKind, String xaDriver, String nonXaDriver, Function<String, String> dialect, BiFunction<String, String, String> defaultUrl,
List<String> liquibaseTypes,
String liquibaseType,
String... aliases) {
this.databaseKind = databaseKind;
this.xaDriver = xaDriver;
this.nonXaDriver = nonXaDriver;
this.dialect = dialect;
this.defaultUrl = defaultUrl;
this.liquibaseTypes = liquibaseTypes;
this.liquibaseType = liquibaseType;
this.aliases = aliases.length == 0 ? new String[]{databaseKind} : aliases;
}
@@ -298,6 +293,10 @@ public final class Database {
defaultValue);
}
public String getLiquibaseType() {
return liquibaseType;
}
@Override
public String toString() {
return databaseKind.toLowerCase(Locale.ROOT);

View File

@@ -38,6 +38,9 @@ quarkus.log.category."io.quarkus.deployment.steps.ReflectiveHierarchyStep".level
# https://hibernate.zulipchat.com/#narrow/channel/132096-hibernate-user/topic/Feature.20Request.3A.20Disable.20logging.20of.20SqlExceptionHelper.20for
quarkus.log.category."org.hibernate.engine.jdbc.spi.SqlExceptionHelper".level=off
# Disable irrelevant EDB warning: EnterpriseDB does not store DATE columns. Instead, it auto-converts them to TIMESTAMPs. (edb_redwood_date=true)
quarkus.log.category."liquibase.database.core.PostgresDatabase".level=error
quarkus.log.console.filter=keycloak-filter
quarkus.log.file.filter=keycloak-filter
quarkus.log.syslog.filter=keycloak-filter

View File

@@ -57,6 +57,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-edb</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-mariadb</artifactId>

View File

@@ -1,4 +1,5 @@
## Database containers ##
edb.container=${edb.container}
mysql.container=${mysql.container}
postgres.container=${postgresql.container}
mariadb.container=${mariadb.container}

View File

@@ -0,0 +1,21 @@
FROM registry.access.redhat.com/ubi9
# Get the token at https://www.enterprisedb.com/repos-downloads
ARG EDB_REPO_TOKEN=token-must-be-set
ENV VERSION=17
ENV PGUSER=enterprisedb
ENV PGPASSWORD=password
ENV PGDATABASE=keycloak
ENV PGPORT=5432
ENV PGDATA=/var/lib/edb/as${VERSION}/data
RUN (curl -1sSLf "https://downloads.enterprisedb.com/${EDB_REPO_TOKEN}/enterprise/setup.rpm.sh" | bash) && \
dnf -y install edb-as${VERSION}-server
USER enterprisedb
WORKDIR /usr/edb/as${VERSION}/bin/
COPY init-and-start-db.sh .
CMD ./init-and-start-db.sh
EXPOSE ${PGPORT}

View File

@@ -0,0 +1,9 @@
#! /bin/bash
set -euo pipefail
PGSETUP_INITDB_OPTIONS="-E UTF-8" ./initdb -A md5 -U $PGUSER --pwfile=<(echo "$PGPASSWORD")
echo "host all all 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf";
(while ! (sleep 1 && ./createdb $PGDATABASE > /dev/null 2>&1); do echo "Retrying database creation..."; done; echo "Database $PGDATABASE created.") &
exec ./edb-postgres -c port=$PGPORT -c logging_collector=off -c listen_addresses=*

41
test-framework/db-edb/pom.xml Executable file
View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-test-framework-parent</artifactId>
<groupId>org.keycloak.testframework</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-test-framework-db-edb</artifactId>
<name>Keycloak Test Framework - EDB support</name>
<packaging>jar</packaging>
<description>EDB support for Keycloak Test Framework</description>
<dependencies>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,15 @@
package org.keycloak.testframework.database;
public class EnterpriseDbDatabaseSupplier extends AbstractDatabaseSupplier {
@Override
public String getAlias() {
return EnterpriseDbTestDatabase.NAME;
}
@Override
TestDatabase getTestDatabase() {
return new EnterpriseDbTestDatabase();
}
}

View File

@@ -0,0 +1,27 @@
package org.keycloak.testframework.database;
import org.jboss.logging.Logger;
import org.keycloak.testframework.util.ContainerImages;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
public class EnterpriseDbTestDatabase extends AbstractContainerTestDatabase {
private static final Logger LOGGER = Logger.getLogger(EnterpriseDbTestDatabase.class);
public static final String NAME = "edb";
@Override
public JdbcDatabaseContainer<?> createContainer() {
return new KeycloakEnterpriseDbContainer(DockerImageName.parse(ContainerImages.getContainerImageName(NAME)));
}
@Override
public String getDatabaseVendor() {
return "postgres";
}
@Override
public Logger getLogger() {
return LOGGER;
}
}

View File

@@ -0,0 +1,14 @@
package org.keycloak.testframework.database;
import org.keycloak.testframework.TestFrameworkExtension;
import org.keycloak.testframework.injection.Supplier;
import java.util.List;
public class EnterpriseDbTestFrameworkExtension implements TestFrameworkExtension {
@Override
public List<Supplier<?, ?>> suppliers() {
return List.of(new EnterpriseDbDatabaseSupplier());
}
}

View File

@@ -0,0 +1,79 @@
package org.keycloak.testframework.database;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public class KeycloakEnterpriseDbContainer extends JdbcDatabaseContainer<KeycloakEnterpriseDbContainer> {
private String databaseName = "keycloak";
private String username = "enterprisedb";
private String password = "password";
private static final int PORT = 5432;
public KeycloakEnterpriseDbContainer(DockerImageName dockerImageName) {
super(dockerImageName);
}
@Override
public String getDriverClassName() {
return "org.postgresql.Driver";
}
@Override
public String getJdbcUrl() {
return String.format("jdbc:postgresql://%s:%d/%s", getHost(), getMappedPort(PORT), getDatabaseName());
}
@Override
protected void configure() {
addEnv("PGDATABASE", getDatabaseName());
addEnv("PGUSER", getUsername());
addEnv("PGPASSWORD", getPassword());
addExposedPort(PORT);
}
@Override
public String getTestQueryString() {
return "SELECT 1";
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getDatabaseName() {
return databaseName;
}
@Override
public KeycloakEnterpriseDbContainer withUsername(String username) {
this.username = username;
return this;
}
@Override
public KeycloakEnterpriseDbContainer withPassword(String password) {
this.password = password;
return this;
}
@Override
public KeycloakEnterpriseDbContainer withDatabaseName(String dbName) {
this.databaseName = dbName;
return this;
}
@Override
public KeycloakEnterpriseDbContainer withUrlParam(String paramName, String paramValue) {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1 @@
org.keycloak.testframework.database.EnterpriseDbTestFrameworkExtension

View File

@@ -36,6 +36,7 @@
<module>bom</module>
<module>core</module>
<module>junit5-config</module>
<module>db-edb</module>
<module>db-mariadb</module>
<module>db-mssql</module>
<module>db-mysql</module>

View File

@@ -56,6 +56,10 @@
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-ui</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-edb</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-db-mariadb</artifactId>

View File

@@ -5,6 +5,8 @@ import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conditions.DisabledForDatabases;
import org.keycloak.testframework.database.DatabaseConfig;
import org.keycloak.testframework.database.DatabaseConfigBuilder;
import org.keycloak.testframework.database.EnterpriseDbDatabaseSupplier;
import org.keycloak.testframework.database.EnterpriseDbTestDatabase;
import org.keycloak.testframework.database.PostgresTestDatabase;
import org.keycloak.testframework.database.TestDatabase;
import org.keycloak.testframework.server.KeycloakServerConfig;
@@ -25,7 +27,8 @@ public class CaseSensitiveSchemaTest extends AbstractDBSchemaTest {
return switch (dbType()) {
// DBs that convert unquoted to lower-case by default
case PostgresTestDatabase.NAME -> config.option("db-schema", "KEYCLOAK");
case PostgresTestDatabase.NAME, EnterpriseDbTestDatabase.NAME
-> config.option("db-schema", "KEYCLOAK");
// DBs that convert unquoted to upper-case by default
case "dev-file", "dev-mem" ->
config.option("db-url-properties", ";INIT=CREATE SCHEMA IF NOT EXISTS keycloak").option("db-schema", "keycloak");
@@ -37,7 +40,7 @@ public class CaseSensitiveSchemaTest extends AbstractDBSchemaTest {
public static class CaseSensitiveDatabaseConfig implements DatabaseConfig {
@Override
public DatabaseConfigBuilder configure(DatabaseConfigBuilder database) {
if (PostgresTestDatabase.NAME.equals(dbType())) {
if (PostgresTestDatabase.NAME.equals(dbType()) || EnterpriseDbTestDatabase.NAME.equals(dbType())) {
database.initScript("org/keycloak/tests/db/case-sensitive-schema-postgres.sql");
}
return database;

View File

@@ -5,6 +5,7 @@ import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.database.DatabaseConfig;
import org.keycloak.testframework.conditions.DisabledForDatabases;
import org.keycloak.testframework.database.DatabaseConfigBuilder;
import org.keycloak.testframework.database.EnterpriseDbTestDatabase;
import org.keycloak.testframework.database.PostgresTestDatabase;
import org.keycloak.testframework.database.TestDatabase;
import org.keycloak.testframework.injection.LifeCycle;
@@ -36,7 +37,7 @@ public class PreserveSchemaCaseLiquibaseTest extends AbstractDBSchemaTest {
private static class PreserveSchemaCaseDatabaseConfig implements DatabaseConfig {
@Override
public DatabaseConfigBuilder configure(DatabaseConfigBuilder database) {
if (dbType().equals(PostgresTestDatabase.NAME)) {
if (dbType().equals(PostgresTestDatabase.NAME) || dbType().equals(EnterpriseDbTestDatabase.NAME)) {
return database.initScript("org/keycloak/tests/db/preserve-schema-case-liquibase-postgres.sql");
}
return database.database("keycloak-t");

View File

@@ -480,6 +480,28 @@
<jdbc.mvn.version>${aws-jdbc-wrapper.version}</jdbc.mvn.version>
</properties>
</profile>
<profile>
<id>db-edb</id>
<properties>
<keycloak.storage.connections.vendor>postgres</keycloak.storage.connections.vendor>
<keycloak.connectionsJpa.driver>org.postgresql.Driver</keycloak.connectionsJpa.driver>
<keycloak.connectionsJpa.database>keycloak</keycloak.connectionsJpa.database>
<keycloak.connectionsJpa.user>keycloak</keycloak.connectionsJpa.user>
<keycloak.connectionsJpa.password>keycloak</keycloak.connectionsJpa.password>
<keycloak.connectionsJpa.url>jdbc:postgresql://${auth.server.db.host}:${docker.database.port}/${keycloak.connectionsJpa.database}</keycloak.connectionsJpa.url>
<!-- JDBC properties point to "default" JDBC driver for the particular DB -->
<!-- For EAP testing, it is recommended to override those with system properties pointing to GAV of more appropriate JDBC driver -->
<!-- for the particular EAP version -->
<jdbc.mvn.groupId>org.postgresql</jdbc.mvn.groupId>
<jdbc.mvn.artifactId>postgresql</jdbc.mvn.artifactId>
<jdbc.mvn.version>${postgresql-jdbc.version}</jdbc.mvn.version>
<docker.database.image>${edb.container}</docker.database.image>
<docker.database.port>5432</docker.database.port>
<docker.database.skip>false</docker.database.skip>
<docker.database.cmd>./init-and-start-db.sh</docker.database.cmd>
<docker.database.wait-for-log-regex>Database keycloak created.</docker.database.wait-for-log-regex>
</properties>
</profile>
<profile>
<id>db-mariadb</id>
<properties>

View File

@@ -395,6 +395,12 @@
<ORACLE_PASSWORD>${keycloak.connectionsJpa.password}</ORACLE_PASSWORD>
<APP_USER>${keycloak.connectionsJpa.user}</APP_USER>
<APP_USER_PASSWORD>${keycloak.connectionsJpa.password}</APP_USER_PASSWORD>
<!-- EnterpriseDB -->
<PGPORT>${docker.database.port}</PGPORT>
<PGDATABASE>${keycloak.connectionsJpa.database}</PGDATABASE>
<PGUSER>${keycloak.connectionsJpa.user}</PGUSER>
<PGPASSWORD>${keycloak.connectionsJpa.password}</PGPASSWORD>
</env>
<cmd>${docker.database.cmd}</cmd>
<wait>