diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java index c3eebcacbd..ec03ffeda9 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/annotations/IgnoreForEntityImplementationGenerator.java @@ -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 { } diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java index d04057e52e..082da94012 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/AbstractGenerateEntityImplementationsProcessor.java @@ -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 + ")"; } diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java index bb6a40bc99..7a5f4f1cdd 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateEntityImplementationsProcessor.java @@ -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);"); diff --git a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java index cf834a79eb..6c8e8abad0 100644 --- a/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java +++ b/model/build-processor/src/main/java/org/keycloak/models/map/processor/GenerateHotRodEntityImplementationsProcessor.java @@ -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 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 findSuitableConstructor(TypeMirror desiredType, TypeMirror[] parameters) { diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java index ecb980d49c..7bd6b537dd 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java @@ -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, M> implements MapStorage, ConcurrentHashMapCrudOperations { +public class HotRodMapStorage & AbstractEntity, M> implements MapStorage, ConcurrentHashMapCrudOperations { private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class); @@ -146,7 +147,7 @@ public class HotRodMapStorage query = queryFactory.create(queryString); + Query query = queryFactory.create(queryString); query.setParameters(iqmcb.getParameters()); return query.execute().hitCount().orElse(0); @@ -167,17 +168,18 @@ public class HotRodMapStorage query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), + Query query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), queryParameters.getLimit()); query.setParameters(iqmcb.getParameters()); AtomicLong result = new AtomicLong(); - CloseableIterator iterator = query.iterator(); + CloseableIterator 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(); diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java index 438b4d7758..fec06e1d5d 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java @@ -47,7 +47,7 @@ public class HotRodMapStorageProvider implements MapStorageProvider { } @SuppressWarnings("unchecked") - public , M> HotRodMapStorage getHotRodStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { + public & AbstractEntity, M> HotRodMapStorage getHotRodStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) factory.getEntityDescriptor(modelType); return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java index e65c632e4d..2cce70aaa0 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java @@ -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, 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 diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java index ac2420f573..85741c06ff 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java @@ -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 hotRodEntityClass; private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY); private final Map parameters; - private static final String NON_ANALYZED_FIELD_REGEX = "[%_\\\\]"; - private static final String ANALYZED_FIELD_REGEX = "[+!^\"~*?:\\\\]"; + 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, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>(); public static final Set> ANALYZED_MODEL_FIELDS = new HashSet<>(); @@ -55,6 +58,14 @@ public class IckleQueryMapModelCriteriaBuilder, 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 parameters) { + private static String whereClauseForAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map 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 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 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); + } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java index b63d0db9a5..9edb4d8dbd 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodEntityDelegate.java @@ -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 extends AbstractEntity, UpdatableEntity { +public interface HotRodEntityDelegate extends UpdatableEntity { E getHotRodEntity(); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java index 7421180bba..03dc281b76 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/HotRodTypesUtils.java @@ -18,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 List migrateList(List p0, Function migrator) { + return p0 == null ? null : p0.stream().map(migrator).collect(Collectors.toList()); + } + + public static Set migrateSet(Set p0, Function migrator) { + return p0 == null ? null : p0.stream().map(migrator).collect(Collectors.toSet()); + } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java index 579bb50658..eb782efed6 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java @@ -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 Martin Kanis @@ -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, diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java index bbcb0aeb56..9098ed0a7b 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/group/HotRodGroupEntity.java @@ -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 grantedRoles; + + @Override + public boolean equals(Object o) { + return HotRodGroupEntityDelegate.entityEquals(this, o); + } + + @Override + public int hashCode() { + return HotRodGroupEntityDelegate.entityHashCode(this); + } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserConsentEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserConsentEntity.java new file mode 100644 index 0000000000..3d77a595e3 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserConsentEntity.java @@ -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 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); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserCredentialEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserCredentialEntity.java new file mode 100644 index 0000000000..fb029bf96e --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserCredentialEntity.java @@ -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); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserEntity.java new file mode 100644 index 0000000000..1dd0219892 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserEntity.java @@ -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 attributes; + + @ProtoField(number = 13) + public Set requiredActions; + + @ProtoField(number = 14) + public List credentials; + + @ProtoField(number = 15) + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + public Set federatedIdentities; + + @ProtoField(number = 16) + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + public Set userConsents; + + @ProtoField(number = 17) + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + public Set groupsMembership = new HashSet<>(); + + @ProtoField(number = 18) + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + public Set 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 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 getUserConsent(String clientId) { + Set 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 consents = getHotRodEntity().userConsents; + boolean removed = consents != null && consents.removeIf(uc -> Objects.equals(uc.clientId, clientId)); + getHotRodEntity().updated |= removed; + return removed; + } + + @Override + public Optional getCredential(String id) { + List 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 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 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 getFederatedIdentity(String identityProviderId) { + Set 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 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); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserFederatedIdentityEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserFederatedIdentityEntity.java new file mode 100644 index 0000000000..996f4c60e5 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/user/HotRodUserFederatedIdentityEntity.java @@ -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); + } +} diff --git a/model/map-hot-rod/src/main/resources/config/cacheConfig.xml b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml index d63b26aa21..005495f270 100644 --- a/model/map-hot-rod/src/main/resources/config/cacheConfig.xml +++ b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml @@ -20,5 +20,15 @@ + + + + kc.HotRodUserEntity + kc.HotRodAttributeEntity + kc.HotRodUserFederatedIdentityEntity + + + + \ No newline at end of file diff --git a/model/map-hot-rod/src/main/resources/config/infinispan.xml b/model/map-hot-rod/src/main/resources/config/infinispan.xml index 44e6fc8aa0..c4f29b8938 100644 --- a/model/map-hot-rod/src/main/resources/config/infinispan.xml +++ b/model/map-hot-rod/src/main/resources/config/infinispan.xml @@ -22,5 +22,15 @@ + + + + kc.HotRodUserEntity + kc.HotRodAttributeEntity + kc.HotRodUserFederatedIdentityEntity + + + + diff --git a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java index 4cb5ffe64c..45b848eec0 100644 --- a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java +++ b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java @@ -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 mcb = criteria(); + DefaultModelCriteria 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 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"))); + } } \ No newline at end of file diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java index e56927ff29..0872905d35 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserEntity.java @@ -200,12 +200,6 @@ public interface MapUserEntity extends UpdatableEntity, AbstractEntity, EntityWi String getEmailConstraint(); void setEmailConstraint(String emailConstraint); - Map> getAttributes(); - List getAttribute(String name); - void setAttributes(Map> attributes); - void setAttribute(String name, List value); - void removeAttribute(String name); - Set getRequiredActions(); void setRequiredActions(Set requiredActions); void addRequiredAction(String requiredAction); diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java index 6fb5d6a9ab..0170892e37 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProvider.java @@ -593,11 +593,11 @@ public class MapUserProvider implements UserProvider.Streams, UserCredentialStor public Stream searchForUserStream(RealmModel realm, Map attributes, Integer firstResult, Integer maxResults) { LOG.tracef("searchForUserStream(%s, %s, %d, %d)%s", realm, attributes, firstResult, maxResults, getShortStackTrace()); - DefaultModelCriteria mcb = criteria(); - mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); + final DefaultModelCriteria mcb = criteria(); + DefaultModelCriteria 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 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); } diff --git a/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java b/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java index d3efd66799..96f39415a2 100644 --- a/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java +++ b/model/map/src/test/java/org/keycloak/models/map/storage/criteria/DefaultModelCriteriaTest.java @@ -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])")); } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index d3fcd9f1ac..2f709917b5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -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}" } }, diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 777cfeba90..87fdda08a1 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -1498,6 +1498,7 @@ hotrod hotrod + hotrod diff --git a/testsuite/model/src/main/resources/hotrod/infinispan.xml b/testsuite/model/src/main/resources/hotrod/infinispan.xml index 623ff4b3dd..9413267b56 100644 --- a/testsuite/model/src/main/resources/hotrod/infinispan.xml +++ b/testsuite/model/src/main/resources/hotrod/infinispan.xml @@ -18,5 +18,15 @@ + + + + kc.HotRodUserEntity + kc.HotRodAttributeEntity + kc.HotRodUserFederatedIdentityEntity + + + + diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java index 7224b89c82..f081b69621 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java @@ -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)