KEYCLOAK-19571 Add indices to HotRodClientEntity fields

This commit is contained in:
Martin Kanis 2021-12-01 21:39:07 +01:00 committed by Hynek Mlnařík
parent a6acc89bf3
commit ddcabe61b2
17 changed files with 383 additions and 56 deletions

View file

@ -132,6 +132,7 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRo
CloseableIterator<E> iterator = query.iterator(); CloseableIterator<E> iterator = query.iterator();
return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)) return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false))
.onClose(iterator::close) .onClose(iterator::close)
.filter(Objects::nonNull) // see https://github.com/keycloak/keycloak/issues/9271
.map(this.delegateProducer); .map(this.delegateProducer);
} }

View file

@ -19,6 +19,8 @@ package org.keycloak.models.map.storage.hotRod;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel; 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.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.storage.SearchableModelField; import org.keycloak.storage.SearchableModelField;
@ -41,7 +43,11 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
private final Class<E> hotRodEntityClass; private final Class<E> hotRodEntityClass;
private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY); private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
private final Map<String, Object> parameters; private final Map<String, Object> parameters;
private static final String NON_ANALYZED_FIELD_REGEX = "[%_\\\\]";
private static final String ANALYZED_FIELD_REGEX = "[+!^\"~*?:\\\\]";
public static final Map<SearchableModelField<?>, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>(); public static final Map<SearchableModelField<?>, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>();
public static final Set<SearchableModelField<?>> ANALYZED_MODEL_FIELDS = new HashSet<>();
static { static {
INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, "scopeMappings"); INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, "scopeMappings");
@ -51,6 +57,14 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
INFINISPAN_NAME_OVERRIDES.put(GroupModel.SearchableFields.ASSIGNED_ROLE, "grantedRoles"); INFINISPAN_NAME_OVERRIDES.put(GroupModel.SearchableFields.ASSIGNED_ROLE, "grantedRoles");
} }
static {
// the "filename" analyzer in Infinispan works correctly for case-insensitive search with whitespaces
ANALYZED_MODEL_FIELDS.add(RoleModel.SearchableFields.DESCRIPTION);
ANALYZED_MODEL_FIELDS.add(UserModel.SearchableFields.FIRST_NAME);
ANALYZED_MODEL_FIELDS.add(UserModel.SearchableFields.LAST_NAME);
ANALYZED_MODEL_FIELDS.add(UserModel.SearchableFields.EMAIL);
}
public IckleQueryMapModelCriteriaBuilder(Class<E> hotRodEntityClass, StringBuilder whereClauseBuilder, Map<String, Object> parameters) { public IckleQueryMapModelCriteriaBuilder(Class<E> hotRodEntityClass, StringBuilder whereClauseBuilder, Map<String, Object> parameters) {
this.hotRodEntityClass = hotRodEntityClass; this.hotRodEntityClass = hotRodEntityClass;
this.whereClauseBuilder.append(whereClauseBuilder); this.whereClauseBuilder.append(whereClauseBuilder);
@ -175,6 +189,43 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
return whereClauseBuilder; return whereClauseBuilder;
} }
public static Object sanitize(Object value) {
if (value instanceof String) {
String sValue = (String) value;
boolean anyBeginning = sValue.startsWith("%");
boolean anyEnd = sValue.endsWith("%");
String sanitizedString = sValue.substring(anyBeginning ? 1 : 0, sValue.length() - (anyEnd ? 1 : 0))
.replaceAll(NON_ANALYZED_FIELD_REGEX, "\\\\\\\\" + "$0");
return (anyBeginning ? "%" : "") + sanitizedString + (anyEnd ? "%" : "");
}
return value;
}
public static Object sanitizeAnalyzed(Object value) {
if (value instanceof String) {
String sValue = (String) value;
boolean anyBeginning = sValue.startsWith("%");
boolean anyEnd = sValue.endsWith("%");
String sanitizedString = sValue.substring(anyBeginning ? 1 : 0, sValue.length() - (anyEnd ? 1 : 0))
.replaceAll("\\\\", "\\\\\\\\"); // escape "\" with extra "\"
// .replaceAll(ANALYZED_FIELD_REGEX, "\\\\\\\\" + "$0"); skipped for now because Infinispan is not able to escape
// special characters for analyzed fields
// TODO reevaluate once https://github.com/keycloak/keycloak/issues/9295 is fixed
return (anyBeginning ? "*" : "") + sanitizedString + (anyEnd ? "*" : "");
}
return value;
}
public static boolean isAnalyzedModelField(SearchableModelField<?> modelField) {
return ANALYZED_MODEL_FIELDS.contains(modelField);
}
/** /**
* *
* @return Ickle query that represents this QueryBuilder * @return Ickle query that represents this QueryBuilder

View file

@ -56,6 +56,8 @@ public class IckleQueryOperators {
OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.IN, IckleQueryOperators::in); 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.EXISTS, IckleQueryOperators::exists);
OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.NOT_EXISTS, IckleQueryOperators::notExists); 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.EQ, "=");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.NE, "!="); OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.NE, "!=");
@ -82,17 +84,29 @@ public class IckleQueryOperators {
String combine(String fieldName, Object[] values, Map<String, Object> parameters); String combine(String fieldName, Object[] values, Map<String, Object> parameters);
} }
private static String exists(String modelField, Object[] values, Map<String, Object> parameters) { private static String exists(String modelFieldName, Object[] values, Map<String, Object> parameters) {
String field = C + "." + modelField; String field = C + "." + modelFieldName;
return field + " IS NOT NULL AND " + field + " IS NOT EMPTY"; return field + " IS NOT NULL AND " + field + " IS NOT EMPTY";
} }
private static String notExists(String modelField, Object[] values, Map<String, Object> parameters) { private static String notExists(String modelFieldName, Object[] values, Map<String, Object> parameters) {
String field = C + "." + modelField; String field = C + "." + modelFieldName;
return field + " IS NULL OR " + field + " IS EMPTY"; return field + " IS NULL OR " + field + " IS EMPTY";
} }
private static String in(String modelField, Object[] values, Map<String, Object> parameters) { private static String iLike(String modelFieldName, Object[] values, Map<String, Object> 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<String, Object> 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<String, Object> parameters) {
if (values == null || values.length == 0) { if (values == null || values.length == 0) {
return "false"; return "false";
} }
@ -113,9 +127,9 @@ public class IckleQueryOperators {
operands = new HashSet<>(Arrays.asList(values)); 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 -> { .map(operand -> {
String namedParam = findAvailableNamedParam(parameters.keySet(), modelField); String namedParam = findAvailableNamedParam(parameters.keySet(), modelFieldName);
parameters.put(namedParam, operand); parameters.put(namedParam, operand);
return ":" + namedParam; return ":" + namedParam;
}) })
@ -177,13 +191,13 @@ public class IckleQueryOperators {
* Provides a string containing where clause for given operator, field name and values * Provides a string containing where clause for given operator, field name and values
* *
* @param op operator * @param op operator
* @param filedName field name * @param modelFieldName field name
* @param values values * @param values values
* @param parameters mapping between named parameters and their values * @param parameters mapping between named parameters and their values
* @return where clause * @return where clause
*/ */
public static String combineExpressions(ModelCriteriaBuilder.Operator op, String filedName, Object[] values, Map<String, Object> parameters) { public static String combineExpressions(ModelCriteriaBuilder.Operator op, String modelFieldName, Object[] values, Map<String, Object> parameters) {
return operatorToExpressionCombinator(op).combine(filedName, values, parameters); return operatorToExpressionCombinator(op).combine(modelFieldName, values, parameters);
} }
} }

View file

@ -26,6 +26,9 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; 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}. * 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, public static String produceWhereClause(SearchableModelField<?> modelField, ModelCriteriaBuilder.Operator op,
Object[] values, Map<String, Object> parameters) { Object[] values, Map<String, Object> parameters) {
return whereClauseProducerForModelField(modelField) String fieldName = IckleQueryMapModelCriteriaBuilder.getFieldName(modelField);
.produceWhereClause(IckleQueryMapModelCriteriaBuilder.getFieldName(modelField), op, values, parameters);
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<String, Object> parameters) { private static String whereClauseForClientsAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters) {

View file

@ -17,9 +17,11 @@
package org.keycloak.models.map.storage.hotRod.client; package org.keycloak.models.map.storage.hotRod.client;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField; import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; 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.HotRodEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.HotRodPair; import org.keycloak.models.map.storage.hotRod.common.HotRodPair;
import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapClientEntity;
@ -37,111 +39,125 @@ import java.util.stream.Stream;
implementInterface = "org.keycloak.models.map.client.MapClientEntity", implementInterface = "org.keycloak.models.map.client.MapClientEntity",
inherits = "org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity.AbstractHotRodClientEntityDelegate" inherits = "org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity.AbstractHotRodClientEntityDelegate"
) )
@ProtoDoc("@Indexed")
public class HotRodClientEntity implements AbstractHotRodEntity { public class HotRodClientEntity implements AbstractHotRodEntity {
@ProtoField(number = 1, required = true) @ProtoField(number = 1, required = true)
public int entityVersion = 1; public int entityVersion = 1;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 2, required = true) @ProtoField(number = 2, required = true)
public String id; public String id;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 3) @ProtoField(number = 3)
public String realmId; public String realmId;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 4) @ProtoField(number = 4)
public String clientId; 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) @ProtoField(number = 5)
public String name; public String clientIdLowercase;
@ProtoField(number = 6) @ProtoField(number = 6)
public String description; public String name;
@ProtoField(number = 7) @ProtoField(number = 7)
public Set<String> redirectUris; public String description;
@ProtoField(number = 8) @ProtoField(number = 8)
public Boolean enabled; public Set<String> redirectUris;
@ProtoField(number = 9) @ProtoField(number = 9)
public Boolean enabled;
@ProtoField(number = 10)
public Boolean alwaysDisplayInConsole; public Boolean alwaysDisplayInConsole;
@ProtoField(number = 10) @ProtoField(number = 11)
public String clientAuthenticatorType; public String clientAuthenticatorType;
@ProtoField(number = 11) @ProtoField(number = 12)
public String secret; public String secret;
@ProtoField(number = 12) @ProtoField(number = 13)
public String registrationToken; public String registrationToken;
@ProtoField(number = 13) @ProtoField(number = 14)
public String protocol; public String protocol;
@ProtoField(number = 14) @ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 15)
public Set<HotRodAttributeEntity> attributes; public Set<HotRodAttributeEntity> attributes;
@ProtoField(number = 15) @ProtoField(number = 16)
public Set<HotRodPair<String, String>> authenticationFlowBindingOverrides; public Set<HotRodPair<String, String>> authenticationFlowBindingOverrides;
@ProtoField(number = 16) @ProtoField(number = 17)
public Boolean publicClient; public Boolean publicClient;
@ProtoField(number = 17) @ProtoField(number = 18)
public Boolean fullScopeAllowed; public Boolean fullScopeAllowed;
@ProtoField(number = 18) @ProtoField(number = 19)
public Boolean frontchannelLogout; public Boolean frontchannelLogout;
@ProtoField(number = 19) @ProtoField(number = 20)
public Integer notBefore; public Integer notBefore;
@ProtoField(number = 20) @ProtoField(number = 21)
public Set<String> scope; public Set<String> scope;
@ProtoField(number = 21) @ProtoField(number = 22)
public Set<String> webOrigins; public Set<String> webOrigins;
@ProtoField(number = 22) @ProtoField(number = 23)
public Set<HotRodProtocolMapperEntity> protocolMappers; public Set<HotRodProtocolMapperEntity> protocolMappers;
@ProtoField(number = 23) @ProtoField(number = 24)
public Set<HotRodPair<String, Boolean>> clientScopes; public Set<HotRodPair<String, Boolean>> clientScopes;
@ProtoField(number = 24, collectionImplementation = LinkedList.class) @ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 25, collectionImplementation = LinkedList.class)
public Collection<String> scopeMappings; public Collection<String> scopeMappings;
@ProtoField(number = 25) @ProtoField(number = 26)
public Boolean surrogateAuthRequired; public Boolean surrogateAuthRequired;
@ProtoField(number = 26) @ProtoField(number = 27)
public String managementUrl; public String managementUrl;
@ProtoField(number = 27) @ProtoField(number = 28)
public String baseUrl; public String baseUrl;
@ProtoField(number = 28) @ProtoField(number = 29)
public Boolean bearerOnly; public Boolean bearerOnly;
@ProtoField(number = 29) @ProtoField(number = 30)
public Boolean consentRequired; public Boolean consentRequired;
@ProtoField(number = 30) @ProtoField(number = 31)
public String rootUrl; public String rootUrl;
@ProtoField(number = 31) @ProtoField(number = 32)
public Boolean standardFlowEnabled; public Boolean standardFlowEnabled;
@ProtoField(number = 32) @ProtoField(number = 33)
public Boolean implicitFlowEnabled; public Boolean implicitFlowEnabled;
@ProtoField(number = 33) @ProtoField(number = 34)
public Boolean directAccessGrantsEnabled; public Boolean directAccessGrantsEnabled;
@ProtoField(number = 34) @ProtoField(number = 35)
public Boolean serviceAccountsEnabled; public Boolean serviceAccountsEnabled;
@ProtoField(number = 35) @ProtoField(number = 36)
public Integer nodeReRegistrationTimeout; public Integer nodeReRegistrationTimeout;
public static abstract class AbstractHotRodClientEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate<HotRodClientEntity>, MapClientEntity { public static abstract class AbstractHotRodClientEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate<HotRodClientEntity>, MapClientEntity {
@ -159,6 +175,14 @@ public class HotRodClientEntity implements AbstractHotRodEntity {
this.updated |= id != null; 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 @Override
public Stream<String> getClientScopes(boolean defaultScope) { public Stream<String> getClientScopes(boolean defaultScope) {
final Map<String, Boolean> clientScopes = getClientScopes(); final Map<String, Boolean> clientScopes = getClientScopes();

View file

@ -15,18 +15,22 @@
* limitations under the License. * 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 org.infinispan.protostream.annotations.ProtoField;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ProtoDoc("@Indexed")
public class HotRodAttributeEntity { public class HotRodAttributeEntity {
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 1) @ProtoField(number = 1)
public String name; public String name;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 2) @ProtoField(number = 2)
public List<String> values = new LinkedList<>(); public List<String> values = new LinkedList<>();

View file

@ -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<String> values = new LinkedList<>();
public HotRodAttributeEntityNonIndexed() {
}
public HotRodAttributeEntityNonIndexed(String name, List<String> values) {
this.name = name;
this.values.addAll(values);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getValues() {
return values;
}
public void setValues(List<String> 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);
}
}

View file

@ -18,7 +18,6 @@
package org.keycloak.models.map.storage.hotRod.common; package org.keycloak.models.map.storage.hotRod.common;
import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodAttributeEntity;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -48,6 +47,10 @@ public class HotRodTypesUtils {
return new HotRodAttributeEntity(entry.getKey(), entry.getValue()); return new HotRodAttributeEntity(entry.getKey(), entry.getValue());
} }
public static HotRodAttributeEntityNonIndexed createHotRodAttributeEntityNonIndexedFromMapEntry(Map.Entry<String, List<String>> entry) {
return new HotRodAttributeEntityNonIndexed(entry.getKey(), entry.getValue());
}
public static <SetType, KeyType> boolean removeFromSetByMapKey(Set<SetType> set, KeyType key, Function<SetType, KeyType> keyGetter) { public static <SetType, KeyType> boolean removeFromSetByMapKey(Set<SetType> set, KeyType key, Function<SetType, KeyType> keyGetter) {
if (set == null || set.isEmpty()) { return false; } if (set == null || set.isEmpty()) { return false; }
return set.stream() return set.stream()
@ -73,10 +76,18 @@ public class HotRodTypesUtils {
return attributeEntity.name; return attributeEntity.name;
} }
public static String getKey(HotRodAttributeEntityNonIndexed attributeEntity) {
return attributeEntity.name;
}
public static List<String> getValue(HotRodAttributeEntity attributeEntity) { public static List<String> getValue(HotRodAttributeEntity attributeEntity) {
return attributeEntity.values; return attributeEntity.values;
} }
public static List<String> getValue(HotRodAttributeEntityNonIndexed attributeEntity) {
return attributeEntity.values;
}
public static String getKey(AbstractEntity entity) { public static String getKey(AbstractEntity entity) {
return entity.getId(); return entity.getId();
} }

View file

@ -19,7 +19,6 @@ package org.keycloak.models.map.storage.hotRod.common;
import org.infinispan.protostream.GeneratedSchema; import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder; 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.HotRodClientEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity; import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity;
import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity; import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity;
@ -31,14 +30,15 @@ import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity;
includeClasses = { includeClasses = {
// Clients // Clients
HotRodClientEntity.class, HotRodClientEntity.class,
HotRodAttributeEntity.class,
HotRodProtocolMapperEntity.class, HotRodProtocolMapperEntity.class,
// Groups // Groups
HotRodGroupEntity.class, HotRodGroupEntity.class,
// Common // Common
HotRodPair.class HotRodPair.class,
HotRodAttributeEntity.class,
HotRodAttributeEntityNonIndexed.class
}, },
schemaFileName = "KeycloakHotRodMapStorage.proto", schemaFileName = "KeycloakHotRodMapStorage.proto",
schemaFilePath = "proto/", schemaFilePath = "proto/",

View file

@ -18,6 +18,7 @@ package org.keycloak.models.map.storage.hotRod.connections;
import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager; 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.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.commons.marshall.ProtoStreamMarshaller; 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.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a> * @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
@ -104,14 +108,31 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
remoteBuilder.addContextInitializer(ProtoSchemaInitializer.INSTANCE); remoteBuilder.addContextInitializer(ProtoSchemaInitializer.INSTANCE);
remoteCacheManager = new RemoteCacheManager(remoteBuilder.build()); remoteCacheManager = new RemoteCacheManager(remoteBuilder.build());
Set<String> remoteCaches = HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
.map(HotRodEntityDescriptor::getCacheName).collect(Collectors.toSet());
if (configureRemoteCaches) { if (configureRemoteCaches) {
// access the caches to force their creation // access the caches to force their creation
HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream() remoteCaches.forEach(remoteCacheManager::getCache);
.map(HotRodEntityDescriptor::getCacheName)
.forEach(remoteCacheManager::getCache);
} }
registerSchemata(ProtoSchemaInitializer.INSTANCE); 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) { private void registerSchemata(GeneratedSchema initializer) {

View file

@ -17,20 +17,23 @@
package org.keycloak.models.map.storage.hotRod.group; package org.keycloak.models.map.storage.hotRod.group;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField; import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.group.MapGroupEntity; 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.AbstractHotRodEntity;
import org.keycloak.models.map.storage.hotRod.common.HotRodAttributeEntityNonIndexed;
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate;
import java.util.Objects;
import java.util.Set; import java.util.Set;
@GenerateHotRodEntityImplementation( @GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.group.MapGroupEntity", implementInterface = "org.keycloak.models.map.group.MapGroupEntity",
inherits = "org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity.AbstractHotRodGroupEntityDelegate" inherits = "org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity.AbstractHotRodGroupEntityDelegate"
) )
@ProtoDoc("@Indexed")
public class HotRodGroupEntity implements AbstractHotRodEntity { public class HotRodGroupEntity implements AbstractHotRodEntity {
public static abstract class AbstractHotRodGroupEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate<HotRodGroupEntity>, MapGroupEntity { public static abstract class AbstractHotRodGroupEntityDelegate extends UpdatableEntity.Impl implements HotRodEntityDelegate<HotRodGroupEntity>, MapGroupEntity {
@ -47,26 +50,47 @@ public class HotRodGroupEntity implements AbstractHotRodEntity {
entity.id = id; entity.id = id;
this.updated |= id != null; 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) @ProtoField(number = 1, required = true)
public int entityVersion = 1; public int entityVersion = 1;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 2, required = true) @ProtoField(number = 2, required = true)
public String id; public String id;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 3) @ProtoField(number = 3)
public String realmId; public String realmId;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 4) @ProtoField(number = 4)
public String name; 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) @ProtoField(number = 5)
public String nameLowercase;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 6)
public String parentId; public String parentId;
@ProtoField(number = 6)
public Set<HotRodAttributeEntity> attributes;
@ProtoField(number = 7) @ProtoField(number = 7)
public Set<HotRodAttributeEntityNonIndexed> attributes;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 8)
public Set<String> grantedRoles; public Set<String> grantedRoles;
} }

View file

@ -4,9 +4,20 @@
<cache-container> <cache-container>
<!-- Specify all remote caches that should be created on the Infinispan server. --> <!-- Specify all remote caches that should be created on the Infinispan server. -->
<distributed-cache name="clients" mode="SYNC"> <distributed-cache name="clients" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientEntity</indexed-entity>
<indexed-entity>kc.HotRodAttributeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/> <encoding media-type="application/x-protostream"/>
</distributed-cache> </distributed-cache>
<distributed-cache name="groups" mode="SYNC"> <distributed-cache name="groups" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodGroupEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/> <encoding media-type="application/x-protostream"/>
</distributed-cache> </distributed-cache>
</cache-container> </cache-container>

View file

@ -6,9 +6,20 @@
<!-- Specify all remote caches that should be created on the embedded Infinispan server. --> <!-- Specify all remote caches that should be created on the embedded Infinispan server. -->
<distributed-cache name="clients" mode="SYNC"> <distributed-cache name="clients" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientEntity</indexed-entity>
<indexed-entity>kc.HotRodAttributeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/> <encoding media-type="application/x-protostream"/>
</distributed-cache> </distributed-cache>
<distributed-cache name="groups" mode="SYNC"> <distributed-cache name="groups" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodGroupEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/> <encoding media-type="application/x-protostream"/>
</distributed-cache> </distributed-cache>
</cache-container> </cache-container>

View file

@ -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.QueryParameters;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;

View file

@ -259,7 +259,8 @@
"configureRemoteCaches": "${keycloak.connectionsHotRod.configureRemoteCaches:true}", "configureRemoteCaches": "${keycloak.connectionsHotRod.configureRemoteCaches:true}",
"username": "${keycloak.connectionsHotRod.username:myuser}", "username": "${keycloak.connectionsHotRod.username:myuser}",
"password": "${keycloak.connectionsHotRod.password:qwer1234!}", "password": "${keycloak.connectionsHotRod.password:qwer1234!}",
"enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}" "enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}",
"reindexCaches": "${keycloak.connectionsHotRod.reindexCaches:clients,groups}"
} }
}, },

View file

@ -2,9 +2,20 @@
<cache-container> <cache-container>
<transport stack="udp"/> <transport stack="udp"/>
<distributed-cache name="clients" mode="SYNC"> <distributed-cache name="clients" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientEntity</indexed-entity>
<indexed-entity>kc.HotRodAttributeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/> <encoding media-type="application/x-protostream"/>
</distributed-cache> </distributed-cache>
<distributed-cache name="groups" mode="SYNC"> <distributed-cache name="groups" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodGroupEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/> <encoding media-type="application/x-protostream"/>
</distributed-cache> </distributed-cache>
</cache-container> </cache-container>

View file

@ -18,6 +18,7 @@ package org.keycloak.testsuite.model;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
@ -31,6 +32,9 @@ import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.RoleProvider; import org.keycloak.models.RoleProvider;
import java.util.Map;
import java.util.Set;
/** /**
* *
* @author rmartinc * @author rmartinc
@ -42,6 +46,8 @@ public class ClientModelTest extends KeycloakModelTest {
private String realmId; private String realmId;
private static final String searchClientId = "My ClIeNt WITH sP%Ces and sp*ci_l Ch***cters \" ?!";
@Override @Override
public void createEnvironment(KeycloakSession s) { public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("realm"); RealmModel realm = s.realms().createRealm("realm");
@ -58,6 +64,12 @@ public class ClientModelTest extends KeycloakModelTest {
public void testClientsBasics() { public void testClientsBasics() {
// Create client // Create client
ClientModel originalModel = withRealm(realmId, (session, realm) -> session.clients().addClient(realm, "myClientId")); 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()); assertThat(originalModel.getId(), notNullValue());
// Find by id // Find by id
@ -76,6 +88,49 @@ public class ClientModelTest extends KeycloakModelTest {
assertThat(model.getClientId(), is(equalTo("myClientId"))); 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<ClientModel, Set<String>> 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 // Test storing flow binding override
{ {
// Add some override // Add some override