Inconsistent Wildcard handling for JPA (#21671)

* Inconsistent Wildcard handling for JPA

Closes #20610

Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Vlasta Ramik 2023-07-27 17:03:22 +02:00 committed by GitHub
parent 0a7fcf43fd
commit 29b67fc8df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 53 additions and 30 deletions

View file

@ -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.

View file

@ -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]);

View file

@ -42,6 +42,8 @@ import org.keycloak.storage.StorageId;
*/
public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUserEntity, UserModel, JpaUserModelCriteriaBuilder> {
private static final char ESCAPE_BACKSLASH = '\\';
public JpaUserModelCriteriaBuilder() {
super(JpaUserModelCriteriaBuilder::new);
}
@ -189,7 +191,7 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUser
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
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<JpaUser
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
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)
)
)

View file

@ -784,6 +784,7 @@ public class MapUserProvider implements UserProvider {
return r;
}
@SuppressWarnings("unchecked")
private DefaultModelCriteria<UserModel> addSearchToModelCriteria(RealmModel realm, String value,
DefaultModelCriteria<UserModel> 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(

View file

@ -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<UserRepresentation> 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

View file

@ -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");