diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java index 5e9392e070..ecb980d49c 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java @@ -132,6 +132,7 @@ public class HotRodMapStorage iterator = query.iterator(); return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)) .onClose(iterator::close) + .filter(Objects::nonNull) // see https://github.com/keycloak/keycloak/issues/9271 .map(this.delegateProducer); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java index 51ea1aefd5..ac2420f573 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java @@ -19,6 +19,8 @@ package org.keycloak.models.map.storage.hotRod; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; import org.keycloak.models.map.storage.ModelCriteriaBuilder; import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; import org.keycloak.storage.SearchableModelField; @@ -41,7 +43,11 @@ public class IckleQueryMapModelCriteriaBuilder hotRodEntityClass; private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY); private final Map parameters; + private static final String NON_ANALYZED_FIELD_REGEX = "[%_\\\\]"; + private static final String ANALYZED_FIELD_REGEX = "[+!^\"~*?:\\\\]"; public static final Map, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>(); + public static final Set> ANALYZED_MODEL_FIELDS = new HashSet<>(); + static { INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, "scopeMappings"); @@ -51,6 +57,14 @@ public class IckleQueryMapModelCriteriaBuilder hotRodEntityClass, StringBuilder whereClauseBuilder, Map parameters) { this.hotRodEntityClass = hotRodEntityClass; this.whereClauseBuilder.append(whereClauseBuilder); @@ -175,6 +189,43 @@ public class IckleQueryMapModelCriteriaBuilder modelField) { + return ANALYZED_MODEL_FIELDS.contains(modelField); + } + /** * * @return Ickle query that represents this QueryBuilder diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java index abae9212d3..00aa690b53 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java @@ -56,6 +56,8 @@ public class IckleQueryOperators { OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.IN, IckleQueryOperators::in); OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.EXISTS, IckleQueryOperators::exists); OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.NOT_EXISTS, IckleQueryOperators::notExists); + OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.ILIKE, IckleQueryOperators::iLike); + OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.LIKE, IckleQueryOperators::like); OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.EQ, "="); OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.NE, "!="); @@ -82,17 +84,29 @@ public class IckleQueryOperators { String combine(String fieldName, Object[] values, Map parameters); } - private static String exists(String modelField, Object[] values, Map parameters) { - String field = C + "." + modelField; + private static String exists(String modelFieldName, Object[] values, Map parameters) { + String field = C + "." + modelFieldName; return field + " IS NOT NULL AND " + field + " IS NOT EMPTY"; } - private static String notExists(String modelField, Object[] values, Map parameters) { - String field = C + "." + modelField; + private static String notExists(String modelFieldName, Object[] values, Map parameters) { + String field = C + "." + modelFieldName; return field + " IS NULL OR " + field + " IS EMPTY"; } - private static String in(String modelField, Object[] values, Map parameters) { + private static String iLike(String modelFieldName, Object[] values, Map parameters) { + String sanitizedValue = (String) IckleQueryMapModelCriteriaBuilder.sanitize(values[0]); + return singleValueOperator(ModelCriteriaBuilder.Operator.ILIKE) + .combine(modelFieldName + "Lowercase", new String[] {sanitizedValue.toLowerCase()}, parameters); + } + + private static String like(String modelFieldName, Object[] values, Map parameters) { + String sanitizedValue = (String) IckleQueryMapModelCriteriaBuilder.sanitize(values[0]); + return singleValueOperator(ModelCriteriaBuilder.Operator.LIKE) + .combine(modelFieldName, new String[] {sanitizedValue}, parameters); + } + + private static String in(String modelFieldName, Object[] values, Map parameters) { if (values == null || values.length == 0) { return "false"; } @@ -113,9 +127,9 @@ public class IckleQueryOperators { operands = new HashSet<>(Arrays.asList(values)); } - return operands.isEmpty() ? "false" : C + "." + modelField + " IN (" + operands.stream() + return operands.isEmpty() ? "false" : C + "." + modelFieldName + " IN (" + operands.stream() .map(operand -> { - String namedParam = findAvailableNamedParam(parameters.keySet(), modelField); + String namedParam = findAvailableNamedParam(parameters.keySet(), modelFieldName); parameters.put(namedParam, operand); return ":" + namedParam; }) @@ -177,13 +191,13 @@ public class IckleQueryOperators { * Provides a string containing where clause for given operator, field name and values * * @param op operator - * @param filedName field name + * @param modelFieldName field name * @param values values * @param parameters mapping between named parameters and their values * @return where clause */ - public static String combineExpressions(ModelCriteriaBuilder.Operator op, String filedName, Object[] values, Map parameters) { - return operatorToExpressionCombinator(op).combine(filedName, values, parameters); + public static String combineExpressions(ModelCriteriaBuilder.Operator op, String modelFieldName, Object[] values, Map parameters) { + return operatorToExpressionCombinator(op).combine(modelFieldName, values, parameters); } -} \ No newline at end of file +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java index 6bcafa853d..16a2e8a4df 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java @@ -26,6 +26,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import static org.keycloak.models.map.storage.hotRod.IckleQueryMapModelCriteriaBuilder.sanitizeAnalyzed; +import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.C; + /** * This class provides knowledge on how to build Ickle query where clauses for specified {@link SearchableModelField}. * @@ -71,8 +74,20 @@ public class IckleQueryWhereClauses { */ public static String produceWhereClause(SearchableModelField modelField, ModelCriteriaBuilder.Operator op, Object[] values, Map parameters) { - return whereClauseProducerForModelField(modelField) - .produceWhereClause(IckleQueryMapModelCriteriaBuilder.getFieldName(modelField), op, values, parameters); + String fieldName = IckleQueryMapModelCriteriaBuilder.getFieldName(modelField); + + if (IckleQueryMapModelCriteriaBuilder.isAnalyzedModelField(modelField) && + (op.equals(ModelCriteriaBuilder.Operator.ILIKE) || op.equals(ModelCriteriaBuilder.Operator.EQ) || op.equals(ModelCriteriaBuilder.Operator.NE))) { + + String clause = C + "." + fieldName + " : '" + sanitizeAnalyzed(values[0]) + "'"; + if (op.equals(ModelCriteriaBuilder.Operator.NE)) { + return "not(" + clause + ")"; + } + + return clause; + } + + return whereClauseProducerForModelField(modelField).produceWhereClause(fieldName, op, values, parameters); } private static String whereClauseForClientsAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map parameters) { diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java index bb6a8f7ec7..629d379bd7 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodClientEntity.java @@ -17,9 +17,11 @@ package org.keycloak.models.map.storage.hotRod.client; +import org.infinispan.protostream.annotations.ProtoDoc; import org.infinispan.protostream.annotations.ProtoField; import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodAttributeEntity; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodPair; import org.keycloak.models.map.client.MapClientEntity; @@ -37,111 +39,125 @@ import java.util.stream.Stream; implementInterface = "org.keycloak.models.map.client.MapClientEntity", inherits = "org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity.AbstractHotRodClientEntityDelegate" ) +@ProtoDoc("@Indexed") public class HotRodClientEntity implements AbstractHotRodEntity { @ProtoField(number = 1, required = true) public int entityVersion = 1; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 2, required = true) public String id; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 3) public String realmId; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 4) public String clientId; + /** + * Lowercase interpretation of {@link #clientId} field. Infinispan doesn't support case-insensitive LIKE for non-analyzed fields. + * Search on analyzed fields can be case-insensitive (based on used analyzer) but doesn't support ORDER BY analyzed field. + */ + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 5) - public String name; + public String clientIdLowercase; @ProtoField(number = 6) - public String description; + public String name; @ProtoField(number = 7) - public Set redirectUris; + public String description; @ProtoField(number = 8) - public Boolean enabled; + public Set redirectUris; @ProtoField(number = 9) + public Boolean enabled; + + @ProtoField(number = 10) public Boolean alwaysDisplayInConsole; - @ProtoField(number = 10) + @ProtoField(number = 11) public String clientAuthenticatorType; - @ProtoField(number = 11) + @ProtoField(number = 12) public String secret; - @ProtoField(number = 12) + @ProtoField(number = 13) public String registrationToken; - @ProtoField(number = 13) + @ProtoField(number = 14) public String protocol; - @ProtoField(number = 14) + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + @ProtoField(number = 15) public Set attributes; - @ProtoField(number = 15) + @ProtoField(number = 16) public Set> authenticationFlowBindingOverrides; - @ProtoField(number = 16) + @ProtoField(number = 17) public Boolean publicClient; - @ProtoField(number = 17) + @ProtoField(number = 18) public Boolean fullScopeAllowed; - @ProtoField(number = 18) + @ProtoField(number = 19) public Boolean frontchannelLogout; - @ProtoField(number = 19) + @ProtoField(number = 20) public Integer notBefore; - @ProtoField(number = 20) + @ProtoField(number = 21) public Set scope; - @ProtoField(number = 21) + @ProtoField(number = 22) public Set webOrigins; - @ProtoField(number = 22) + @ProtoField(number = 23) public Set protocolMappers; - @ProtoField(number = 23) + @ProtoField(number = 24) public Set> clientScopes; - @ProtoField(number = 24, collectionImplementation = LinkedList.class) + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + @ProtoField(number = 25, collectionImplementation = LinkedList.class) public Collection scopeMappings; - @ProtoField(number = 25) + @ProtoField(number = 26) public Boolean surrogateAuthRequired; - @ProtoField(number = 26) + @ProtoField(number = 27) public String managementUrl; - @ProtoField(number = 27) + @ProtoField(number = 28) public String baseUrl; - @ProtoField(number = 28) + @ProtoField(number = 29) public Boolean bearerOnly; - @ProtoField(number = 29) + @ProtoField(number = 30) public Boolean consentRequired; - @ProtoField(number = 30) + @ProtoField(number = 31) public String rootUrl; - @ProtoField(number = 31) + @ProtoField(number = 32) public Boolean standardFlowEnabled; - @ProtoField(number = 32) + @ProtoField(number = 33) public Boolean implicitFlowEnabled; - @ProtoField(number = 33) + @ProtoField(number = 34) public Boolean directAccessGrantsEnabled; - @ProtoField(number = 34) + @ProtoField(number = 35) public Boolean serviceAccountsEnabled; - @ProtoField(number = 35) + @ProtoField(number = 36) public Integer nodeReRegistrationTimeout; public static abstract class AbstractHotRodClientEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate, MapClientEntity { @@ -159,6 +175,14 @@ public class HotRodClientEntity implements AbstractHotRodEntity { this.updated |= id != null; } + @Override + public void setClientId(String clientId) { + HotRodClientEntity entity = getHotRodEntity(); + this.updated |= ! Objects.equals(entity.clientId, clientId); + entity.clientId = clientId; + entity.clientIdLowercase = clientId == null ? null : clientId.toLowerCase(); + } + @Override public Stream getClientScopes(boolean defaultScope) { final Map clientScopes = getClientScopes(); diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodAttributeEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodAttributeEntity.java similarity index 87% rename from model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodAttributeEntity.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodAttributeEntity.java index ca5ded6c02..fabacb4f7d 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/client/HotRodAttributeEntity.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodAttributeEntity.java @@ -15,18 +15,22 @@ * limitations under the License. */ -package org.keycloak.models.map.storage.hotRod.client; +package org.keycloak.models.map.storage.hotRod.common; +import org.infinispan.protostream.annotations.ProtoDoc; import org.infinispan.protostream.annotations.ProtoField; import java.util.LinkedList; import java.util.List; import java.util.Objects; +@ProtoDoc("@Indexed") public class HotRodAttributeEntity { + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 1) public String name; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 2) public List values = new LinkedList<>(); diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodAttributeEntityNonIndexed.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodAttributeEntityNonIndexed.java new file mode 100644 index 0000000000..746ab20dda --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodAttributeEntityNonIndexed.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod.common; + +import org.infinispan.protostream.annotations.ProtoDoc; +import org.infinispan.protostream.annotations.ProtoField; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + + +public class HotRodAttributeEntityNonIndexed { + @ProtoField(number = 1) + public String name; + + @ProtoField(number = 2) + public List values = new LinkedList<>(); + + public HotRodAttributeEntityNonIndexed() { + } + + public HotRodAttributeEntityNonIndexed(String name, List values) { + this.name = name; + this.values.addAll(values); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getValues() { + return values; + } + + public void setValues(List values) { + this.values = values; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + HotRodAttributeEntityNonIndexed that = (HotRodAttributeEntityNonIndexed) o; + return Objects.equals(name, that.name) && Objects.equals(values, that.values); + } + + @Override + public int hashCode() { + return Objects.hash(name, values); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java index 0a852eaa0b..7421180bba 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java @@ -18,7 +18,6 @@ package org.keycloak.models.map.storage.hotRod.common; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.storage.hotRod.client.HotRodAttributeEntity; import java.util.List; import java.util.Map; @@ -48,6 +47,10 @@ public class HotRodTypesUtils { return new HotRodAttributeEntity(entry.getKey(), entry.getValue()); } + public static HotRodAttributeEntityNonIndexed createHotRodAttributeEntityNonIndexedFromMapEntry(Map.Entry> entry) { + return new HotRodAttributeEntityNonIndexed(entry.getKey(), entry.getValue()); + } + public static boolean removeFromSetByMapKey(Set set, KeyType key, Function keyGetter) { if (set == null || set.isEmpty()) { return false; } return set.stream() @@ -73,10 +76,18 @@ public class HotRodTypesUtils { return attributeEntity.name; } + public static String getKey(HotRodAttributeEntityNonIndexed attributeEntity) { + return attributeEntity.name; + } + public static List getValue(HotRodAttributeEntity attributeEntity) { return attributeEntity.values; } + public static List getValue(HotRodAttributeEntityNonIndexed attributeEntity) { + return attributeEntity.values; + } + public static String getKey(AbstractEntity entity) { return entity.getId(); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java index 2fffa6bc19..579bb50658 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java @@ -19,7 +19,6 @@ package org.keycloak.models.map.storage.hotRod.common; import org.infinispan.protostream.GeneratedSchema; import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder; -import org.keycloak.models.map.storage.hotRod.client.HotRodAttributeEntity; import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity; import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity; import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity; @@ -31,14 +30,15 @@ import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity; includeClasses = { // Clients HotRodClientEntity.class, - HotRodAttributeEntity.class, HotRodProtocolMapperEntity.class, // Groups HotRodGroupEntity.class, // Common - HotRodPair.class + HotRodPair.class, + HotRodAttributeEntity.class, + HotRodAttributeEntityNonIndexed.class }, schemaFileName = "KeycloakHotRodMapStorage.proto", schemaFilePath = "proto/", diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java index 52301ffc44..75a98cad92 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/connections/DefaultHotRodConnectionProviderFactory.java @@ -18,6 +18,7 @@ package org.keycloak.models.map.storage.hotRod.connections; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.client.hotrod.RemoteCacheManagerAdmin; import org.infinispan.client.hotrod.configuration.ClientIntelligence; import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; import org.infinispan.commons.marshall.ProtoStreamMarshaller; @@ -33,6 +34,9 @@ import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; /** * @author Martin Kanis @@ -104,14 +108,31 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP remoteBuilder.addContextInitializer(ProtoSchemaInitializer.INSTANCE); remoteCacheManager = new RemoteCacheManager(remoteBuilder.build()); + Set remoteCaches = HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream() + .map(HotRodEntityDescriptor::getCacheName).collect(Collectors.toSet()); + if (configureRemoteCaches) { // access the caches to force their creation - HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream() - .map(HotRodEntityDescriptor::getCacheName) - .forEach(remoteCacheManager::getCache); + remoteCaches.forEach(remoteCacheManager::getCache); } registerSchemata(ProtoSchemaInitializer.INSTANCE); + + RemoteCacheManagerAdmin administration = remoteCacheManager.administration(); + if (config.getBoolean("reindexAllCaches", false)) { + LOG.infof("Reindexing all caches. This can take a long time to complete. While the rebuild operation is in progress, queries might return fewer results."); + remoteCaches.forEach(administration::reindexCache); + } else { + String reindexCaches = config.get("reindexCaches", ""); + if (reindexCaches != null) { + Arrays.stream(reindexCaches.split(",")) + .map(String::trim) + .filter(e -> !e.isEmpty()) + .filter(remoteCaches::contains) + .peek(cacheName -> LOG.infof("Reindexing %s cache. This can take a long time to complete. While the rebuild operation is in progress, queries might return fewer results.", cacheName)) + .forEach(administration::reindexCache); + } + } } private void registerSchemata(GeneratedSchema initializer) { diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java index 3c5b733e9f..c3e52f30e7 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java @@ -17,20 +17,23 @@ package org.keycloak.models.map.storage.hotRod.group; +import org.infinispan.protostream.annotations.ProtoDoc; import org.infinispan.protostream.annotations.ProtoField; import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.group.MapGroupEntity; -import org.keycloak.models.map.storage.hotRod.client.HotRodAttributeEntity; import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodAttributeEntityNonIndexed; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; +import java.util.Objects; import java.util.Set; @GenerateHotRodEntityImplementation( implementInterface = "org.keycloak.models.map.group.MapGroupEntity", inherits = "org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity.AbstractHotRodGroupEntityDelegate" ) +@ProtoDoc("@Indexed") public class HotRodGroupEntity implements AbstractHotRodEntity { public static abstract class AbstractHotRodGroupEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate, MapGroupEntity { @@ -47,26 +50,47 @@ public class HotRodGroupEntity implements AbstractHotRodEntity { entity.id = id; this.updated |= id != null; } + + @Override + public void setName(String name) { + HotRodGroupEntity entity = getHotRodEntity(); + updated |= ! Objects.equals(entity.name, name); + entity.name = name; + entity.nameLowercase = name == null ? null : name.toLowerCase(); + } } @ProtoField(number = 1, required = true) public int entityVersion = 1; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 2, required = true) public String id; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 3) public String realmId; + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 4) public String name; + /** + * Lowercase interpretation of {@link #name} field. Infinispan doesn't support case-insensitive LIKE for non-analyzed fields. + * Search on analyzed fields can be case-insensitive (based on used analyzer) but doesn't support ORDER BY analyzed field. + */ + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") @ProtoField(number = 5) + public String nameLowercase; + + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + @ProtoField(number = 6) public String parentId; - @ProtoField(number = 6) - public Set attributes; - @ProtoField(number = 7) + public Set attributes; + + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + @ProtoField(number = 8) public Set grantedRoles; } diff --git a/model/map-hot-rod/src/main/resources/config/cacheConfig.xml b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml index a98ecd6af4..d63b26aa21 100644 --- a/model/map-hot-rod/src/main/resources/config/cacheConfig.xml +++ b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml @@ -4,9 +4,20 @@ + + + kc.HotRodClientEntity + kc.HotRodAttributeEntity + + + + + kc.HotRodGroupEntity + + diff --git a/model/map-hot-rod/src/main/resources/config/infinispan.xml b/model/map-hot-rod/src/main/resources/config/infinispan.xml index 5fe47bc163..44e6fc8aa0 100644 --- a/model/map-hot-rod/src/main/resources/config/infinispan.xml +++ b/model/map-hot-rod/src/main/resources/config/infinispan.xml @@ -6,9 +6,20 @@ + + + kc.HotRodClientEntity + kc.HotRodAttributeEntity + + + + + kc.HotRodGroupEntity + + diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java index 88516d195a..6fc4725d17 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java @@ -32,6 +32,8 @@ import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; + +import java.util.List; import java.util.Objects; import java.util.function.Function; import java.util.function.UnaryOperator; diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index e5e61c5b20..d3fcd9f1ac 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -259,7 +259,8 @@ "configureRemoteCaches": "${keycloak.connectionsHotRod.configureRemoteCaches:true}", "username": "${keycloak.connectionsHotRod.username:myuser}", "password": "${keycloak.connectionsHotRod.password:qwer1234!}", - "enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}" + "enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}", + "reindexCaches": "${keycloak.connectionsHotRod.reindexCaches:clients,groups}" } }, diff --git a/testsuite/model/src/main/resources/hotrod/infinispan.xml b/testsuite/model/src/main/resources/hotrod/infinispan.xml index 155a3001e3..623ff4b3dd 100644 --- a/testsuite/model/src/main/resources/hotrod/infinispan.xml +++ b/testsuite/model/src/main/resources/hotrod/infinispan.xml @@ -2,9 +2,20 @@ + + + kc.HotRodClientEntity + kc.HotRodAttributeEntity + + + + + kc.HotRodGroupEntity + + diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java index 8ca08de0c8..5093417fdc 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.model; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -31,6 +32,9 @@ import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.RoleProvider; +import java.util.Map; +import java.util.Set; + /** * * @author rmartinc @@ -42,6 +46,8 @@ public class ClientModelTest extends KeycloakModelTest { private String realmId; + private static final String searchClientId = "My ClIeNt WITH sP%Ces and sp*ci_l Ch***cters \" ?!"; + @Override public void createEnvironment(KeycloakSession s) { RealmModel realm = s.realms().createRealm("realm"); @@ -58,6 +64,12 @@ public class ClientModelTest extends KeycloakModelTest { public void testClientsBasics() { // Create client ClientModel originalModel = withRealm(realmId, (session, realm) -> session.clients().addClient(realm, "myClientId")); + ClientModel searchClient = withRealm(realmId, (session, realm) -> { + ClientModel client = session.clients().addClient(realm, searchClientId); + client.setAlwaysDisplayInConsole(true); + client.addRedirectUri("http://www.redirecturi.com"); + return client; + }); assertThat(originalModel.getId(), notNullValue()); // Find by id @@ -76,6 +88,49 @@ public class ClientModelTest extends KeycloakModelTest { assertThat(model.getClientId(), is(equalTo("myClientId"))); } + // Search by clientId + { + withRealm(realmId, (session, realm) -> { + ClientModel client = session.clients().searchClientsByClientIdStream(realm, "client with", 0, 10).findFirst().orElse(null); + assertThat(client, notNullValue()); + assertThat(client.getId(), is(equalTo(searchClient.getId()))); + assertThat(client.getClientId(), is(equalTo(searchClientId))); + return null; + }); + + + withRealm(realmId, (session, realm) -> { + ClientModel client = session.clients().searchClientsByClientIdStream(realm, "sp*ci_l Ch***cters", 0, 10).findFirst().orElse(null); + assertThat(client, notNullValue()); + assertThat(client.getId(), is(equalTo(searchClient.getId()))); + assertThat(client.getClientId(), is(equalTo(searchClientId))); + return null; + }); + + withRealm(realmId, (session, realm) -> { + ClientModel client = session.clients().searchClientsByClientIdStream(realm, " AND ", 0, 10).findFirst().orElse(null); + assertThat(client, notNullValue()); + assertThat(client.getId(), is(equalTo(searchClient.getId()))); + assertThat(client.getClientId(), is(equalTo(searchClientId))); + return null; + }); + + withRealm(realmId, (session, realm) -> { + ClientModel client = session.clients().searchClientsByClientIdStream(realm, "%", 0, 10).findFirst().orElse(null); + assertThat(client, notNullValue()); + assertThat(client.getId(), is(equalTo(searchClient.getId()))); + assertThat(client.getClientId(), is(equalTo(searchClientId))); + return null; + }); + } + + // using Boolean operand + { + Map> allRedirectUrisOfEnabledClients = withRealm(realmId, (session, realm) -> session.clients().getAllRedirectUrisOfEnabledClients(realm)); + assertThat(allRedirectUrisOfEnabledClients.values(), hasSize(1)); + assertThat(allRedirectUrisOfEnabledClients.keySet().iterator().next().getId(), is(equalTo(searchClient.getId()))); + } + // Test storing flow binding override { // Add some override