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`,
|
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,
|
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.
|
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 USERNAME = "username";
|
||||||
private static final String FIRST_NAME = "firstName";
|
private static final String FIRST_NAME = "firstName";
|
||||||
private static final String LAST_NAME = "lastName";
|
private static final String LAST_NAME = "lastName";
|
||||||
|
private static final char ESCAPE_BACKSLASH = '\\';
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
protected EntityManager em;
|
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(FIRST_NAME)), value));
|
||||||
orPredicates.add(builder.equal(builder.lower(from.get(LAST_NAME)), value));
|
orPredicates.add(builder.equal(builder.lower(from.get(LAST_NAME)), value));
|
||||||
} else {
|
} else {
|
||||||
if (value.length() >= 2 && value.charAt(0) == '*' && value.charAt(value.length() - 1) == '*') {
|
value = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
|
||||||
// infix search
|
value = value.replace("*", "%");
|
||||||
value = "%" + value.substring(1, value.length() - 1) + "%";
|
if (value.isEmpty() || value.charAt(value.length() - 1) != '%') value += "%";
|
||||||
} else {
|
|
||||||
// default to prefix search
|
|
||||||
if (value.length() > 0 && value.charAt(value.length() - 1) == '*') {
|
|
||||||
value = value.substring(0, value.length() - 1);
|
|
||||||
}
|
|
||||||
value += "%";
|
|
||||||
}
|
|
||||||
|
|
||||||
orPredicates.add(builder.like(from.get(USERNAME), value));
|
orPredicates.add(builder.like(from.get(USERNAME), value, ESCAPE_BACKSLASH));
|
||||||
orPredicates.add(builder.like(from.get(EMAIL), value));
|
orPredicates.add(builder.like(from.get(EMAIL), value, ESCAPE_BACKSLASH));
|
||||||
orPredicates.add(builder.like(builder.lower(from.get(FIRST_NAME)), value));
|
orPredicates.add(builder.like(builder.lower(from.get(FIRST_NAME)), value, ESCAPE_BACKSLASH));
|
||||||
orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value));
|
orPredicates.add(builder.like(builder.lower(from.get(LAST_NAME)), value, ESCAPE_BACKSLASH));
|
||||||
}
|
}
|
||||||
|
|
||||||
return orPredicates.toArray(new Predicate[0]);
|
return orPredicates.toArray(new Predicate[0]);
|
||||||
|
|
|
@ -42,6 +42,8 @@ import org.keycloak.storage.StorageId;
|
||||||
*/
|
*/
|
||||||
public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUserEntity, UserModel, JpaUserModelCriteriaBuilder> {
|
public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUserEntity, UserModel, JpaUserModelCriteriaBuilder> {
|
||||||
|
|
||||||
|
private static final char ESCAPE_BACKSLASH = '\\';
|
||||||
|
|
||||||
public JpaUserModelCriteriaBuilder() {
|
public JpaUserModelCriteriaBuilder() {
|
||||||
super(JpaUserModelCriteriaBuilder::new);
|
super(JpaUserModelCriteriaBuilder::new);
|
||||||
}
|
}
|
||||||
|
@ -189,7 +191,7 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUser
|
||||||
validateValue(value, modelField, op, String.class);
|
validateValue(value, modelField, op, String.class);
|
||||||
|
|
||||||
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
|
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 {
|
} else {
|
||||||
|
@ -203,11 +205,11 @@ public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUser
|
||||||
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
|
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
|
||||||
cb.or(
|
cb.or(
|
||||||
cb.and(
|
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.ge(root.get("entityVersion"), 2)
|
||||||
),
|
),
|
||||||
cb.and(
|
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)
|
cb.le(root.get("entityVersion"), 1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -784,6 +784,7 @@ public class MapUserProvider implements UserProvider {
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
private DefaultModelCriteria<UserModel> addSearchToModelCriteria(RealmModel realm, String value,
|
private DefaultModelCriteria<UserModel> addSearchToModelCriteria(RealmModel realm, String value,
|
||||||
DefaultModelCriteria<UserModel> mcb) {
|
DefaultModelCriteria<UserModel> mcb) {
|
||||||
|
|
||||||
|
@ -791,16 +792,9 @@ public class MapUserProvider implements UserProvider {
|
||||||
// exact search
|
// exact search
|
||||||
value = value.substring(1, value.length() - 1);
|
value = value.substring(1, value.length() - 1);
|
||||||
} else {
|
} else {
|
||||||
if (value.length() >= 2 && value.charAt(0) == '*' && value.charAt(value.length() - 1) == '*') {
|
value = value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
|
||||||
// infix search
|
value = value.replace("*", "%");
|
||||||
value = "%" + value.substring(1, value.length() - 1) + "%";
|
if (value.isEmpty() || value.charAt(value.length() - 1) != '%') value += "%";
|
||||||
} else {
|
|
||||||
// default to prefix search
|
|
||||||
if (value.length() > 0 && value.charAt(value.length() - 1) == '*') {
|
|
||||||
value = value.substring(0, value.length() - 1);
|
|
||||||
}
|
|
||||||
value += "%";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mcb.or(
|
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.RoleMappingResource;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
import org.keycloak.admin.client.resource.UsersResource;
|
import org.keycloak.admin.client.resource.UsersResource;
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.common.Profile.Feature;
|
import org.keycloak.common.Profile.Feature;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.Base64;
|
import org.keycloak.common.util.Base64;
|
||||||
|
@ -1136,11 +1135,24 @@ public class UserTest extends AbstractAdminTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void circumfixSearchNotSupported() {
|
public void circumfixSearch() {
|
||||||
createUsers();
|
createUsers();
|
||||||
|
|
||||||
List<UserRepresentation> users = realm.users().search("u*name", null, null);
|
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
|
@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
|
@Test
|
||||||
public void searchUserDefaultSettings() throws Exception {
|
public void searchUserDefaultSettings() throws Exception {
|
||||||
createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com");
|
createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com");
|
||||||
|
|
Loading…
Reference in a new issue