Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2026 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.custom;

import org.keycloak.connections.jpa.updater.liquibase.LiquibaseConstants;

import liquibase.Scope;
import liquibase.database.Database;
import liquibase.exception.CustomPreconditionErrorException;
import liquibase.exception.CustomPreconditionFailedException;
import liquibase.exception.DatabaseException;
import liquibase.executor.ExecutorService;
import liquibase.precondition.CustomPrecondition;
import liquibase.statement.core.RawParameterizedSqlStatement;

/**
* Passes when the given changeset is recorded in the persisted changelog table (DATABASECHANGELOG).
* <p>
* Unlike {@code changeSetExecuted}, which reads Liquibase's in-memory history, this queries the table. When a script
* is generated (migration-strategy=manual) Liquibase also adds changesets that are only written to the script to its
* in-memory history, so {@code changeSetExecuted} cannot tell them apart from changesets that have really been applied
* to the database. This precondition reflects what the database contains at the time the script is generated.
* <p>
* The table is resolved and escaped through the {@link Database}, so a changelog table in a schema that needs quoting
* (for example {@code keycloak-t}) is handled.
*/
public class ChangeSetPersistedPrecondition implements CustomPrecondition {

private String id;
private String author;
private String file;

public void setId(String id) {
this.id = id;
}

public void setAuthor(String author) {
this.author = author;
}

public void setFile(String file) {
this.file = file;
}

@Override
public void check(Database database) throws CustomPreconditionFailedException, CustomPreconditionErrorException {
String table = database.escapeTableName(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(),
database.getDatabaseChangeLogTableName());
try {
long count = Scope.getCurrentScope().getSingleton(ExecutorService.class)
.getExecutor(LiquibaseConstants.JDBC_EXECUTOR, database)
.queryForLong(new RawParameterizedSqlStatement(
"SELECT COUNT(*) FROM " + table + " WHERE ID = ? AND AUTHOR = ? AND FILENAME = ?", id, author, file));
if (count == 0) {
throw new CustomPreconditionFailedException(
"Changeset " + file + "::" + id + "::" + author + " is not recorded in " + table);
}
} catch (DatabaseException e) {
throw new CustomPreconditionErrorException("Could not query " + table, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private void populateDomainIdpRouting() throws CustomChangeException {
String ipcTable = database.correctObjectName("IDENTITY_PROVIDER_CONFIG", Table.class);
String idpTable = database.correctObjectName("IDENTITY_PROVIDER", Table.class);
String orgDomainTable = database.correctObjectName("ORG_DOMAIN", Table.class);
String orgTable = database.correctObjectName("ORG", Table.class);

try (PreparedStatement ps = connection.prepareStatement(
"SELECT c.IDENTITY_PROVIDER_ID, c.VALUE FROM " + getTableName(ipcTable) +
Expand All @@ -79,11 +80,15 @@ private void populateDomainIdpRouting() throws CustomChangeException {
boolean autoRedirect = isEmailMatchEnabled(idpInternalId);

if (!"ANY".equals(domainValue)) {
// Case A: specific domain name — look up ORG_DOMAIN.ID scoped by IdP's realm
// Case A: specific domain name — look up ORG_DOMAIN.ID scoped by IdP's realm.
// Join through ORG instead of ORG_DOMAIN.REALM_ID: during manual migration script generation
// (migration-strategy=manual), this query runs against the pre-upgrade database where
// ORG_DOMAIN.REALM_ID does not exist yet, which would fail with "column does not exist".
try (PreparedStatement domLookup = connection.prepareStatement(
"SELECT dom.ID FROM " + getTableName(orgDomainTable) + " dom" +
" JOIN " + getTableName(idpTable) + " ip ON dom.REALM_ID = ip.REALM_ID" +
" AND dom.ORG_ID = ip.ORGANIZATION_ID" +
" JOIN " + getTableName(orgTable) + " o ON o.ID = dom.ORG_ID" +
" JOIN " + getTableName(idpTable) + " ip ON ip.ORGANIZATION_ID = o.ID" +
" AND ip.REALM_ID = o.REALM_ID" +
" WHERE dom.NAME = ? AND ip.INTERNAL_ID = ?")) {
domLookup.setString(1, domainValue);
domLookup.setString(2, idpInternalId);
Expand Down Expand Up @@ -169,10 +174,14 @@ private void setIdpForOrgDomains(String idpInternalId, Set<String> excludedDomai
Set<String> existingDomainNames = new HashSet<>();
Set<String> insertedDomains = new HashSet<>();

// Get all domains for this org (ORG_ID is directly on ORG_DOMAIN)
// Get all domains for this org (ORG_ID is directly on ORG_DOMAIN).
// Do not filter on ORG_DOMAIN.IDP_ID: that column is added by an earlier changeset of the same
// release, so it is absent on the pre-upgrade database that a manual migration script
// (migration-strategy=manual) is generated against. The updates are only queued here, so the
// column holds no routing yet when this task runs during an automatic migration either.
try (PreparedStatement domPs = connection.prepareStatement(
"SELECT d.ID, d.NAME FROM " + getTableName(orgDomainTable) +
" d WHERE d.ORG_ID = ? AND d.IDP_ID IS NULL")) {
" d WHERE d.ORG_ID = ?")) {
domPs.setString(1, orgId);
try (ResultSet domRs = domPs.executeQuery()) {
while (domRs.next()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,25 @@
*/
package org.keycloak.connections.jpa.updater.liquibase.custom;

import java.sql.PreparedStatement;
import java.sql.ResultSet;

import liquibase.exception.CustomChangeException;
import liquibase.statement.core.UpdateStatement;
import liquibase.statement.core.RawParameterizedSqlStatement;
import liquibase.structure.core.Table;

public class JpaUpdate26_8_0_PopulateOrgDomainRealmId extends CustomKeycloakTask {

@Override
protected void generateStatementsImpl() throws CustomChangeException {
String orgDomainTable = database.correctObjectName("ORG_DOMAIN", Table.class);
String orgTable = database.correctObjectName("ORG", Table.class);

try (PreparedStatement ps = connection.prepareStatement(
"SELECT d.ID, o.REALM_ID FROM " + getTableName(orgDomainTable) +
" d JOIN " + getTableName(orgTable) + " o ON d.ORG_ID = o.ID WHERE d.REALM_ID IS NULL");
ResultSet rs = ps.executeQuery()) {
String orgDomainTable = getTableName(database.correctObjectName("ORG_DOMAIN", Table.class));
String orgTable = getTableName(database.correctObjectName("ORG", Table.class));

int count = 0;
while (rs.next()) {
statements.add(new UpdateStatement(null, null, orgDomainTable)
.addNewColumnValue("REALM_ID", rs.getString(2))
.setWhereClause("ID=?")
.addWhereParameter(rs.getString(1)));
count++;
}
confirmationMessage.append("Populated REALM_ID for ").append(count).append(" ORG_DOMAIN rows.");
} catch (Exception e) {
throw new CustomChangeException(getTaskId() + ": Exception when populating ORG_DOMAIN.REALM_ID", e);
}
// A single set-based UPDATE works both in update mode and in script mode:
// script mode emits this as one SQL statement that is safe to apply at any time
statements.add(new RawParameterizedSqlStatement(
"UPDATE " + orgDomainTable + " SET REALM_ID = " +
"(SELECT o.REALM_ID FROM " + orgTable + " o WHERE o.ID = " + orgDomainTable + ".ORG_ID) " +
"WHERE " + orgDomainTable + ".REALM_ID IS NULL"
));
confirmationMessage.append("Populated REALM_ID for ORG_DOMAIN rows from ORG.");
}

@Override
Expand Down
Loading
Loading