Implement HotRod storage for Users

Closes #9671
This commit is contained in:
Michal Hajas 2022-01-06 12:54:50 +01:00 committed by Hynek Mlnařík
parent 623aaf1e8b
commit b50b8f883b
27 changed files with 753 additions and 67 deletions

View file

@ -26,6 +26,6 @@ import java.lang.annotation.Target;
* @author hmlnarik
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.METHOD})
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
public @interface IgnoreForEntityImplementationGenerator {
}

View file

@ -51,6 +51,7 @@ import javax.lang.model.SourceVersion;
import static org.keycloak.models.map.processor.FieldAccessorType.GETTER;
import static org.keycloak.models.map.processor.Util.getGenericsDeclaration;
import static org.keycloak.models.map.processor.Util.isMapType;
import static org.keycloak.models.map.processor.Util.isSetType;
import static org.keycloak.models.map.processor.Util.singularToPlural;
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@ -207,6 +208,9 @@ public abstract class AbstractGenerateEntityImplementationsProcessor extends Abs
", (o1, o2) -> o1" +
", java.util.HashMap::new" +
"))";
} else if (isCollection(typeElement.asType())) {
TypeMirror collectionType = getGenericsDeclaration(fieldType).get(0);
return parameterName + " == null ? null : " + parameterName + ".stream().map(entry -> " + deepClone(collectionType, "entry") + ").collect(java.util.stream.Collectors.toCollection(" + (isSetType(typeElement) ? "java.util.HashSet::new" : "java.util.LinkedList::new") + "))";
}
return "deepClone(" + parameterName + ")";
}

View file

@ -424,7 +424,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
case SETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
if (! isImmutableFinalType(fieldType)) {
pw.println(" p0 = " + deepClone(fieldType, "p0") + ";");
pw.println(" p0 = " + deepClone(firstParameterType, "p0") + ";");
}
pw.println(" updated |= ! Objects.equals(" + fieldName + ", p0);");
pw.println(" " + fieldName + " = p0;");
@ -434,7 +434,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" if (" + fieldName + " == null) { " + fieldName + " = " + interfaceToImplementation(typeElement, "") + "; }");
if (! isImmutableFinalType(firstParameterType)) {
pw.println(" p0 = " + deepClone(fieldType, "p0") + ";");
pw.println(" p0 = " + deepClone(firstParameterType, "p0") + ";");
}
if (isSetType(typeElement)) {
pw.println(" updated |= " + fieldName + ".add(p0);");

View file

@ -30,6 +30,7 @@ import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
@ -108,6 +109,10 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
.anyMatch(fieldType -> ! isKnownCollectionOfImmutableFinalTypes(fieldType) && ! isImmutableFinalType(fieldType));
boolean usingGeneratedCloner = ! hasDeepClone && needsDeepClone;
boolean hasId = methodsPerAttribute.containsKey("Id") || allMembers.stream().anyMatch(el -> "getId".equals(el.getSimpleName().toString()));
boolean hasFieldId = elements.getAllMembers(e).stream()
.filter(VariableElement.class::isInstance)
.map(VariableElement.class::cast)
.anyMatch(variableElement -> variableElement.getSimpleName().toString().equals("id"));
JavaFileObject file = processingEnv.getFiler().createSourceFile(hotRodImplClassName);
try (PrintWriter pw = new PrintWriterNoJavaLang(file.openWriter())) {
@ -218,6 +223,7 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
pw.print(" return ");
pw.println(elements.getAllMembers(e).stream()
.filter(Util::isNotIgnored)
.filter(VariableElement.class::isInstance)
.map(VariableElement.class::cast)
.map(var -> "Objects.equals(e1." + var.getSimpleName().toString() + ", e2." + var.getSimpleName().toString() + ")")
@ -227,13 +233,13 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
pw.println(" public static int entityHashCode(" + className + " e) {");
pw.println(" return "
+ (hasId
+ (hasFieldId
? "(e.id == null ? Objects.hash(e) : e.id.hashCode())"
: "Objects.hash("
+ elements.getAllMembers(e).stream()
.filter(VariableElement.class::isInstance)
.map(VariableElement.class::cast)
.map(var -> var.getSimpleName().toString())
.map(var -> "e." + var.getSimpleName().toString())
.collect(Collectors.joining(",\n "))
+ ")")
+ ";"
@ -268,7 +274,7 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
.filter(variableElement -> variableElement.getSimpleName().toString().equals(hotRodEntityFieldName))
.findFirst();
if (!hotRodVariable.isPresent()) {
if (!hasField(e, hotRodEntityFieldName)) {
// throw an error when no variable found
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Cannot find " + e.getSimpleName().toString() + " field for methods: [" + me.getValue().stream().map(ee -> ee.getSimpleName().toString()).collect(Collectors.joining(", ")) + "]", parentInterfaceElement);
return;
@ -326,7 +332,7 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
case SETTER:
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
if (! isImmutableFinalType(firstParameterType)) {
pw.println(" p0 = " + deepClone(fieldType, "p0") + ";");
pw.println(" p0 = " + deepClone(firstParameterType, "p0") + ";");
}
pw.println(" " + hotRodFieldType.toString() + " migrated = " + migrateToType(hotRodFieldType, firstParameterType, "p0") + ";");
pw.println(" " + hotRodEntityField("updated") + " |= ! Objects.equals(" + hotRodEntityField(fieldName) + ", migrated);");
@ -338,7 +344,7 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
pw.println(" if (" + hotRodEntityField(fieldName) + " == null) { " + hotRodEntityField(fieldName) + " = " + interfaceToImplementation(typeElement, "") + "; }");
if (! isImmutableFinalType(firstParameterType)) {
pw.println(" p0 = " + deepClone(fieldType, "p0") + ";");
pw.println(" p0 = " + deepClone(firstParameterType, "p0") + ";");
}
pw.println(" " + collectionItemType.toString() + " migrated = " + migrateToType(collectionItemType, firstParameterType, "p0") + ";");
if (isSetType(typeElement)) {
@ -351,19 +357,22 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
return true;
case COLLECTION_DELETE:
collectionItemType = getGenericsDeclaration(hotRodFieldType).get(0);
boolean needsReturn = method.getReturnType().getKind() != TypeKind.VOID;
pw.println(" @SuppressWarnings(\"unchecked\") @Override public " + method.getReturnType() + " " + method.getSimpleName() + "(" + firstParameterType + " p0) {");
if (isMapType(typeElement)) {
// Maps are stored as sets
pw.println(" " + hotRodEntityField("updated") + " |= " + hotRodUtils.getQualifiedName().toString() + ".removeFromSetByMapKey("
pw.println(" boolean removed = " + hotRodUtils.getQualifiedName().toString() + ".removeFromSetByMapKey("
+ hotRodEntityField(fieldName) + ", "
+ "p0, "
+ keyGetterReference(collectionItemType) + ");"
);
pw.println(" " + hotRodEntityField("updated") + " |= removed;");
} else {
pw.println(" if (" + hotRodEntityField(fieldName) + " == null) { return; }");
pw.println(" if (" + hotRodEntityField(fieldName) + " == null) { return" + (needsReturn ? " false" : "") + "; }");
pw.println(" boolean removed = " + hotRodEntityField(fieldName) + ".remove(p0);");
pw.println(" " + hotRodEntityField("updated") + " |= removed;");
}
if (needsReturn) pw.println(" return removed;");
pw.println(" }");
return true;
case MAP_ADD:
@ -409,8 +418,20 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
return e.getSimpleName().toString();
}
private boolean hasField(TypeElement type, String fieldName) {
Optional<VariableElement> hotRodVariable = elements.getAllMembers(type).stream()
.filter(VariableElement.class::isInstance)
.map(VariableElement.class::cast)
.filter(variableElement -> variableElement.getSimpleName().toString().equals(fieldName))
.findFirst();
return hotRodVariable.isPresent();
}
private String keyGetterReference(TypeMirror type) {
if (types.isAssignable(type, abstractHotRodEntity.asType())) {
TypeElement typeElement = elements.getTypeElement(types.erasure(type).toString());
if (hasField(typeElement, "id")) {
return "e -> e.id";
}
return hotRodUtils.getQualifiedName().toString() + "::getKey";
@ -423,15 +444,29 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
return hotRodUtils.getQualifiedName().toString() + "::getValue";
}
private boolean isAssignable(TypeMirror fromType, TypeMirror toType) {
return types.isAssignable(types.erasure(fromType), types.erasure(toType));
}
private String migrateToType(TypeMirror toType, TypeMirror[] fromType, String[] fieldNames) {
// No migration needed, fromType is assignable to toType directly
if (fromType.length == 1 && types.isAssignable(types.erasure(fromType[0]), types.erasure(toType))) {
if (fromType.length == 1 && isAssignable(fromType[0], toType) && !isCollection(fromType[0])) {
return fieldNames[0];
}
// HotRod entities are not allowed to use Maps, therefore we often need to migrate from Map to Set and the other way around
// Solve migration of data within collections
if (fromType.length == 1) {
if (isSetType((TypeElement) types.asElement(types.erasure(toType)))
if (isAssignable(fromType[0], toType)) { // First case, the collection is the same
TypeMirror fromGeneric = getGenericsDeclaration(fromType[0]).get(0);
TypeMirror toGeneric = getGenericsDeclaration(toType).get(0);
// Generics are assignable too, so we can just assign the same value
if (isAssignable(fromGeneric, toGeneric)) return fieldNames[0];
return hotRodUtils.getQualifiedName().toString() + ".migrate" + toSimpleName(fromType[0]) + "("
+ fieldNames[0] + ", "
+ "collectionItem -> " + migrateToType(toGeneric, fromGeneric, "collectionItem") + ")";
} else if (isSetType((TypeElement) types.asElement(types.erasure(toType)))
&& isMapType((TypeElement) types.asElement(types.erasure(fromType[0])))) {
TypeMirror setType = getGenericsDeclaration(toType).get(0);
@ -456,19 +491,19 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
return "new " + toType.toString() + "(" + String.join(", ", fieldNames) + ")";
}
// Check if any of parameters is another Map*Entity
OptionalInt anotherMapEntityIndex = IntStream.range(0, fromType.length)
.filter(i -> types.isAssignable(fromType[i], abstractEntity.asType()))
.findFirst();
if (isAssignable(toType, abstractHotRodEntity.asType())) {
// Check if any of parameters is another Map*Entity
OptionalInt anotherMapEntityIndex = IntStream.range(0, fromType.length)
.filter(i -> isAssignable(fromType[i], abstractEntity.asType()))
.findFirst();
if (anotherMapEntityIndex.isPresent()) {
// If yes, we can be sure that it implements HotRodEntityDelegate (this is achieved by HotRod cloner settings) so we can just call getHotRodEntity method
return "((" + generalHotRodDelegate.getQualifiedName().toString() + "<" + toType.toString() + ">) " + fieldNames[anotherMapEntityIndex.getAsInt()] + ").getHotRodEntity()";
return "((" + generalHotRodDelegate.getQualifiedName().toString() + "<" + toType.toString() + ">) " + fieldNames[anotherMapEntityIndex.orElse(0)] + ").getHotRodEntity()";
}
// Check if any of parameters is another HotRod*Entity
OptionalInt anotherHotRodEntityIndex = IntStream.range(0, fromType.length)
.filter(i -> types.isAssignable(fromType[i], abstractHotRodEntity.asType()))
.filter(i -> isAssignable(fromType[i], abstractHotRodEntity.asType()))
.findFirst();
if (anotherHotRodEntityIndex.isPresent()) {
@ -476,7 +511,8 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
return "new " + fromType[anotherHotRodEntityIndex.getAsInt()] + "Delegate(" + String.join(", ", fieldNames) + ")";
}
throw new CannotMigrateTypeException(toType, fromType);
int last = fromType.length -1 ;
return hotRodUtils.getQualifiedName().toString() + ".migrate" + toSimpleName(fromType[last]) + "To" + toSimpleName(toType) + "(" + fieldNames[last] + ")";
}
private Optional<ExecutableElement> findSuitableConstructor(TypeMirror desiredType, TypeMirror[] parameters) {

View file

@ -39,6 +39,7 @@ import org.keycloak.models.map.storage.chm.MapFieldPredicates;
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
import org.keycloak.storage.SearchableModelField;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterators;
@ -51,7 +52,7 @@ import java.util.stream.StreamSupport;
import static org.keycloak.models.map.storage.hotRod.common.HotRodUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing;
public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E>, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class);
@ -146,7 +147,7 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRo
QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
Query<V> query = queryFactory.create(queryString);
Query<E> query = queryFactory.create(queryString);
query.setParameters(iqmcb.getParameters());
return query.execute().hitCount().orElse(0);
@ -167,17 +168,18 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRo
QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
Query<V> query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
Query<Object[]> query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
queryParameters.getLimit());
query.setParameters(iqmcb.getParameters());
AtomicLong result = new AtomicLong();
CloseableIterator<V> iterator = query.iterator();
CloseableIterator<Object[]> iterator = query.iterator();
StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)
.peek(e -> result.incrementAndGet())
.map(AbstractEntity::getId)
.map(a -> a[0])
.map(String.class::cast)
.forEach(this::delete);
iterator.close();

View file

@ -47,7 +47,7 @@ public class HotRodMapStorageProvider implements MapStorageProvider {
}
@SuppressWarnings("unchecked")
public <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E>, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
public <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
HotRodEntityDescriptor<E, V> entityDescriptor = (HotRodEntityDescriptor<E, V>) factory.getEntityDescriptor(modelType);
return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner);
}

View file

@ -25,6 +25,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.group.MapGroupEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate;
@ -38,6 +39,15 @@ import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProviderFactory;
import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity;
import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEntityDelegate;
import org.keycloak.models.map.user.MapUserConsentEntity;
import org.keycloak.models.map.user.MapUserCredentialEntity;
import org.keycloak.models.map.user.MapUserEntity;
import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import java.util.HashMap;
@ -49,9 +59,13 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
private static final Logger LOG = Logger.getLogger(HotRodMapStorageProviderFactory.class);
private final static DeepCloner CLONER = new DeepCloner.Builder()
.constructor(MapClientEntity.class, HotRodClientEntityDelegate::new)
.constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntityDelegate::new)
.constructor(MapGroupEntity.class, HotRodGroupEntityDelegate::new)
.constructor(MapClientEntity.class, HotRodClientEntityDelegate::new)
.constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntityDelegate::new)
.constructor(MapGroupEntity.class, HotRodGroupEntityDelegate::new)
.constructor(MapUserEntity.class, HotRodUserEntityDelegate::new)
.constructor(MapUserCredentialEntity.class, HotRodUserCredentialEntityDelegate::new)
.constructor(MapUserFederatedIdentityEntity.class, HotRodUserFederatedIdentityEntityDelegate::new)
.constructor(MapUserConsentEntity.class, HotRodUserConsentEntityDelegate::new)
.build();
public static final Map<Class<?>, HotRodEntityDescriptor<?, ?>> ENTITY_DESCRIPTOR_MAP = new HashMap<>();
@ -67,6 +81,11 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
new HotRodEntityDescriptor<>(GroupModel.class,
HotRodGroupEntity.class,
HotRodGroupEntityDelegate::new));
// Users descriptor
ENTITY_DESCRIPTOR_MAP.put(UserModel.class,
new HotRodEntityDescriptor<>(UserModel.class,
HotRodUserEntity.class,
HotRodUserEntityDelegate::new));
}
@Override

View file

@ -31,6 +31,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.C;
@ -43,8 +44,10 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
private final Class<E> hotRodEntityClass;
private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
private final Map<String, Object> parameters;
private static final String NON_ANALYZED_FIELD_REGEX = "[%_\\\\]";
private static final String ANALYZED_FIELD_REGEX = "[+!^\"~*?:\\\\]";
private static final Pattern NON_ANALYZED_FIELD_REGEX = Pattern.compile("[%_\\\\]");
// private static final Pattern ANALYZED_FIELD_REGEX = Pattern.compile("[+!^\"~*?:\\\\]"); // TODO reevaluate once https://github.com/keycloak/keycloak/issues/9295 is fixed
private static final Pattern ANALYZED_FIELD_REGEX = Pattern.compile("\\\\"); // escape "\" with extra "\"
private static final Pattern SINGLE_PERCENT_CHARACTER = Pattern.compile("^%+$");
public static final Map<SearchableModelField<?>, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>();
public static final Set<SearchableModelField<?>> ANALYZED_MODEL_FIELDS = new HashSet<>();
@ -55,6 +58,14 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
INFINISPAN_NAME_OVERRIDES.put(GroupModel.SearchableFields.PARENT_ID, "parentId");
INFINISPAN_NAME_OVERRIDES.put(GroupModel.SearchableFields.ASSIGNED_ROLE, "grantedRoles");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT, "serviceAccountClientLink");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.CONSENT_FOR_CLIENT, "userConsents.clientId");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.CONSENT_WITH_CLIENT_SCOPE, "userConsents.grantedClientScopesIds");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.ASSIGNED_ROLE, "rolesMembership");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.ASSIGNED_GROUP, "groupsMembership");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.ATTRIBUTE, "attributes");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.IDP_AND_USER, "federatedIdentities");
}
static {
@ -192,11 +203,16 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
public static Object sanitize(Object value) {
if (value instanceof String) {
String sValue = (String) value;
if(SINGLE_PERCENT_CHARACTER.matcher(sValue).matches()) {
return 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");
String sanitizedString = NON_ANALYZED_FIELD_REGEX.matcher(sValue.substring(anyBeginning ? 1 : 0, sValue.length() - (anyEnd ? 1 : 0)))
.replaceAll("\\\\\\\\" + "$0");
return (anyBeginning ? "%" : "") + sanitizedString + (anyEnd ? "%" : "");
}
@ -210,9 +226,13 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
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
if(SINGLE_PERCENT_CHARACTER.matcher(sValue).matches()) {
return "*";
}
String sanitizedString = ANALYZED_FIELD_REGEX.matcher(sValue.substring(anyBeginning ? 1 : 0, sValue.length() - (anyEnd ? 1 : 0)))
.replaceAll("\\\\\\\\"); // escape "\" with extra "\"
// .replaceAll("\\\\\\\\" + "$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

View file

@ -18,14 +18,17 @@
package org.keycloak.models.map.storage.hotRod;
import org.keycloak.models.ClientModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.storage.SearchableModelField;
import org.keycloak.storage.StorageId;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.keycloak.models.map.storage.hotRod.IckleQueryMapModelCriteriaBuilder.getFieldName;
import static org.keycloak.models.map.storage.hotRod.IckleQueryMapModelCriteriaBuilder.sanitizeAnalyzed;
import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.C;
@ -47,7 +50,10 @@ public class IckleQueryWhereClauses {
private static final Map<SearchableModelField<?>, WhereClauseProducer> WHERE_CLAUSE_PRODUCER_OVERRIDES = new HashMap<>();
static {
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(ClientModel.SearchableFields.ATTRIBUTE, IckleQueryWhereClauses::whereClauseForClientsAttributes);
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(ClientModel.SearchableFields.ATTRIBUTE, IckleQueryWhereClauses::whereClauseForAttributes);
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.ATTRIBUTE, IckleQueryWhereClauses::whereClauseForAttributes);
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.IDP_AND_USER, IckleQueryWhereClauses::whereClauseForUserIdpAlias);
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK, IckleQueryWhereClauses::whereClauseForConsentClientFederationLink);
}
@FunctionalInterface
@ -79,7 +85,7 @@ public class IckleQueryWhereClauses {
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]) + "'";
String clause = C + "." + fieldName + " : '" + sanitizeAnalyzed(((String)values[0]).toLowerCase()) + "'";
if (op.equals(ModelCriteriaBuilder.Operator.NE)) {
return "not(" + clause + ")";
}
@ -90,7 +96,7 @@ public class IckleQueryWhereClauses {
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 whereClauseForAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters) {
if (values == null || values.length != 2) {
throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected attribute_name-value pair, got: " + Arrays.toString(values));
}
@ -111,4 +117,41 @@ public class IckleQueryWhereClauses {
return "(" + nameClause + ")" + " AND " + "(" + valueClause + ")";
}
private static String whereClauseForUserIdpAlias(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters) {
if (op != ModelCriteriaBuilder.Operator.EQ) {
throw new CriterionNotSupportedException(UserModel.SearchableFields.IDP_AND_USER, op);
}
if (values == null || values.length == 0 || values.length > 2) {
throw new CriterionNotSupportedException(UserModel.SearchableFields.IDP_AND_USER, op, "Invalid arguments, expected (idp_alias) or (idp_alias, idp_user), got: " + Arrays.toString(values));
}
final Object idpAlias = values[0];
if (values.length == 1) {
return IckleQueryOperators.combineExpressions(op, modelFieldName + ".identityProvider", values, parameters);
} else if (idpAlias == null) {
final Object idpUserId = values[1];
return IckleQueryOperators.combineExpressions(op, modelFieldName + ".userId", new Object[] { idpUserId }, parameters);
} else {
final Object idpUserId = values[1];
// Clause for searching federated identity id
String idClause = IckleQueryOperators.combineExpressions(op, modelFieldName + ".identityProvider", new Object[]{ idpAlias }, parameters);
// Clause for searching federated identity userId
String userIdClause = IckleQueryOperators.combineExpressions(op, modelFieldName + ".userId", new Object[] { idpUserId }, parameters);
return "(" + idClause + ")" + " AND " + "(" + userIdClause + ")";
}
}
private static String whereClauseForConsentClientFederationLink(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters) {
if (op != ModelCriteriaBuilder.Operator.EQ) {
throw new CriterionNotSupportedException(UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK, op);
}
if (values == null || values.length != 1) {
throw new CriterionNotSupportedException(UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK, op, "Invalid arguments, expected (federation_provider_id), got: " + Arrays.toString(values));
}
String providerId = new StorageId((String) values[0], "").getId();
return IckleQueryOperators.combineExpressions(ModelCriteriaBuilder.Operator.LIKE, getFieldName(UserModel.SearchableFields.CONSENT_FOR_CLIENT), new String[] {providerId + "%"}, parameters);
}
}

View file

@ -20,6 +20,6 @@ package org.keycloak.models.map.storage.hotRod.common;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.UpdatableEntity;
public interface HotRodEntityDelegate<E> extends AbstractEntity, UpdatableEntity {
public interface HotRodEntityDelegate<E> extends UpdatableEntity {
E getHotRodEntity();
}

View file

@ -18,6 +18,8 @@
package org.keycloak.models.map.storage.hotRod.common;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEntity;
import java.util.List;
import java.util.Map;
@ -91,4 +93,20 @@ public class HotRodTypesUtils {
public static String getKey(AbstractEntity entity) {
return entity.getId();
}
public static String getKey(HotRodUserFederatedIdentityEntity hotRodUserFederatedIdentityEntity) {
return hotRodUserFederatedIdentityEntity.identityProvider;
}
public static String getKey(HotRodUserConsentEntity hotRodUserConsentEntity) {
return hotRodUserConsentEntity.clientId;
}
public static <T, V> List<V> migrateList(List<T> p0, Function<T, V> migrator) {
return p0 == null ? null : p0.stream().map(migrator).collect(Collectors.toList());
}
public static <T, V> Set<V> migrateSet(Set<T> p0, Function<T, V> migrator) {
return p0 == null ? null : p0.stream().map(migrator).collect(Collectors.toSet());
}
}

View file

@ -22,6 +22,10 @@ import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
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;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEntity;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
@ -35,6 +39,12 @@ import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity;
// Groups
HotRodGroupEntity.class,
// Users
HotRodUserEntity.class,
HotRodUserConsentEntity.class,
HotRodUserCredentialEntity.class,
HotRodUserFederatedIdentityEntity.class,
// Common
HotRodPair.class,
HotRodAttributeEntity.class,

View file

@ -21,6 +21,7 @@ import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.group.MapGroupEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityDelegate;
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.UpdatableHotRodEntityDelegateImpl;
@ -92,4 +93,14 @@ public class HotRodGroupEntity extends AbstractHotRodEntity {
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 8)
public Set<String> grantedRoles;
@Override
public boolean equals(Object o) {
return HotRodGroupEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodGroupEntityDelegate.entityHashCode(this);
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2022 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.user;
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.client.HotRodProtocolMapperEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import java.util.Set;
@GenerateHotRodEntityImplementation(implementInterface = "org.keycloak.models.map.user.MapUserConsentEntity")
@ProtoDoc("@Indexed")
public class HotRodUserConsentEntity extends AbstractHotRodEntity {
@ProtoField(number = 1)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String clientId;
@ProtoField(number = 2)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Set<String> grantedClientScopesIds;
@ProtoField(number = 3)
public Long createdDate;
@ProtoField(number = 4)
public Long lastUpdatedDate;
@Override
public boolean equals(Object o) {
return HotRodUserConsentEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodUserConsentEntityDelegate.entityHashCode(this);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2022 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.user;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
@GenerateHotRodEntityImplementation(implementInterface = "org.keycloak.models.map.user.MapUserCredentialEntity")
public class HotRodUserCredentialEntity extends AbstractHotRodEntity {
@ProtoField(number = 1)
public String id;
@ProtoField(number = 2)
public String type;
@ProtoField(number = 3)
public String userLabel;
@ProtoField(number = 4)
public Long createdDate;
@ProtoField(number = 5)
public String secretData;
@ProtoField(number = 6)
public String credentialData;
@ProtoField(number = 7)
public Integer priority;
@Override
public boolean equals(Object o) {
return HotRodUserCredentialEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodUserCredentialEntityDelegate.entityHashCode(this);
}
}

View file

@ -0,0 +1,305 @@
/*
* Copyright 2022 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.user;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.jboss.logging.Logger;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator;
import org.keycloak.models.map.common.UpdatableEntity;
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.UpdatableHotRodEntityDelegateImpl;
import org.keycloak.models.map.user.MapUserConsentEntity;
import org.keycloak.models.map.user.MapUserCredentialEntity;
import org.keycloak.models.map.user.MapUserEntity;
import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.user.MapUserEntity",
inherits = "org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity.AbstractHotRodUserEntityDelegate"
)
@ProtoDoc("@Indexed")
public class HotRodUserEntity extends AbstractHotRodEntity {
@IgnoreForEntityImplementationGenerator
private static final Logger LOG = Logger.getLogger(HotRodUserEntity.class);
@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;
@ProtoField(number = 4)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String username;
@ProtoField(number = 22)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String usernameLowercase;
@ProtoField(number = 5)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = \"filename\"))")
public String firstName;
@ProtoField(number = 6)
public Long createdTimestamp;
@ProtoField(number = 7)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = \"filename\"))")
public String lastName;
@ProtoField(number = 8)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = \"filename\"))")
public String email;
@ProtoField(number = 9)
/**
* TODO: Workaround for ISPN-8584
*
* This index shouldn't be there as majority of object will be enabled == true
*
* When this index is missing Ickle queries like following:
* FROM kc.HotRodUserEntity c WHERE (c.realmId = "admin-client-test" AND c.enabled = true AND c.email : "user*")
* fail with:
* Error: {"error":{"message":"Error executing search","cause":"Unexpected condition type (FullTextTermExpr): PROP(email):'user*'"}}
*
* In other words it is not possible to combine searching for Analyzed field and non-indexed field in one Ickle query
*/
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Boolean enabled;
@ProtoField(number = 10)
/**
* TODO: Workaround for ISPN-8584
*
* When this index is missing Ickle queries like following:
* FROM kc.HotRodUserEntity c WHERE (c.realmId = "admin-client-test" AND c.enabled = true AND c.email : "user*")
* fail with:
* Error: {"error":{"message":"Error executing search","cause":"Unexpected condition type (FullTextTermExpr): PROP(email):'user*'"}}
*
* In other words it is not possible to combine searching for Analyzed field and non-indexed field in one Ickle query
*/
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Boolean emailVerified;
// This is necessary to be able to dynamically switch unique email constraints on and off in the realm settings
@ProtoField(number = 11)
public String emailConstraint;
@ProtoField(number = 12)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Set<HotRodAttributeEntity> attributes;
@ProtoField(number = 13)
public Set<String> requiredActions;
@ProtoField(number = 14)
public List<HotRodUserCredentialEntity> credentials;
@ProtoField(number = 15)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Set<HotRodUserFederatedIdentityEntity> federatedIdentities;
@ProtoField(number = 16)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Set<HotRodUserConsentEntity> userConsents;
@ProtoField(number = 17)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Set<String> groupsMembership = new HashSet<>();
@ProtoField(number = 18)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public Set<String> rolesMembership = new HashSet<>();
@ProtoField(number = 19)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String federationLink;
@ProtoField(number = 20)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String serviceAccountClientLink;
@ProtoField(number = 21)
public Integer notBefore;
public static abstract class AbstractHotRodUserEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodUserEntity> implements MapUserEntity {
@Override
public String getId() {
return getHotRodEntity().id;
}
@Override
public void setId(String id) {
HotRodUserEntity entity = getHotRodEntity();
if (entity.id != null) throw new IllegalStateException("Id cannot be changed");
entity.id = id;
entity.updated |= id != null;
}
@Override
public void setEmail(String email, boolean duplicateEmailsAllowed) {
this.setEmail(email);
this.setEmailConstraint(email == null || duplicateEmailsAllowed ? KeycloakModelUtils.generateId() : email);
}
@Override
public void setUsername(String username) {
HotRodUserEntity entity = getHotRodEntity();
entity.updated |= ! Objects.equals(entity.username, username);
entity.username = username;
entity.usernameLowercase = username == null ? null : username.toLowerCase();
}
@Override
public boolean isUpdated() {
return getHotRodEntity().updated
|| Optional.ofNullable(getUserConsents()).orElseGet(Collections::emptySet).stream().anyMatch(MapUserConsentEntity::isUpdated)
|| Optional.ofNullable(getCredentials()).orElseGet(Collections::emptyList).stream().anyMatch(MapUserCredentialEntity::isUpdated)
|| Optional.ofNullable(getFederatedIdentities()).orElseGet(Collections::emptySet).stream().anyMatch(MapUserFederatedIdentityEntity::isUpdated);
}
@Override
public void clearUpdatedFlag() {
getHotRodEntity().updated = false;
Optional.ofNullable(getUserConsents()).orElseGet(Collections::emptySet).forEach(UpdatableEntity::clearUpdatedFlag);
Optional.ofNullable(getCredentials()).orElseGet(Collections::emptyList).forEach(UpdatableEntity::clearUpdatedFlag);
Optional.ofNullable(getFederatedIdentities()).orElseGet(Collections::emptySet).forEach(UpdatableEntity::clearUpdatedFlag);
}
@Override
public Optional<MapUserConsentEntity> getUserConsent(String clientId) {
Set<HotRodUserConsentEntity> ucs = getHotRodEntity().userConsents;
if (ucs == null || ucs.isEmpty()) return Optional.empty();
return ucs.stream().filter(uc -> Objects.equals(uc.clientId, clientId)).findFirst().map(HotRodUserConsentEntityDelegate::new);
}
@Override
public Boolean removeUserConsent(String clientId) {
Set<HotRodUserConsentEntity> consents = getHotRodEntity().userConsents;
boolean removed = consents != null && consents.removeIf(uc -> Objects.equals(uc.clientId, clientId));
getHotRodEntity().updated |= removed;
return removed;
}
@Override
public Optional<MapUserCredentialEntity> getCredential(String id) {
List<HotRodUserCredentialEntity> uce = getHotRodEntity().credentials;
if (uce == null || uce.isEmpty()) return Optional.empty();
return uce.stream().filter(uc -> Objects.equals(uc.id, id)).findFirst().map(HotRodUserCredentialEntityDelegate::new);
}
@Override
public Boolean removeCredential(String id) {
List<HotRodUserCredentialEntity> credentials = getHotRodEntity().credentials;
boolean removed = credentials != null && credentials.removeIf(c -> Objects.equals(c.id, id));
getHotRodEntity().updated |= removed;
return removed;
}
@Override
public Boolean moveCredential(String credentialId, String newPreviousCredentialId) {
// 1 - Get all credentials from the entity.
List<HotRodUserCredentialEntity> credentialsList = getHotRodEntity().credentials;
// 2 - Find indexes of our and newPrevious credential
int ourCredentialIndex = -1;
int newPreviousCredentialIndex = -1;
HotRodUserCredentialEntity ourCredential = null;
int i = 0;
for (HotRodUserCredentialEntity credential : credentialsList) {
if (credentialId.equals(credential.id)) {
ourCredentialIndex = i;
ourCredential = credential;
} else if(newPreviousCredentialId != null && newPreviousCredentialId.equals(credential.id)) {
newPreviousCredentialIndex = i;
}
i++;
}
if (ourCredentialIndex == -1) {
LOG.warnf("Not found credential with id [%s] of user [%s]", credentialId, getUsername());
return false;
}
if (newPreviousCredentialId != null && newPreviousCredentialIndex == -1) {
LOG.warnf("Can't move up credential with id [%s] of user [%s]", credentialId, getUsername());
return false;
}
// 3 - Compute index where we move our credential
int toMoveIndex = newPreviousCredentialId==null ? 0 : newPreviousCredentialIndex + 1;
// 4 - Insert our credential to new position, remove it from the old position
if (toMoveIndex == ourCredentialIndex) return true;
credentialsList.add(toMoveIndex, ourCredential);
int indexToRemove = toMoveIndex < ourCredentialIndex ? ourCredentialIndex + 1 : ourCredentialIndex;
credentialsList.remove(indexToRemove);
getHotRodEntity().updated = true;
return true;
}
@Override
public Optional<MapUserFederatedIdentityEntity> getFederatedIdentity(String identityProviderId) {
Set<HotRodUserFederatedIdentityEntity> fes = getHotRodEntity().federatedIdentities;
if (fes == null || fes.isEmpty()) return Optional.empty();
return fes.stream().filter(fi -> Objects.equals(fi.identityProvider, identityProviderId)).findFirst().map(HotRodUserFederatedIdentityEntityDelegate::new);
}
@Override
public Boolean removeFederatedIdentity(String identityProviderId) {
Set<HotRodUserFederatedIdentityEntity> federatedIdentities = getHotRodEntity().federatedIdentities;
boolean removed = federatedIdentities != null && federatedIdentities.removeIf(fi -> Objects.equals(fi.identityProvider, identityProviderId));
getHotRodEntity().updated |= removed;
return removed;
}
}
@Override
public boolean equals(Object o) {
return HotRodUserEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodUserEntityDelegate.entityHashCode(this);
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2022 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.user;
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.client.HotRodProtocolMapperEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
@GenerateHotRodEntityImplementation(implementInterface = "org.keycloak.models.map.user.MapUserFederatedIdentityEntity")
@ProtoDoc("@Indexed")
public class HotRodUserFederatedIdentityEntity extends AbstractHotRodEntity {
@ProtoField(number = 1)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String identityProvider;
@ProtoField(number = 2)
public String token;
@ProtoField(number = 3)
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
public String userId;
@ProtoField(number = 4)
public String userName;
@Override
public boolean equals(Object o) {
return HotRodUserFederatedIdentityEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodUserFederatedIdentityEntityDelegate.entityHashCode(this);
}
}

View file

@ -20,5 +20,15 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="users" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserEntity</indexed-entity>
<indexed-entity>kc.HotRodAttributeEntity</indexed-entity>
<indexed-entity>kc.HotRodUserFederatedIdentityEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -22,5 +22,15 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="users" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserEntity</indexed-entity>
<indexed-entity>kc.HotRodAttributeEntity</indexed-entity>
<indexed-entity>kc.HotRodUserFederatedIdentityEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -19,10 +19,11 @@ package org.keycloak.models.map.storage.hotRod;
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
@ -69,4 +70,23 @@ public class IckleQueryMapModelCriteriaBuilderTest {
assertThat(mcb.getParameters().entrySet(), hasSize(2));
assertThat(mcb.getParameters(), allOf(hasEntry("clientId0", 4), hasEntry("id0", 5)));
}
@Test
public void testUser() {
final DefaultModelCriteria<UserModel> mcb = criteria();
DefaultModelCriteria<UserModel> criteria = mcb.compare(UserModel.SearchableFields.REALM_ID, ModelCriteriaBuilder.Operator.EQ, "realm1");
criteria = criteria.compare(UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT, ModelCriteriaBuilder.Operator.NOT_EXISTS);
criteria = mcb.and(criteria, mcb.or(
mcb.compare(UserModel.SearchableFields.USERNAME, ModelCriteriaBuilder.Operator.ILIKE, "a"),
mcb.compare(UserModel.SearchableFields.EMAIL, ModelCriteriaBuilder.Operator.ILIKE, "a"),
mcb.compare(UserModel.SearchableFields.FIRST_NAME, ModelCriteriaBuilder.Operator.ILIKE, "a"),
mcb.compare(UserModel.SearchableFields.LAST_NAME, ModelCriteriaBuilder.Operator.ILIKE, "a")
));
IckleQueryMapModelCriteriaBuilder<HotRodUserEntity, UserModel> ickle = criteria.flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>(HotRodUserEntity.class));
assertThat(ickle.getIckleQuery(), is(equalTo("FROM kc.HotRodUserEntity c WHERE ((c.realmId = :realmId0) AND (c.serviceAccountClientLink IS NULL OR c.serviceAccountClientLink IS EMPTY) AND ((c.usernameLowercase LIKE :usernameLowercase0) OR (c.email : 'a') OR (c.firstName : 'a') OR (c.lastName : 'a')))")));
assertThat(ickle.getParameters().entrySet(), hasSize(2));
assertThat(ickle.getParameters(), allOf(hasEntry("realmId0", "realm1"), hasEntry("usernameLowercase0", "a")));
}
}

View file

@ -200,12 +200,6 @@ public interface MapUserEntity extends UpdatableEntity, AbstractEntity, EntityWi
String getEmailConstraint();
void setEmailConstraint(String emailConstraint);
Map<String, List<String>> getAttributes();
List<String> getAttribute(String name);
void setAttributes(Map<String, List<String>> attributes);
void setAttribute(String name, List<String> value);
void removeAttribute(String name);
Set<String> getRequiredActions();
void setRequiredActions(Set<String> requiredActions);
void addRequiredAction(String requiredAction);

View file

@ -593,11 +593,11 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace());
DefaultModelCriteria<UserModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
final DefaultModelCriteria<UserModel> mcb = criteria();
DefaultModelCriteria<UserModel> criteria = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
if (! session.getAttributeOrDefault(UserModel.INCLUDE_SERVICE_ACCOUNT, true)) {
mcb = mcb.compare(SearchableFields.SERVICE_ACCOUNT_CLIENT, Operator.NOT_EXISTS);
criteria = criteria.compare(SearchableFields.SERVICE_ACCOUNT_CLIENT, Operator.NOT_EXISTS);
}
final boolean exactSearch = Boolean.parseBoolean(attributes.getOrDefault(UserModel.EXACT, Boolean.FALSE.toString()));
@ -614,47 +614,54 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
switch (key) {
case UserModel.SEARCH:
DefaultModelCriteria<UserModel> searchCriteria = null;
for (String stringToSearch : value.split("\\s+")) {
mcb = addSearchToModelCriteria(stringToSearch, mcb);
if (searchCriteria == null) {
searchCriteria = addSearchToModelCriteria(stringToSearch, mcb);
} else {
searchCriteria = mcb.and(searchCriteria, addSearchToModelCriteria(stringToSearch, mcb));
}
}
criteria = mcb.and(criteria, searchCriteria);
break;
case USERNAME:
mcb = mcb.compare(SearchableFields.USERNAME, Operator.ILIKE, searchedString);
criteria = criteria.compare(SearchableFields.USERNAME, Operator.ILIKE, searchedString);
break;
case FIRST_NAME:
mcb = mcb.compare(SearchableFields.FIRST_NAME, Operator.ILIKE, searchedString);
criteria = criteria.compare(SearchableFields.FIRST_NAME, Operator.ILIKE, searchedString);
break;
case LAST_NAME:
mcb = mcb.compare(SearchableFields.LAST_NAME, Operator.ILIKE, searchedString);
criteria = criteria.compare(SearchableFields.LAST_NAME, Operator.ILIKE, searchedString);
break;
case EMAIL:
mcb = mcb.compare(SearchableFields.EMAIL, Operator.ILIKE, searchedString);
criteria = criteria.compare(SearchableFields.EMAIL, Operator.ILIKE, searchedString);
break;
case EMAIL_VERIFIED: {
boolean booleanValue = Boolean.parseBoolean(value);
mcb = mcb.compare(SearchableFields.EMAIL_VERIFIED, Operator.EQ, booleanValue);
criteria = criteria.compare(SearchableFields.EMAIL_VERIFIED, Operator.EQ, booleanValue);
break;
}
case UserModel.ENABLED: {
boolean booleanValue = Boolean.parseBoolean(value);
mcb = mcb.compare(SearchableFields.ENABLED, Operator.EQ, booleanValue);
criteria = criteria.compare(SearchableFields.ENABLED, Operator.EQ, booleanValue);
break;
}
case UserModel.IDP_ALIAS: {
if (!attributes.containsKey(UserModel.IDP_USER_ID)) {
mcb = mcb.compare(SearchableFields.IDP_AND_USER, Operator.EQ, value);
criteria = criteria.compare(SearchableFields.IDP_AND_USER, Operator.EQ, value);
}
break;
}
case UserModel.IDP_USER_ID: {
mcb = mcb.compare(SearchableFields.IDP_AND_USER, Operator.EQ, attributes.get(UserModel.IDP_ALIAS),
criteria = criteria.compare(SearchableFields.IDP_AND_USER, Operator.EQ, attributes.get(UserModel.IDP_ALIAS),
value);
break;
}
case UserModel.EXACT:
break;
default:
mcb = mcb.compare(SearchableFields.ATTRIBUTE, Operator.EQ, key, value);
criteria = criteria.compare(SearchableFields.ATTRIBUTE, Operator.EQ, key, value);
break;
}
}
@ -680,10 +687,10 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor
return resourceStore.findByResourceServer(values, null, 0, 1).isEmpty();
});
mcb = mcb.compare(SearchableFields.ASSIGNED_GROUP, Operator.IN, authorizedGroups);
criteria = criteria.compare(SearchableFields.ASSIGNED_GROUP, Operator.IN, authorizedGroups);
}
return tx.read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.USERNAME))
return tx.read(withCriteria(criteria).pagination(firstResult, maxResults, SearchableFields.USERNAME))
.map(entityToAdapterFunc(realm))
.filter(Objects::nonNull);
}

View file

@ -205,4 +205,5 @@ public class DefaultModelCriteriaTest {
.partiallyEvaluate((field, operator, operatorArguments) -> field == CLIENT_ID && operator == Operator.EQ && Arrays.asList(operatorArguments).contains(6) ? true : null),
hasToString("(clientId EQ [4] && id EQ [5])"));
}
}

View file

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

View file

@ -1498,6 +1498,7 @@
<systemPropertyVariables>
<keycloak.client.map.storage.provider>hotrod</keycloak.client.map.storage.provider>
<keycloak.group.map.storage.provider>hotrod</keycloak.group.map.storage.provider>
<keycloak.user.map.storage.provider>hotrod</keycloak.user.map.storage.provider>
</systemPropertyVariables>
</configuration>
</plugin>

View file

@ -18,5 +18,15 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="users" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserEntity</indexed-entity>
<indexed-entity>kc.HotRodAttributeEntity</indexed-entity>
<indexed-entity>kc.HotRodUserFederatedIdentityEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -75,12 +75,12 @@ public class HotRodMapStorage extends KeycloakModelParameters {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("role").provider(MapRoleProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(DeploymentStateSpi.NAME).provider(MapDeploymentStateProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(StoreFactorySpi.NAME).provider(MapAuthorizationStoreFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("user").provider(MapUserProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("user").provider(MapUserProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID).config("storage-user-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.config("storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(UserLoginFailureSpi.NAME).provider(MapUserLoginFailureProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)