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:
parent
0a7fcf43fd
commit
29b67fc8df
6 changed files with 53 additions and 30 deletions
|
@ -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.
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in a new issue