mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
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:
parent
d03bba598c
commit
41f3f095b2
8 changed files with 298 additions and 6 deletions
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue