diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate26_6_0_MigrateRealmDisplayName.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate26_6_0_MigrateRealmDisplayName.java new file mode 100644 index 00000000000..1bb48e9b33e --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate26_6_0_MigrateRealmDisplayName.java @@ -0,0 +1,126 @@ +/* + * 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 liquibase.database.core.MSSQLDatabase; +import liquibase.database.core.MySQLDatabase; +import liquibase.database.core.OracleDatabase; +import liquibase.database.core.PostgresDatabase; +import liquibase.exception.CustomChangeException; +import liquibase.statement.core.RawParameterizedSqlStatement; + +/** + * Custom SQL change to migrate the displayName from the REALM_ATTRIBUTE table to the REALM table. + * See: keycloak#45292 + * + * @author tre2man + */ +public class JpaUpdate26_6_0_MigrateRealmDisplayName extends CustomKeycloakTask { + + @Override + protected void generateStatementsImpl() throws CustomChangeException { + String realmTable = getTableName("REALM"); + String realmAttributeTable = getTableName("REALM_ATTRIBUTE"); + + if (database instanceof PostgresDatabase) { + generateUpdateQueryForPostgresSQL(realmTable, realmAttributeTable); + } else if (database instanceof MySQLDatabase) { + generateUpdateQueryForMySQL(realmTable, realmAttributeTable); + } else if (database instanceof MSSQLDatabase) { + generateUpdateQueryForMsSQL(realmTable, realmAttributeTable); + } else if (database instanceof OracleDatabase) { + generateUpdateQueryForOracleSQL(realmTable, realmAttributeTable); + } else { + generateUpdateQueryUsingStandardSQL(realmTable, realmAttributeTable); + } + + generateDelete(realmAttributeTable); + } + + private void generateUpdateQueryForPostgresSQL(String realmTable, String realmAttributeTable) { + statements.add(new RawParameterizedSqlStatement(""" + UPDATE %s r + SET display_name = ra.value + FROM %s ra + WHERE ra.realm_id = r.id and ra.name = 'displayName' + """ + .formatted(realmTable, realmAttributeTable))); + } + + private void generateUpdateQueryForMySQL(String realmTable, String realmAttributeTable) { + statements.add(new RawParameterizedSqlStatement(""" + UPDATE %s r + JOIN %s ra ON r.id = ra.realm_id + SET r.display_name = ra.value + WHERE ra.name = 'displayName' + """ + .formatted(realmTable, realmAttributeTable))); + } + + private void generateUpdateQueryForMsSQL(String realmTable, String realmAttributeTable) { + statements.add(new RawParameterizedSqlStatement(""" + UPDATE r + SET display_name = ra.value + FROM %s r + JOIN %s ra ON r.id = ra.realm_id + WHERE ra.name = 'displayName' + """ + .formatted(realmTable, realmAttributeTable))); + } + + private void generateUpdateQueryForOracleSQL(String realmTable, String realmAttributeTable) { + statements.add(new RawParameterizedSqlStatement(""" + MERGE INTO %s r + USING ( + SELECT realm_id, value + FROM %s + WHERE name = 'displayName' + ) ra + ON (ra.realm_id = r.id) + WHEN MATCHED THEN + UPDATE SET r.display_name = ra.value + """ + .formatted(realmTable, realmAttributeTable))); + } + + private void generateUpdateQueryUsingStandardSQL(String realmTable, String realmAttributeTable) { + statements.add(new RawParameterizedSqlStatement(""" + UPDATE %s r + SET r.display_name = ( + SELECT ra.value + FROM %s ra + WHERE ra.realm_id = r.id AND ra.name = 'displayName' + ) + WHERE EXISTS ( + SELECT 1 + FROM %s ra + WHERE ra.realm_id = r.id AND ra.name = 'displayName' + ) + """ + .formatted(realmTable, realmAttributeTable, realmAttributeTable))); + } + + private void generateDelete(String realmAttributeTable) { + statements.add(new RawParameterizedSqlStatement( + "DELETE FROM %s WHERE name = 'displayName'".formatted(realmAttributeTable))); + } + + @Override + protected String getTaskId() { + return "Migrate displayName from REALM_ATTRIBUTE to REALM.DISPLAY_NAME column"; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index f00c5d18cab..1c399653573 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -144,12 +144,13 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel attributes = new LinkedList<>(); @@ -500,6 +503,14 @@ public class RealmEntity { this.emailTheme = emailTheme; } + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + public int getNotBefore() { return notBefore; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.6.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.6.0.xml index 6fb98db1186..d8dab91ce8b 100644 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-26.6.0.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-26.6.0.xml @@ -97,4 +97,14 @@ + + + + + + + + + + diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmAttributesTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmAttributesTest.java index 5dcba84f6f3..a7008c85a71 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmAttributesTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmAttributesTest.java @@ -268,4 +268,28 @@ public class RealmAttributesTest extends AbstractRealmTest { adminClient.realm(realmName).remove(); } } + + @Test + public void testDisplayNameNotRemovableViaRemoveAttribute() { + String realmName = "testDisplayNameNotRemovable"; + RealmRepresentation rep = new RealmRepresentation(); + rep.setRealm(realmName); + rep.setDisplayName("Test Display Name"); + + adminClient.realms().create(rep); + + try { + runOnServer.run(session -> { + RealmModel realm = session.realms().getRealmByName(realmName); + + assertEquals("Test Display Name", realm.getDisplayName()); + realm.removeAttribute("displayName"); + assertEquals("Test Display Name", realm.getDisplayName()); + realm.setDisplayName("Updated Display Name"); + assertEquals("Updated Display Name", realm.getDisplayName()); + }); + } finally { + adminClient.realm(realmName).remove(); + } + } } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmSearchTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmSearchTest.java new file mode 100644 index 00000000000..d905b64df15 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmSearchTest.java @@ -0,0 +1,120 @@ +package org.keycloak.tests.admin.realm; + +import org.junit.jupiter.api.Test; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@KeycloakIntegrationTest +public class RealmSearchTest extends AbstractRealmTest { + + @Test + public void testSearchRealmByName() { + String realmName1 = "testSearchRealmA"; + String realmName2 = "testSearchRealmB"; + String realmName3 = "anotherRealmC"; + + // Create realms + RealmRepresentation rep1 = new RealmRepresentation(); + rep1.setRealm(realmName1); + adminClient.realms().create(rep1); + + RealmRepresentation rep2 = new RealmRepresentation(); + rep2.setRealm(realmName2); + adminClient.realms().create(rep2); + + RealmRepresentation rep3 = new RealmRepresentation(); + rep3.setRealm(realmName3); + adminClient.realms().create(rep3); + + try { + runOnServer.run(session -> { + RealmProvider realmProvider = session.realms(); + + List realmNames = realmProvider.getRealmsStream("testSearch") + .map(RealmModel::getName) + .collect(Collectors.toList()); + + assertTrue(realmNames.contains(realmName1), "Should find realm1"); + assertTrue(realmNames.contains(realmName2), "Should find realm2"); + assertFalse(realmNames.contains(realmName3), "Should not find realm3"); + + List realmNames2 = realmProvider.getRealmsStream("anotherRealmC") + .map(RealmModel::getName) + .collect(Collectors.toList()); + + assertTrue(realmNames2.contains(realmName3), "Should find realm3"); + }); + } finally { + adminClient.realm(realmName1).remove(); + adminClient.realm(realmName2).remove(); + adminClient.realm(realmName3).remove(); + } + } + + @Test + public void testSearchRealmByDisplayName() { + String realmName1 = "testSearchRealm1"; + String realmName2 = "testSearchRealm2"; + String realmName3 = "testSearchRealm3"; + + String displayName1 = "Unique Display Name ABC"; + String displayName2 = "Different Name XYZ"; + String displayName3 = "Another Unique Display"; + + RealmRepresentation rep1 = new RealmRepresentation(); + rep1.setRealm(realmName1); + rep1.setDisplayName(displayName1); + adminClient.realms().create(rep1); + + RealmRepresentation rep2 = new RealmRepresentation(); + rep2.setRealm(realmName2); + rep2.setDisplayName(displayName2); + adminClient.realms().create(rep2); + + RealmRepresentation rep3 = new RealmRepresentation(); + rep3.setRealm(realmName3); + rep3.setDisplayName(displayName3); + adminClient.realms().create(rep3); + + try { + runOnServer.run(session -> { + RealmProvider realmProvider = session.realms(); + + List realmNames = realmProvider.getRealmsStream("Unique") + .map(RealmModel::getName) + .collect(Collectors.toList()); + + assertTrue(realmNames.contains(realmName1)); + assertFalse(realmNames.contains(realmName2)); + assertTrue(realmNames.contains(realmName3)); + + List realmNames2 = realmProvider.getRealmsStream("XYZ") + .map(RealmModel::getName) + .collect(Collectors.toList()); + + assertFalse(realmNames2.contains(realmName1)); + assertTrue(realmNames2.contains(realmName2)); + assertFalse(realmNames2.contains(realmName3)); + + List realmNames3 = realmProvider.getRealmsStream(realmName1) + .map(RealmModel::getName) + .collect(Collectors.toList()); + + assertTrue(realmNames3.contains(realmName1)); + }); + } finally { + adminClient.realm(realmName1).remove(); + adminClient.realm(realmName2).remove(); + adminClient.realm(realmName3).remove(); + } + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmUpdateTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmUpdateTest.java index c0957f95d1c..0e0fc9ffe2e 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmUpdateTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/realm/RealmUpdateTest.java @@ -257,11 +257,13 @@ public class RealmUpdateTest extends AbstractRealmTest { assertThat(adminClient.realm(realmName).components().query(null, UserProfileProvider.class.getName()), empty()); - rep.setDisplayName("displayName"); + String displayName = "displayName"; + rep.setDisplayName(displayName); adminClient.realm(realmName).update(rep); // this used to return non-empty collection assertThat(adminClient.realm(realmName).components().query(null, UserProfileProvider.class.getName()), empty()); + assertEquals(displayName, adminClient.realm(realmName).toRepresentation().getDisplayName()); adminClient.realms().realm(realmName).remove(); }