From ac6fdcd7cfaad72f759d812d29e33e5bc7259c17 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:41:08 +0300 Subject: [PATCH] GH-14: don't use lower() to use built in index --- .../jpa/EncryptedUserProvider.java | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/main/java/my/unifi/eset/keycloak/piidataencryption/jpa/EncryptedUserProvider.java b/src/main/java/my/unifi/eset/keycloak/piidataencryption/jpa/EncryptedUserProvider.java index f44de5d..02a2114 100644 --- a/src/main/java/my/unifi/eset/keycloak/piidataencryption/jpa/EncryptedUserProvider.java +++ b/src/main/java/my/unifi/eset/keycloak/piidataencryption/jpa/EncryptedUserProvider.java @@ -18,6 +18,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -28,6 +36,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.jpa.JpaUserProvider; +import org.keycloak.models.jpa.UserAdapter; +import org.keycloak.models.jpa.entities.UserAttributeEntity; +import org.keycloak.models.jpa.entities.UserEntity; public class EncryptedUserProvider extends JpaUserProvider { @@ -76,7 +87,85 @@ public Stream searchForUserStream(RealmModel realm, Map + * This bypasses JpaUserProvider's default LOWER() wrapping which is redundant + * for SHA-1 hash values (already lowercase hex) and prevents Postgres from + * using a standard B-tree index on the value column. + */ + private Stream searchByHashedAttributes(RealmModel realm, Map attributes, Integer firstResult, Integer maxResults) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(UserEntity.class); + Root root = cq.from(UserEntity.class); + + List predicates = new ArrayList<>(); + predicates.add(cb.equal(root.get("realmId"), realm.getId())); + + // Exclude service accounts unless explicitly included + if (!Boolean.parseBoolean(attributes.getOrDefault(UserModel.INCLUDE_SERVICE_ACCOUNT, Boolean.FALSE.toString()))) { + predicates.add(cb.isNull(root.get("serviceAccountClientLink"))); + } + + for (Map.Entry entry : attributes.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (value == null || value.isEmpty()) continue; + + switch (key) { + case UserModel.SEARCH: + // Hash was computed from the full search term — exact match across all user fields + predicates.add(cb.or( + cb.equal(root.get("username"), value), + cb.equal(root.get("email"), value), + cb.equal(root.get("firstName"), value), + cb.equal(root.get("lastName"), value) + )); + break; + case "username": + predicates.add(cb.equal(root.get("username"), value)); + break; + case "email": + predicates.add(cb.equal(root.get("email"), value)); + break; + case "firstName": + predicates.add(cb.equal(root.get("firstName"), value)); + break; + case "lastName": + predicates.add(cb.equal(root.get("lastName"), value)); + break; + case UserModel.ENABLED: + predicates.add(cb.equal(root.get("enabled"), Boolean.parseBoolean(value))); + break; + case UserModel.EMAIL_VERIFIED: + predicates.add(cb.equal(root.get("emailVerified"), Boolean.parseBoolean(value))); + break; + case UserModel.EXACT: + case UserModel.INCLUDE_SERVICE_ACCOUNT: + // Control flags, not search values + break; + default: + // Custom attribute — exact match on hashed value, no LOWER() + Join attrJoin = root.join("attributes", JoinType.INNER); + predicates.add(cb.and( + cb.equal(attrJoin.get("name"), key), + cb.equal(attrJoin.get("value"), value) + )); + break; + } + } + + cq.select(root).distinct(true).where(predicates.toArray(new Predicate[0])); + + TypedQuery tq = em.createQuery(cq); + if (firstResult != null && firstResult >= 0) tq.setFirstResult(firstResult); + if (maxResults != null && maxResults >= 0) tq.setMaxResults(maxResults); + + return tq.getResultStream() + .map(entity -> new UserAdapter(ks, realm, em, entity)); } @Override