diff --git a/docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc b/docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc index 5d589d8b23..200aeaeb7e 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc +++ b/docs/documentation/upgrading/topics/keycloak/changes-23_0_0.adoc @@ -11,3 +11,12 @@ In these cases, it may be useful to disable adding the `iss` parameter to the au for the particular client in the {project_name} Admin console, in client details in the section with `OpenID Connect Compatibility Modes`, described in <<_compatibility_with_older_adapters>>. Dedicated `Exclude Issuer From Authentication Response` switch exists, which can be turned on to prevent adding the `iss` parameter to the authentication response. + += Wildcard characters handling + +JPA allows wildcards `%` and `_` when searching, while other providers like LDAP allow only `*`. +As `*` is a natural wildcard character in LDAP, it works in all places, while with JPA it only +worked at the beginning and the end of the search string. Starting with this release the only +wildcard character is `*` which work consistently across all providers in all places in the search +string. All special characters in a specific provider like `%` and `_` for JPA are escaped. For exact +search, with added quotes e.g. `"w*ord"`, the behavior remains the same as in previous releases. diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 7b4c29e844..0205ed068f 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -88,6 +88,7 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { private static final String USERNAME = "username"; private static final String FIRST_NAME = "firstName"; private static final String LAST_NAME = "lastName"; + private static final char ESCAPE_BACKSLASH = '\\'; private final KeycloakSession session; protected EntityManager em; @@ -910,21 +911,14 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { orPredicates.add(builder.equal(builder.lower(from.get(FIRST_NAME)), value)); orPredicates.add(builder.equal(builder.lower(from.get(LAST_NAME)), value)); } else { - if (value.length() >= 2 && value.charAt(0) == '*' && value.charAt(value.length() - 1) == '*') { - // infix search - value = "%" + value.substring(1, value.length() - 1) + "%"; - } else { - // default to prefix search - if (value.length() > 0 && value.charAt(value.length() - 1) == '*') { - value = value.substring(0, value.length() - 1); - } - value += "%"; - } + value = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_"); + value = value.replace("*", "%"); + if (value.isEmpty() || value.charAt(value.length() - 1) != '%') value += "%"; - orPredicates.add(builder.like(from.get(USERNAME), value)); - orPredicates.add(builder.like(from.get(EMAIL), value)); - orPredicates.add(builder.like(builder.lower(from.get(FIRST_NAME)), value)); - orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value)); + orPredicates.add(builder.like(from.get(USERNAME), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(from.get(EMAIL), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(builder.lower(from.get(FIRST_NAME)), value, ESCAPE_BACKSLASH)); + orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value, ESCAPE_BACKSLASH)); } return orPredicates.toArray(new Predicate[0]); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java index 7bfc71ebb2..b7706f97c1 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserModelCriteriaBuilder.java @@ -42,6 +42,8 @@ import org.keycloak.storage.StorageId; */ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder { + private static final char ESCAPE_BACKSLASH = '\\'; + public JpaUserModelCriteriaBuilder() { super(JpaUserModelCriteriaBuilder::new); } @@ -189,7 +191,7 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder - cb.like(root.get("username"), value[0].toString().toLowerCase()) + cb.like(root.get("username"), value[0].toString().toLowerCase(), ESCAPE_BACKSLASH) ); } else { @@ -203,11 +205,11 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder cb.or( cb.and( - cb.like(root.get("usernameWithCase"), value[0].toString()), + cb.like(root.get("usernameWithCase"), value[0].toString(), ESCAPE_BACKSLASH), cb.ge(root.get("entityVersion"), 2) ), cb.and( - cb.like(root.get("username"), value[0].toString()), + cb.like(root.get("username"), value[0].toString(), ESCAPE_BACKSLASH), cb.le(root.get("entityVersion"), 1) ) ) diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java index 35024ffe4e..cfce47760c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -784,6 +784,7 @@ public class MapUserProvider implements UserProvider { return r; } + @SuppressWarnings("unchecked") private DefaultModelCriteria addSearchToModelCriteria(RealmModel realm, String value, DefaultModelCriteria mcb) { @@ -791,16 +792,9 @@ public class MapUserProvider implements UserProvider { // exact search value = value.substring(1, value.length() - 1); } else { - if (value.length() >= 2 && value.charAt(0) == '*' && value.charAt(value.length() - 1) == '*') { - // infix search - value = "%" + value.substring(1, value.length() - 1) + "%"; - } else { - // default to prefix search - if (value.length() > 0 && value.charAt(value.length() - 1) == '*') { - value = value.substring(0, value.length() - 1); - } - value += "%"; - } + value = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_"); + value = value.replace("*", "%"); + if (value.isEmpty() || value.charAt(value.length() - 1) != '%') value += "%"; } return mcb.or( diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index fed8dda628..e117cac25b 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -33,7 +33,6 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleMappingResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; import org.keycloak.common.VerificationException; import org.keycloak.common.util.Base64; @@ -1136,11 +1135,24 @@ public class UserTest extends AbstractAdminTest { } @Test - public void circumfixSearchNotSupported() { + public void circumfixSearch() { createUsers(); List users = realm.users().search("u*name", null, null); - assertThat(users, hasSize(0)); + assertThat(users, hasSize(9)); + } + + @Test + public void wildcardSearch() { + createUser("0user\\\\0", "email0@emal"); + createUser("1user\\\\", "email1@emal"); + createUser("2user\\\\%", "email2@emal"); + createUser("3user\\\\*", "email3@emal"); + createUser("4user\\\\_", "email4@emal"); + + assertThat(realm.users().search("*", null, null), hasSize(5)); + assertThat(realm.users().search("*user\\", null, null), hasSize(5)); + assertThat(realm.users().search("\"2user\\\\%\"", null, null), hasSize(1)); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java index 84e34b719f..4b5ba696a8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java @@ -69,6 +69,18 @@ public class UsersTest extends AbstractAdminTest { } } + @Test + public void searchUserWithWildcards() throws Exception { + createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com"); + + assertThat(adminClient.realm(REALM_NAME).users().search("Use%", null, null), hasSize(0)); + assertThat(adminClient.realm(REALM_NAME).users().search("Use_", null, null), hasSize(0)); + assertThat(adminClient.realm(REALM_NAME).users().search("Us_r", null, null), hasSize(0)); + assertThat(adminClient.realm(REALM_NAME).users().search("Use", null, null), hasSize(1)); + assertThat(adminClient.realm(REALM_NAME).users().search("Use*", null, null), hasSize(1)); + assertThat(adminClient.realm(REALM_NAME).users().search("Us*e", null, null), hasSize(1)); + } + @Test public void searchUserDefaultSettings() throws Exception { createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com");