Feature Search realms by "DisplayName" in the "Manage realms" view

Closes keycloak#45292

Signed-off-by: tre2man <kimtree3940@gmail.com>
This commit is contained in:
tre2man 2026-01-25 22:19:20 +09:00
parent d03bba598c
commit 41f3f095b2
8 changed files with 298 additions and 6 deletions

View file

@ -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: <a href="https://github.com/keycloak/keycloak/issues/45292">keycloak#45292</a>
*
* @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";
}
}

View file

@ -144,12 +144,13 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
@Override
public String getDisplayName() {
return getAttribute(RealmAttributes.DISPLAY_NAME);
return realm.getDisplayName();
}
@Override
public void setDisplayName(String displayName) {
setAttribute(RealmAttributes.DISPLAY_NAME, displayName);
realm.setDisplayName(displayName);
em.flush();
}
@Override

View file

@ -22,8 +22,6 @@ package org.keycloak.models.jpa.entities;
*/
public interface RealmAttributes {
String DISPLAY_NAME = "displayName";
String DISPLAY_NAME_HTML = "displayNameHtml";
String ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN = "actionTokenGeneratedByAdminLifespan";

View file

@ -50,7 +50,7 @@ import jakarta.persistence.Table;
@Entity
@NamedQueries({
@NamedQuery(name="getAllRealmIds", query="select realm.id from RealmEntity realm"),
@NamedQuery(name="getRealmIdsWithNameContaining", query="select realm.id from RealmEntity realm where LOWER(realm.name) like CONCAT('%', LOWER(:search), '%')"),
@NamedQuery(name="getRealmIdsWithNameContaining", query="select realm.id from RealmEntity realm where LOWER(realm.name) like CONCAT('%', LOWER(:search), '%') or LOWER(realm.displayName) like CONCAT('%', LOWER(:search), '%')"),
@NamedQuery(name="getRealmIdByName", query="select realm.id from RealmEntity realm where realm.name = :name"),
@NamedQuery(name="getRealmIdsWithProviderType", query="select distinct c.realm.id from ComponentEntity c where c.providerType = :providerType"),
})
@ -138,6 +138,9 @@ public class RealmEntity {
@Column(name="EMAIL_THEME")
protected String emailTheme;
@Column(name="DISPLAY_NAME")
protected String displayName;
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm", fetch = FetchType.EAGER)
Collection<RealmAttributeEntity> 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;
}

View file

@ -97,4 +97,14 @@
</createIndex>
</changeSet>
<changeSet author="keycloak" id="26.6.0-45292-add-realm-display-name-column">
<addColumn tableName="REALM">
<column name="DISPLAY_NAME" type="VARCHAR(255)"/>
</addColumn>
</changeSet>
<changeSet author="keycloak" id="26.6.0-45292-migrate-realm-display-name">
<customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.JpaUpdate26_6_0_MigrateRealmDisplayName"/>
</changeSet>
</databaseChangeLog>

View file

@ -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();
}
}
}

View file

@ -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<String> 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<String> 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<String> realmNames = realmProvider.getRealmsStream("Unique")
.map(RealmModel::getName)
.collect(Collectors.toList());
assertTrue(realmNames.contains(realmName1));
assertFalse(realmNames.contains(realmName2));
assertTrue(realmNames.contains(realmName3));
List<String> realmNames2 = realmProvider.getRealmsStream("XYZ")
.map(RealmModel::getName)
.collect(Collectors.toList());
assertFalse(realmNames2.contains(realmName1));
assertTrue(realmNames2.contains(realmName2));
assertFalse(realmNames2.contains(realmName3));
List<String> 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();
}
}
}

View file

@ -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();
}