From b730d861e7a8f7bdb6b9fdc317dbabd866c3044e Mon Sep 17 00:00:00 2001 From: Michal Hajas Date: Wed, 12 Apr 2023 11:21:14 +0200 Subject: [PATCH] Refactor map storage transaction initialization * Refactor transaction to be enlisted in MapStorageProvider instead of area provider * Make KeycloakTransaction methods optional for MapKeycloakTransaction * Remove MapStorage interface that contained only createTransaction method * Rename *MapStorage to *CrudOperations * Adjust File store to new structure * Rename MapKeycloakTransaction to MapStorage * Rename getEnlistedTransaction to getMapStorage in AbstractMapProviderFactory * Rename variables tx and transaction to store * Add createMapStorageIfAbsent to JpaMapStorageProvider * Update JavaDoc Co-authored-by: Hynek Mlnarik --- .../map/storage/file/FileCrudOperations.java | 423 ++++++++++++ .../file/FileMapKeycloakTransaction.java | 266 -------- .../map/storage/file/FileMapStorage.java | 602 ++++++------------ .../storage/file/FileMapStorageProvider.java | 35 +- .../file/FileMapStorageProviderFactory.java | 32 +- ...Storage.java => HotRodCrudOperations.java} | 33 +- .../hotRod/HotRodMapStorageProvider.java | 89 ++- .../HotRodMapStorageProviderFactory.java | 108 +--- ... SingleUseObjectHotRodCrudOperations.java} | 33 +- ....java => AllAreasHotRodStoresWrapper.java} | 24 +- .../NoActionHotRodTransactionWrapper.java | 111 ---- ....java => HotRodUserSessionMapStorage.java} | 45 +- ...oakTransaction.java => JpaMapStorage.java} | 38 +- .../storage/jpa/JpaMapStorageProvider.java | 35 +- .../jpa/JpaMapStorageProviderFactory.java | 111 ++-- ...aRootAuthenticationSessionMapStorage.java} | 6 +- ...tion.java => JpaPermissionMapStorage.java} | 6 +- ...nsaction.java => JpaPolicyMapStorage.java} | 6 +- ...action.java => JpaResourceMapStorage.java} | 6 +- ....java => JpaResourceServerMapStorage.java} | 6 +- ...ansaction.java => JpaScopeMapStorage.java} | 6 +- ...nsaction.java => JpaClientMapStorage.java} | 6 +- ...ion.java => JpaClientScopeMapStorage.java} | 6 +- ...tion.java => JpaAdminEventMapStorage.java} | 9 +- ...ction.java => JpaAuthEventMapStorage.java} | 9 +- ...ansaction.java => JpaGroupMapStorage.java} | 6 +- .../listeners/JpaAutoFlushListener.java | 6 +- ...ransaction.java => JpaLockMapStorage.java} | 6 +- ...ava => JpaUserLoginFailureMapStorage.java} | 8 +- ...ansaction.java => JpaRealmMapStorage.java} | 9 +- ...ransaction.java => JpaRoleMapStorage.java} | 8 +- .../delegate/JpaMapRoleEntityDelegate.java | 3 +- ...java => JpaSingleUseObjectMapStorage.java} | 9 +- ...ransaction.java => JpaUserMapStorage.java} | 9 +- ...ion.java => JpaUserSessionMapStorage.java} | 6 +- ...akTransaction.java => LdapMapStorage.java} | 10 +- .../storage/ldap/LdapMapStorageProvider.java | 31 +- .../ldap/LdapMapStorageProviderFactory.java | 24 +- ...ansaction.java => LdapRoleMapStorage.java} | 7 +- .../ldap/role/entity/LdapRoleEntity.java | 20 +- .../MapRootAuthenticationSessionProvider.java | 31 +- ...tAuthenticationSessionProviderFactory.java | 2 +- .../authorization/MapAuthorizationStore.java | 20 +- .../MapAuthorizationStoreFactory.java | 12 +- .../MapPermissionTicketStore.java | 45 +- .../map/authorization/MapPolicyStore.java | 57 +- .../authorization/MapResourceServerStore.java | 31 +- .../map/authorization/MapResourceStore.java | 53 +- .../map/authorization/MapScopeStore.java | 41 +- .../models/map/client/MapClientProvider.java | 54 +- .../map/client/MapClientProviderFactory.java | 3 +- .../clientscope/MapClientScopeProvider.java | 32 +- .../MapClientScopeProviderFactory.java | 2 +- .../common/AbstractMapProviderFactory.java | 16 +- .../map/common/SessionAttributesUtils.java | 114 ++++ .../map/datastore/MapExportImportManager.java | 16 +- .../map/events/MapEventStoreProvider.java | 20 +- .../events/MapEventStoreProviderFactory.java | 4 +- .../models/map/group/MapGroupProvider.java | 48 +- .../map/group/MapGroupProviderFactory.java | 2 +- .../map/lock/MapGlobalLockProvider.java | 18 +- .../lock/MapGlobalLockProviderFactory.java | 2 +- .../MapUserLoginFailureProvider.java | 7 +- .../MapUserLoginFailureProviderFactory.java | 2 +- .../models/map/realm/MapRealmProvider.java | 20 +- .../map/realm/MapRealmProviderFactory.java | 2 +- .../models/map/role/MapRoleProvider.java | 52 +- .../map/role/MapRoleProviderFactory.java | 2 +- .../MapSingleUseObjectProvider.java | 12 +- .../MapSingleUseObjectProviderFactory.java | 2 +- .../models/map/storage/CrudOperations.java | 140 ++++ .../map/storage/MapKeycloakTransaction.java | 120 ---- .../MapKeycloakTransactionWithAuth.java | 41 -- .../models/map/storage/MapStorage.java | 118 +++- .../map/storage/MapStorageProvider.java | 9 +- .../map/storage/MapStorageWithAuth.java | 27 +- .../chm/ConcurrentHashMapCrudOperations.java | 254 +++++--- .../ConcurrentHashMapKeycloakTransaction.java | 514 --------------- .../storage/chm/ConcurrentHashMapStorage.java | 572 +++++++++++++---- .../chm/ConcurrentHashMapStorageProvider.java | 29 +- ...ncurrentHashMapStorageProviderFactory.java | 33 +- ...bjectConcurrentHashMapCrudOperations.java} | 19 +- ...on.java => SingleUseObjectMapStorage.java} | 11 +- .../map/storage/tree/EmptyMapStorage.java | 43 +- .../models/map/user/MapUserProvider.java | 70 +- .../map/user/MapUserProviderFactory.java | 2 +- .../userSession/MapUserSessionProvider.java | 70 +- .../MapUserSessionProviderFactory.java | 2 +- .../model/ConcurrentHashMapStorageTest.java | 43 +- .../storage/tree/sample/DictStorage.java | 99 +-- 90 files changed, 2404 insertions(+), 2747 deletions(-) create mode 100644 model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileCrudOperations.java delete mode 100644 model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapKeycloakTransaction.java rename model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/{HotRodMapStorage.java => HotRodCrudOperations.java} (87%) rename model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/{SingleUseObjectHotRodMapStorage.java => SingleUseObjectHotRodCrudOperations.java} (57%) rename model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/{AllAreasHotRodTransactionsWrapper.java => AllAreasHotRodStoresWrapper.java} (56%) delete mode 100644 model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java rename model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/{HotRodUserSessionTransaction.java => HotRodUserSessionMapStorage.java} (77%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/{JpaMapKeycloakTransaction.java => JpaMapStorage.java} (93%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/{JpaRootAuthenticationSessionMapKeycloakTransaction.java => JpaRootAuthenticationSessionMapStorage.java} (93%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/{JpaPermissionMapKeycloakTransaction.java => JpaPermissionMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/{JpaPolicyMapKeycloakTransaction.java => JpaPolicyMapStorage.java} (90%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/{JpaResourceMapKeycloakTransaction.java => JpaResourceMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/{JpaResourceServerMapKeycloakTransaction.java => JpaResourceServerMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/{JpaScopeMapKeycloakTransaction.java => JpaScopeMapStorage.java} (90%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/{JpaClientMapKeycloakTransaction.java => JpaClientMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/{JpaClientScopeMapKeycloakTransaction.java => JpaClientScopeMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/{JpaAdminEventMapKeycloakTransaction.java => JpaAdminEventMapStorage.java} (83%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/{JpaAuthEventMapKeycloakTransaction.java => JpaAuthEventMapStorage.java} (83%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/{JpaGroupMapKeycloakTransaction.java => JpaGroupMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/{JpaLockMapKeycloakTransaction.java => JpaLockMapStorage.java} (89%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/{JpaUserLoginFailureMapKeycloakTransaction.java => JpaUserLoginFailureMapStorage.java} (86%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/{JpaRealmMapKeycloakTransaction.java => JpaRealmMapStorage.java} (86%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/{JpaRoleMapKeycloakTransaction.java => JpaRoleMapStorage.java} (90%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/{JpaSingleUseObjectMapKeycloakTransaction.java => JpaSingleUseObjectMapStorage.java} (82%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/{JpaUserMapKeycloakTransaction.java => JpaUserMapStorage.java} (88%) rename model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/{JpaUserSessionMapKeycloakTransaction.java => JpaUserSessionMapStorage.java} (86%) rename model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/{LdapMapKeycloakTransaction.java => LdapMapStorage.java} (87%) rename model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/{LdapRoleMapKeycloakTransaction.java => LdapRoleMapStorage.java} (97%) create mode 100644 model/map/src/main/java/org/keycloak/models/map/common/SessionAttributesUtils.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/CrudOperations.java delete mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java delete mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransactionWithAuth.java delete mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java rename model/map/src/main/java/org/keycloak/models/map/storage/chm/{SingleUseObjectConcurrentHashMapStorage.java => SingleUseObjectConcurrentHashMapCrudOperations.java} (68%) rename model/map/src/main/java/org/keycloak/models/map/storage/chm/{SingleUseObjectKeycloakTransaction.java => SingleUseObjectMapStorage.java} (75%) diff --git a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileCrudOperations.java b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileCrudOperations.java new file mode 100644 index 0000000000..8cb3081fed --- /dev/null +++ b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileCrudOperations.java @@ -0,0 +1,423 @@ +/* + * Copyright 2023 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.file; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.StackUtil; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.ExpirationUtils; +import org.keycloak.models.map.common.HasRealmId; +import org.keycloak.models.map.common.StringKeyConverter; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.realm.MapRealmEntity; +import org.keycloak.models.map.storage.ModelEntityUtil; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.CrudOperations; +import org.keycloak.models.map.storage.chm.MapFieldPredicates; +import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; +import org.keycloak.models.map.storage.file.common.MapEntityContext; +import org.keycloak.models.map.storage.file.yaml.PathWriter; +import org.keycloak.models.map.storage.file.yaml.YamlParser; +import org.keycloak.models.map.storage.file.yaml.YamlWritingMechanism; +import org.keycloak.storage.SearchableModelField; +import org.snakeyaml.engine.v2.emitter.Emitter; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.snakeyaml.engine.v2.api.DumpSettings; +import static org.keycloak.utils.StreamsUtil.paginatedStream; + +public abstract class FileCrudOperations implements CrudOperations, HasRealmId { + + private static final Logger LOG = Logger.getLogger(FileCrudOperations.class); + private String defaultRealmId; + private final Class entityClass; + private final Function dataDirectoryFunc; + private final Function suggestedPath; + private final boolean isExpirableEntity; + private final Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates; + + private static final Map, Map, MapModelCriteriaBuilder.UpdatePredicatesFunc>> ENTITY_FIELD_PREDICATES = new HashMap<>(); + + public static final String SEARCHABLE_FIELD_REALM_ID_FIELD_NAME = ClientModel.SearchableFields.REALM_ID.getName(); + public static final String FILE_SUFFIX = ".yaml"; + public static final DumpSettings DUMP_SETTINGS = DumpSettings.builder() + .setIndent(4) + .setIndicatorIndent(2) + .setIndentWithIndicator(false) + .build(); + + public FileCrudOperations(Class entityClass, + Function dataDirectoryFunc, + Function suggestedPath, + boolean isExpirableEntity) { + this.entityClass = entityClass; + this.dataDirectoryFunc = dataDirectoryFunc; + this.suggestedPath = suggestedPath; + this.isExpirableEntity = isExpirableEntity; + this.fieldPredicates = new IdentityHashMap<>(getPredicates(entityClass)); + this.fieldPredicates.keySet().stream() // Ignore realmId since this is treated in reading differently + .filter(f -> Objects.equals(SEARCHABLE_FIELD_REALM_ID_FIELD_NAME, f.getName())) + .findAny() + .ifPresent(key -> this.fieldPredicates.replace(key, (builder, op, params) -> builder)); + } + + @SuppressWarnings("unchecked") + public static Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> getPredicates(Class entityClass) { + return (Map) ENTITY_FIELD_PREDICATES.computeIfAbsent(entityClass, n -> { + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = new IdentityHashMap<>(MapFieldPredicates.getPredicates(ModelEntityUtil.getModelType(entityClass))); + fieldPredicates.keySet().stream() // Ignore realmId since this is treated in reading differently + .filter(f -> Objects.equals(SEARCHABLE_FIELD_REALM_ID_FIELD_NAME, f.getName())) + .findAny() + .ifPresent(key -> fieldPredicates.replace(key, (builder, op, params) -> builder)); + + return (Map) fieldPredicates; + }); + } + + protected Path getPathForEscapedId(String[] escapedIdPathArray) { + Path parentDirectory = getDataDirectory(); + Path targetPath = parentDirectory; + for (String path : escapedIdPathArray) { + targetPath = targetPath.resolve(path).normalize(); + if (!targetPath.getParent().equals(parentDirectory)) { + LOG.warnf("Path traversal detected: %s", Arrays.toString(escapedIdPathArray)); + return null; + } + parentDirectory = targetPath; + } + + return targetPath.resolveSibling(targetPath.getFileName() + FILE_SUFFIX); + } + + protected Path getPathForEscapedId(String escapedId) { + if (escapedId == null) { + throw new IllegalStateException("Invalid ID to escape"); + } + + String[] escapedIdArray = ID_COMPONENT_SEPARATOR_PATTERN.split(escapedId); + return getPathForEscapedId(escapedIdArray); + } + + // Percent sign + Unix (/) and https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file reserved characters + private static final Pattern RESERVED_CHARACTERS = Pattern.compile("[%<:>\"/\\\\|?*=]"); + private static final String ID_COMPONENT_SEPARATOR = ":"; + private static final String ESCAPING_CHARACTER = "="; + private static final Pattern ID_COMPONENT_SEPARATOR_PATTERN = Pattern.compile(Pattern.quote(ID_COMPONENT_SEPARATOR) + "+"); + + private static String[] escapeId(String[] idArray) { + if (idArray == null || idArray.length == 0 || idArray.length == 1 && idArray[0] == null) { + return null; + } + return Stream.of(idArray) + .map(FileCrudOperations::escapeId) + .toArray(String[]::new); + } + + private static String escapeId(String id) { + Objects.requireNonNull(id, "ID must be non-null"); + + StringBuilder idEscaped = new StringBuilder(); + Matcher m = RESERVED_CHARACTERS.matcher(id); + while (m.find()) { + m.appendReplacement(idEscaped, String.format(ESCAPING_CHARACTER + "%02x", (int) m.group().charAt(0))); + } + m.appendTail(idEscaped); + final Path pId = Path.of(idEscaped.toString()); + + return pId.toString(); + } + + public static boolean canParseFile(Path p) { + final String fn = p.getFileName().toString(); + try { + return Files.isRegularFile(p) + && Files.size(p) > 0L + && ! fn.startsWith(".") + && fn.endsWith(FILE_SUFFIX) + && Files.isReadable(p); + } catch (IOException ex) { + return false; + } + } + + protected V parse(Path fileName) { + getLastModifiedTime(fileName); + final V parsedObject = YamlParser.parse(fileName, new MapEntityContext<>(entityClass)); + if (parsedObject == null) { + LOG.debugf("Could not parse %s%s", fileName, StackUtil.getShortStackTrace()); + return null; + } + + String escapedId = determineKeyFromValue(parsedObject, false); + final String fileNameStr = fileName.getFileName().toString(); + final String idFromFilename = fileNameStr.substring(0, fileNameStr.length() - FILE_SUFFIX.length()); + if (escapedId == null) { + LOG.debugf("Determined ID from filename: %s%s", idFromFilename); + escapedId = idFromFilename; + } else if (!escapedId.endsWith(idFromFilename)) { + LOG.warnf("Id \"%s\" does not conform with filename \"%s\", expected: %s", escapedId, fileNameStr, escapeId(escapedId)); + } + + parsedObject.setId(escapedId); + parsedObject.clearUpdatedFlag(); + + return parsedObject; + } + + @Override + public V create(V value) { + // TODO: Lock realm directory for changes (e.g. on realm deletion) + String escapedId = value.getId(); + + writeYamlContents(getPathForEscapedId(escapedId), value); + + return value; + } + + /** + * Returns escaped ID - relative file name in the file system with path separator {@link #ID_COMPONENT_SEPARATOR}. + * + * @param value Object + * @param forCreate Whether this is for create operation ({@code true}) or + * @return + */ + @Override + public String determineKeyFromValue(V value, boolean forCreate) { + final boolean randomId; + String[] proposedId = suggestedPath.apply(value); + + if (!forCreate) { + String[] escapedProposedId = escapeId(proposedId); + final String res = proposedId == null ? null : String.join(ID_COMPONENT_SEPARATOR, escapedProposedId); + if (LOG.isDebugEnabled()) { + LOG.debugf("determineKeyFromValue: got %s (%s) for %s", res, res == null ? null : String.join(" [/] ", proposedId), value); + } + return res; + } + + if (proposedId == null || proposedId.length == 0) { + randomId = value.getId() == null; + proposedId = new String[]{value.getId() == null ? StringKeyConverter.StringKey.INSTANCE.yieldNewUniqueKey() : value.getId()}; + } else { + randomId = false; + } + + String[] escapedProposedId = escapeId(proposedId); + Path sp = getPathForEscapedId(escapedProposedId); // sp will never be null + + final Path parentDir = sp.getParent(); + if (!Files.isDirectory(parentDir)) { + try { + Files.createDirectories(parentDir); + } catch (IOException ex) { + throw new IllegalStateException("Directory does not exist and cannot be created: " + parentDir, ex); + } + } + + for (int counter = 0; counter < 100; counter++) { + LOG.tracef("Attempting to create file %s", sp, StackUtil.getShortStackTrace()); + try { + touch(sp); + final String res = String.join(ID_COMPONENT_SEPARATOR, escapedProposedId); + LOG.debugf("determineKeyFromValue: got %s for created %s", res, value); + return res; + } catch (FileAlreadyExistsException ex) { + if (!randomId) { + throw new ModelDuplicateException("File " + sp + " already exists!"); + } + final String lastComponent = StringKeyConverter.StringKey.INSTANCE.yieldNewUniqueKey(); + escapedProposedId[escapedProposedId.length - 1] = lastComponent; + sp = getPathForEscapedId(escapedProposedId); + } catch (IOException ex) { + throw new IllegalStateException("Could not create file " + sp, ex); + } + } + + return null; + } + + @Override + public V read(String key) { + return Optional.ofNullable(key) + .map(this::getPathForEscapedId) + .filter(Files::isReadable) + .map(this::parse) + .orElse(null); + } + + public MapModelCriteriaBuilder createCriteriaBuilder() { + return new MapModelCriteriaBuilder<>(StringKeyConverter.StringKey.INSTANCE, fieldPredicates); + } + + @Override + public Stream read(QueryParameters queryParameters) { + final List paths; + FileCriteriaBuilder cb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(FileCriteriaBuilder.criteria()); + String realmId = (String) cb.getSingleRestrictionArgument(SEARCHABLE_FIELD_REALM_ID_FIELD_NAME); + setRealmId(realmId); + + final Path dataDirectory = getDataDirectory(); + if (!Files.isDirectory(dataDirectory)) { + return Stream.empty(); + } + + // We cannot use Files.find since it throws an UncheckedIOException if it lists a file which is removed concurrently + // before its BasicAttributes can be retrieved for its BiPredicate parameter + try (Stream dirStream = Files.walk(dataDirectory, entityClass == MapRealmEntity.class ? 1 : 2)) { + // The paths list has to be materialized first, otherwise "dirStream" would be closed + // before the resulting stream would be read and would return empty result + paths = dirStream.collect(Collectors.toList()); + } catch (IOException | UncheckedIOException ex) { + LOG.warnf(ex, "Error listing %s", dataDirectory); + return Stream.empty(); + } + Stream res = paths.stream() + .filter(FileCrudOperations::canParseFile) + .map(this::parse) + .filter(Objects::nonNull); + + MapModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(createCriteriaBuilder()); + + Predicate keyFilter = mcb.getKeyFilter(); + Predicate entityFilter; + + if (isExpirableEntity) { + entityFilter = mcb.getEntityFilter().and(ExpirationUtils::isNotExpired); + } else { + entityFilter = mcb.getEntityFilter(); + } + + res = res.filter(e -> keyFilter.test(e.getId()) && entityFilter.test(e)); + + if (!queryParameters.getOrderBy().isEmpty()) { + res = res.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream())); + } + + return paginatedStream(res, queryParameters.getOffset(), queryParameters.getLimit()); + } + + @Override + public V update(V value) { + String escapedId = value.getId(); + + Path sp = getPathForEscapedId(escapedId); + if (sp == null) { + throw new IllegalArgumentException("Invalid path: " + escapedId); + } + + checkIsSafeToModify(sp); + + // TODO: improve locking + synchronized (FileMapStorageProviderFactory.class) { + writeYamlContents(sp, value); + } + + return value; + } + + @Override + public boolean delete(String key) { + return Optional.ofNullable(key) + .map(this::getPathForEscapedId) + .map(this::removeIfExists) + .orElse(false); + } + + @Override + public long delete(QueryParameters queryParameters) { + return read(queryParameters).map(AbstractEntity::getId).map(this::delete).filter(a -> a).count(); + } + + @Override + public long getCount(QueryParameters queryParameters) { + return read(queryParameters).count(); + } + + @Override + public String getRealmId() { + return defaultRealmId; + } + + @Override + public void setRealmId(String realmId) { + this.defaultRealmId = realmId; + } + + private Path getDataDirectory() { + return dataDirectoryFunc.apply(defaultRealmId == null ? null : escapeId(defaultRealmId)); + } + + private void writeYamlContents(Path sp, V value) { + Path tempSp = sp.resolveSibling("." + getTxId() + "-" + sp.getFileName()); + try (PathWriter w = new PathWriter(tempSp)) { + final Emitter emitter = new Emitter(DUMP_SETTINGS, w); + try (YamlWritingMechanism mech = new YamlWritingMechanism(emitter::emit)) { + new MapEntityContext<>(entityClass).writeValue(value, mech); + } + registerRenameOnCommit(tempSp, sp); + } catch (IOException ex) { + throw new IllegalStateException("Cannot write " + sp, ex); + } + } + + protected abstract void touch(Path sp) throws IOException; + + protected abstract boolean removeIfExists(Path sp); + + protected abstract void registerRenameOnCommit(Path tempSp, Path sp); + + protected abstract String getTxId(); + + /** + * Hook to obtain the last modified time of the file identified by the supplied {@link Path}. + * + * @param path the {@link Path} to the file whose last modified time it to be obtained. + * @return the {@link FileTime} corresponding to the file's last modified time. + */ + protected abstract FileTime getLastModifiedTime(final Path path); + + /** + * Hook to validate that it is safe to modify the file identified by the supplied {@link Path}. The primary + * goal is to identify if other transactions have modified the file after it was read by the current transaction, + * preventing updates to a stale entity. + * + * @param path the {@link Path} to the file that is to be modified. + */ + protected abstract void checkIsSafeToModify(final Path path); +} diff --git a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapKeycloakTransaction.java b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapKeycloakTransaction.java deleted file mode 100644 index cd067d54a4..0000000000 --- a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapKeycloakTransaction.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2023 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.file; - - -import org.jboss.logging.Logger; -import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.DeepCloner; -import org.keycloak.models.map.common.StringKeyConverter; -import org.keycloak.models.map.common.StringKeyConverter.StringKey; -import org.keycloak.models.map.common.UpdatableEntity; -import org.keycloak.models.map.common.delegate.EntityFieldDelegate; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.ModelEntityUtil; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; -import org.keycloak.models.map.storage.chm.MapFieldPredicates; -import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; -import org.keycloak.storage.ReadOnlyException; -import org.keycloak.storage.SearchableModelField; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileTime; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; - -/** - * {@link MapKeycloakTransaction} implementation used with the file map storage. - * - * @author Stefan Guilhen - */ -public class FileMapKeycloakTransaction - extends ConcurrentHashMapKeycloakTransaction { - - private static final Logger LOG = Logger.getLogger(FileMapKeycloakTransaction.class); - - private final List createdPaths = new LinkedList<>(); - private final List pathsToDelete = new LinkedList<>(); - private final Map renameOnCommit = new HashMap<>(); - private final Map lastModified = new HashMap<>(); - - private final String txId = StringKey.INSTANCE.yieldNewUniqueKey(); - - public static FileMapKeycloakTransaction newInstance(Class entityClass, - Function dataDirectoryFunc, Function suggestedPath, - boolean isExpirableEntity, Map, UpdatePredicatesFunc> fieldPredicates) { - Crud crud = new Crud<>(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity, fieldPredicates); - FileMapKeycloakTransaction tx = new FileMapKeycloakTransaction<>(entityClass, crud); - crud.tx = tx; - return tx; - } - - private FileMapKeycloakTransaction(Class entityClass, Crud crud) { - super( - crud, - StringKeyConverter.StringKey.INSTANCE, - DeepCloner.DUMB_CLONER, - MapFieldPredicates.getPredicates(ModelEntityUtil.getModelType(entityClass)), - ModelEntityUtil.getRealmIdField(entityClass) - ); - } - - @Override - public void rollback() { - // remove all temporary and empty files that were created. - this.renameOnCommit.keySet().forEach(FileMapKeycloakTransaction::silentDelete); - this.createdPaths.forEach(FileMapKeycloakTransaction::silentDelete); - super.rollback(); - } - - @Override - public void commit() { - super.commit(); - // check it is still safe to update/delete before moving the temp files into the actual files or deleting them. - Set allChangedPaths = new HashSet<>(); - allChangedPaths.addAll(this.renameOnCommit.values()); - allChangedPaths.addAll(this.pathsToDelete); - allChangedPaths.forEach(this::checkIsSafeToModify); - try { - this.renameOnCommit.forEach(FileMapKeycloakTransaction::move); - this.pathsToDelete.forEach(FileMapKeycloakTransaction::silentDelete); - // TODO: catch exception thrown by move and try to restore any previously completed moves. - } finally { - // ensure all temp files are removed. - this.renameOnCommit.keySet().forEach(FileMapKeycloakTransaction::silentDelete); - // remove any created files that may have been left empty. - this.createdPaths.forEach(path -> silenteDelete(path, true)); - } - } - - private static void move(Path from, Path to) { - try { - Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - private static void silentDelete(Path p) { - silenteDelete(p, false); - } - - private static void silenteDelete(final Path path, final boolean checkEmpty) { - try { - if (Files.exists(path)) { - if (!checkEmpty || Files.size(path) == 0) { - Files.delete(path); - } - } - } catch(IOException e) { - // swallow the exception. - } - } - - public void touch(Path path) throws IOException { - Files.createFile(path); - createdPaths.add(path); - } - - public boolean removeIfExists(Path path) { - final boolean res = ! pathsToDelete.contains(path) && Files.exists(path); - pathsToDelete.add(path); - return res; - } - - void registerRenameOnCommit(Path from, Path to) { - this.renameOnCommit.put(from, to); - } - - /** - * Obtains and stores the last modified time of the file identified by the supplied {@link Path}. This value is used - * to determine if the file was changed by another transaction after it was read by this transaction. - * - * @param path the {@link Path} to the file. - */ - FileTime getLastModifiedTime(final Path path) { - try { - BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); - FileTime lastModifiedTime = attr.lastModifiedTime(); - this.lastModified.put(path, lastModifiedTime); - return lastModifiedTime; - } catch (IOException ex) { - throw new IllegalStateException("Could not read file attributes " + path, ex); - } - } - - /** - * Checks if it is safe to modify the file identified by the supplied {@link Path}. In particular, this method - * verifies if the file was changed (removed, updated) after it was read by this transaction. Being it the case, this - * transaction should refrain from performing further updates as it must assume its data has become stale. - * - * @param path the {@link Path} to the file that will be updated. - * @throws IllegalStateException if the file was altered by another transaction. - */ - void checkIsSafeToModify(final Path path) { - try { - // path wasn't previously loaded - log a message and return. - if (this.lastModified.get(path) == null) { - LOG.debugf("File %s was not previously loaded, skipping validation prior to writing", path); - return; - } - // check if the original file was deleted by another transaction. - if (!Files.exists(path)) { - throw new IllegalStateException("File " + path + " was removed by another transaction"); - } - // check if the original file was modified by another transaction. - BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); - long lastModifiedTime = attr.lastModifiedTime().toMillis(); - if (this.lastModified.get(path).toMillis() < lastModifiedTime) { - throw new IllegalStateException("File " + path + " was changed by another transaction"); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - - @Override - public V registerEntityForChanges(V origEntity) { - final V watchedValue = super.registerEntityForChanges(origEntity); - return DeepCloner.DUMB_CLONER.entityFieldDelegate(watchedValue, new IdProtector(watchedValue)); - } - - private static class Crud extends FileMapStorage.Crud { - - private FileMapKeycloakTransaction tx; - - public Crud(Class entityClass, Function dataDirectoryFunc, Function suggestedPath, boolean isExpirableEntity, Map, UpdatePredicatesFunc> fieldPredicates) { - super(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity, fieldPredicates); - } - - @Override - protected void touch(Path sp) throws IOException { - tx.touch(sp); - } - - @Override - protected void registerRenameOnCommit(Path from, Path to) { - tx.registerRenameOnCommit(from, to); - } - - @Override - protected boolean removeIfExists(Path sp) { - return tx.removeIfExists(sp); - } - - @Override - protected String getTxId() { - return tx.txId; - } - - @Override - protected FileTime getLastModifiedTime(final Path sp) { - return tx.getLastModifiedTime(sp); - } - - @Override - protected void checkIsSafeToModify(final Path sp) { - tx.checkIsSafeToModify(sp); - } - } - - private class IdProtector extends EntityFieldDelegate.WithEntity { - - public IdProtector(V entity) { - super(entity); - } - - @Override - public > & org.keycloak.models.map.common.EntityField> void set(EF field, T value) { - String id = entity.getId(); - super.set(field, value); - if (! Objects.equals(id, map.determineKeyFromValue(entity, false))) { - throw new ReadOnlyException("Cannot change " + field + " as that would change primary key"); - } - } - - @Override - public String toString() { - return super.toString() + " [protected ID]"; - } - } -} diff --git a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorage.java b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorage.java index 1d2ac1691b..6860604954 100644 --- a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorage.java +++ b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorage.java @@ -16,438 +16,248 @@ */ package org.keycloak.models.map.storage.file; -import org.keycloak.common.util.StackUtil; -import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelDuplicateException; +import org.jboss.logging.Logger; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.ExpirableEntity; -import org.keycloak.models.map.common.ExpirationUtils; -import org.keycloak.models.map.common.HasRealmId; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.common.StringKeyConverter.StringKey; -import org.keycloak.models.map.realm.MapRealmEntity; import org.keycloak.models.map.common.UpdatableEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.common.delegate.EntityFieldDelegate; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelEntityUtil; -import org.keycloak.models.map.storage.QueryParameters; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapCrudOperations; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage; import org.keycloak.models.map.storage.chm.MapFieldPredicates; -import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; -import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; -import org.keycloak.models.map.storage.file.yaml.YamlParser; -import org.keycloak.models.map.storage.file.common.MapEntityContext; -import org.keycloak.models.map.storage.file.yaml.PathWriter; -import org.keycloak.models.map.storage.file.yaml.YamlWritingMechanism; -import org.keycloak.storage.SearchableModelField; +import org.keycloak.storage.ReadOnlyException; import java.io.IOException; import java.io.UncheckedIOException; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; -import java.util.Arrays; -import java.util.IdentityHashMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; +import java.util.Set; import java.util.function.Function; -import java.util.function.Predicate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.jboss.logging.Logger; -import org.snakeyaml.engine.v2.api.DumpSettings; -import org.snakeyaml.engine.v2.emitter.Emitter; -import static org.keycloak.utils.StreamsUtil.paginatedStream; /** - * A file-based {@link MapStorage}. + * {@link MapStorage} implementation used with the file map storage. * * @author Stefan Guilhen */ -public class FileMapStorage implements MapStorage { +public class FileMapStorage + extends ConcurrentHashMapStorage { private static final Logger LOG = Logger.getLogger(FileMapStorage.class); - // any REALM_ID field would do, they share the same name - private static final String SEARCHABLE_FIELD_REALM_ID_FIELD_NAME = ClientModel.SearchableFields.REALM_ID.getName(); - private static final String FILE_SUFFIX = ".yaml"; + private final List createdPaths = new LinkedList<>(); + private final List pathsToDelete = new LinkedList<>(); + private final Map renameOnCommit = new HashMap<>(); + private final Map lastModified = new HashMap<>(); - private final static DumpSettings DUMP_SETTINGS = DumpSettings.builder() - .setIndent(4) - .setIndicatorIndent(2) - .setIndentWithIndicator(false) - .build(); + private final String txId = StringKey.INSTANCE.yieldNewUniqueKey(); - private final Class entityClass; - private final Function dataDirectoryFunc; - private final Function suggestedPath; - private final boolean isExpirableEntity; - private final Map, UpdatePredicatesFunc> fieldPredicates; + public static FileMapStorage newInstance(Class entityClass, + Function dataDirectoryFunc, Function suggestedPath, + boolean isExpirableEntity) { + Crud crud = new Crud<>(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity); + FileMapStorage store = new FileMapStorage<>(entityClass, crud); + crud.store = store; + return store; + } - // TODO: Add auxiliary directory for indices, locks etc. - // private final String auxiliaryFilesDirectory; - - public FileMapStorage(Class entityClass, Function uniqueHumanReadableField, Function dataDirectoryFunc) { - this.entityClass = entityClass; - this.fieldPredicates = new IdentityHashMap<>(MapFieldPredicates.getPredicates(ModelEntityUtil.getModelType(entityClass))); - this.fieldPredicates.keySet().stream() // Ignore realmId since this is treated in reading differently - .filter(f -> Objects.equals(SEARCHABLE_FIELD_REALM_ID_FIELD_NAME, f.getName())) - .findAny() - .ifPresent(key -> this.fieldPredicates.replace(key, (builder, op, params) -> builder)); - this.dataDirectoryFunc = dataDirectoryFunc; - this.suggestedPath = uniqueHumanReadableField == null ? v -> v.getId() == null ? null : new String[] { v.getId() } : uniqueHumanReadableField; - this.isExpirableEntity = ExpirableEntity.class.isAssignableFrom(entityClass); + private FileMapStorage(Class entityClass, Crud crud) { + super( + crud, + StringKeyConverter.StringKey.INSTANCE, + DeepCloner.DUMB_CLONER, + MapFieldPredicates.getPredicates(ModelEntityUtil.getModelType(entityClass)), + ModelEntityUtil.getRealmIdField(entityClass) + ); } @Override - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - @SuppressWarnings("unchecked") - MapKeycloakTransaction sessionTransaction = session.getAttribute("file-map-transaction-" + hashCode(), MapKeycloakTransaction.class); - - if (sessionTransaction == null) { - sessionTransaction = createTransactionInternal(session); - session.setAttribute("file-map-transaction-" + hashCode(), sessionTransaction); - } - return sessionTransaction; + public void rollback() { + // remove all temporary and empty files that were created. + this.renameOnCommit.keySet().forEach(FileMapStorage::silentDelete); + this.createdPaths.forEach(FileMapStorage::silentDelete); + super.rollback(); } - public FileMapKeycloakTransaction createTransactionInternal(KeycloakSession session) { - return FileMapKeycloakTransaction.newInstance(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity, fieldPredicates); - } - - private static boolean canParseFile(Path p) { - final String fn = p.getFileName().toString(); + @Override + public void commit() { + super.commit(); + // check it is still safe to update/delete before moving the temp files into the actual files or deleting them. + Set allChangedPaths = new HashSet<>(); + allChangedPaths.addAll(this.renameOnCommit.values()); + allChangedPaths.addAll(this.pathsToDelete); + allChangedPaths.forEach(this::checkIsSafeToModify); try { - return Files.isRegularFile(p) - && Files.size(p) > 0L - && ! fn.startsWith(".") - && fn.endsWith(FILE_SUFFIX) - && Files.isReadable(p); + this.renameOnCommit.forEach(FileMapStorage::move); + this.pathsToDelete.forEach(FileMapStorage::silentDelete); + // TODO: catch exception thrown by move and try to restore any previously completed moves. + } finally { + // ensure all temp files are removed. + this.renameOnCommit.keySet().forEach(FileMapStorage::silentDelete); + // remove any created files that may have been left empty. + this.createdPaths.forEach(path -> silenteDelete(path, true)); + } + } + + private static void move(Path from, Path to) { + try { + Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); } catch (IOException ex) { - return false; + throw new UncheckedIOException(ex); } } - public static abstract class Crud implements ConcurrentHashMapCrudOperations, HasRealmId { - - private String defaultRealmId; - private final Class entityClass; - private final Function dataDirectoryFunc; - private final Function suggestedPath; - private final boolean isExpirableEntity; - private final Map, UpdatePredicatesFunc> fieldPredicates; - - public Crud(Class entityClass, Function dataDirectoryFunc, Function suggestedPath, boolean isExpirableEntity, Map, UpdatePredicatesFunc> fieldPredicates) { - this.entityClass = entityClass; - this.dataDirectoryFunc = dataDirectoryFunc; - this.suggestedPath = suggestedPath; - this.isExpirableEntity = isExpirableEntity; - - this.fieldPredicates = new IdentityHashMap<>(fieldPredicates); - this.fieldPredicates.keySet().stream() // Ignore realmId since this is treated in reading differently - .filter(f -> Objects.equals(SEARCHABLE_FIELD_REALM_ID_FIELD_NAME, f.getName())) - .findAny() - .ifPresent(key -> this.fieldPredicates.replace(key, (builder, op, params) -> builder)); - } - - protected Path getPathForEscapedId(String[] escapedIdPathArray) { - Path parentDirectory = getDataDirectory(); - Path targetPath = parentDirectory; - for (String path : escapedIdPathArray) { - targetPath = targetPath.resolve(path).normalize(); - if (! targetPath.getParent().equals(parentDirectory)) { - LOG.warnf("Path traversal detected: %s", Arrays.toString(escapedIdPathArray)); - return null; - } - parentDirectory = targetPath; - } - - return targetPath.resolveSibling(targetPath.getFileName() + FILE_SUFFIX); - } - - protected Path getPathForEscapedId(String escapedId) { - if (escapedId == null) { - throw new IllegalStateException("Invalid ID to escape"); - } - - String[] escapedIdArray = ID_COMPONENT_SEPARATOR_PATTERN.split(escapedId); - return getPathForEscapedId(escapedIdArray); - } - - // Percent sign + Unix (/) and https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file reserved characters - private static final Pattern RESERVED_CHARACTERS = Pattern.compile("[%<:>\"/\\\\|?*=]"); - private static final String ID_COMPONENT_SEPARATOR = ":"; - private static final String ESCAPING_CHARACTER = "="; - private static final Pattern ID_COMPONENT_SEPARATOR_PATTERN = Pattern.compile(Pattern.quote(ID_COMPONENT_SEPARATOR) + "+"); - - private static String[] escapeId(String[] idArray) { - if (idArray == null || idArray.length == 0 || idArray.length == 1 && idArray[0] == null) { - return null; - } - return Stream.of(idArray) - .map(Crud::escapeId) - .toArray(String[]::new); - } - - private static String escapeId(String id) { - Objects.requireNonNull(id, "ID must be non-null"); - - StringBuilder idEscaped = new StringBuilder(); - Matcher m = RESERVED_CHARACTERS.matcher(id); - while (m.find()) { - m.appendReplacement(idEscaped, String.format(ESCAPING_CHARACTER + "%02x", (int) m.group().charAt(0))); - } - m.appendTail(idEscaped); - final Path pId = Path.of(idEscaped.toString()); - - return pId.toString(); - } - - protected V parse(Path fileName) { - getLastModifiedTime(fileName); - final V parsedObject = YamlParser.parse(fileName, new MapEntityContext<>(entityClass)); - if (parsedObject == null) { - LOG.debugf("Could not parse %s%s", fileName, StackUtil.getShortStackTrace()); - return null; - } - - String escapedId = determineKeyFromValue(parsedObject, false); - final String fileNameStr = fileName.getFileName().toString(); - final String idFromFilename = fileNameStr.substring(0, fileNameStr.length() - FILE_SUFFIX.length()); - if (escapedId == null) { - LOG.debugf("Determined ID from filename: %s%s", idFromFilename); - escapedId = idFromFilename; - } else if (! escapedId.endsWith(idFromFilename)) { - LOG.warnf("Id \"%s\" does not conform with filename \"%s\", expected: %s", escapedId, fileNameStr, escapeId(escapedId)); - } - - parsedObject.setId(escapedId); - parsedObject.clearUpdatedFlag(); - - return parsedObject; - } - - @Override - public V create(V value) { - // TODO: Lock realm directory for changes (e.g. on realm deletion) - String escapedId = value.getId(); - - writeYamlContents(getPathForEscapedId(escapedId), value); - - return value; - } - - /** - * Returns escaped ID - relative file name in the file system with path separator {@link #ID_COMPONENT_SEPARATOR}. - * @param value Object - * @param forCreate Whether this is for create operation ({@code true}) or - * @return - */ - @Override - public String determineKeyFromValue(V value, boolean forCreate) { - final boolean randomId; - String[] proposedId = suggestedPath.apply(value); - - if (! forCreate) { - String[] escapedProposedId = escapeId(proposedId); - final String res = proposedId == null ? null : String.join(ID_COMPONENT_SEPARATOR, escapedProposedId); - if (LOG.isDebugEnabled()) { - LOG.debugf("determineKeyFromValue: got %s (%s) for %s", res, res == null ? null : String.join(" [/] ", proposedId), value); - } - return res; - } - - if (proposedId == null || proposedId.length == 0) { - randomId = value.getId() == null; - proposedId = new String[] { value.getId() == null ? StringKey.INSTANCE.yieldNewUniqueKey() : value.getId() }; - } else { - randomId = false; - } - - String[] escapedProposedId = escapeId(proposedId); - Path sp = getPathForEscapedId(escapedProposedId); // sp will never be null - - final Path parentDir = sp.getParent(); - if (! Files.isDirectory(parentDir)) { - try { - Files.createDirectories(parentDir); - } catch (IOException ex) { - throw new IllegalStateException("Directory does not exist and cannot be created: " + parentDir, ex); - } - } - - for (int counter = 0; counter < 100; counter++) { - LOG.tracef("Attempting to create file %s", sp, StackUtil.getShortStackTrace()); - try { - touch(sp); - final String res = String.join(ID_COMPONENT_SEPARATOR, escapedProposedId); - LOG.debugf("determineKeyFromValue: got %s for created %s", res, value); - return res; - } catch (FileAlreadyExistsException ex) { - if (! randomId) { - throw new ModelDuplicateException("File " + sp + " already exists!"); - } - final String lastComponent = StringKey.INSTANCE.yieldNewUniqueKey(); - escapedProposedId[escapedProposedId.length - 1] = lastComponent; - sp = getPathForEscapedId(escapedProposedId); - } catch (IOException ex) { - throw new IllegalStateException("Could not create file " + sp, ex); - } - } - - return null; - } - - @Override - public V read(String key) { - return Optional.ofNullable(key) - .map(this::getPathForEscapedId) - .filter(Files::isReadable) - .map(this::parse) - .orElse(null); - } - - public MapModelCriteriaBuilder createCriteriaBuilder() { - return new MapModelCriteriaBuilder<>(StringKey.INSTANCE, fieldPredicates); - } - - @Override - public Stream read(QueryParameters queryParameters) { - final List paths; - FileCriteriaBuilder cb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(FileCriteriaBuilder.criteria()); - String realmId = (String) cb.getSingleRestrictionArgument(SEARCHABLE_FIELD_REALM_ID_FIELD_NAME); - setRealmId(realmId); - - final Path dataDirectory = getDataDirectory(); - if (! Files.isDirectory(dataDirectory)) { - return Stream.empty(); - } - - // We cannot use Files.find since it throws an UncheckedIOException if it lists a file which is removed concurrently - // before its BasicAttributes can be retrieved for its BiPredicate parameter - try (Stream dirStream = Files.walk(dataDirectory, entityClass == MapRealmEntity.class ? 1 : 2)) { - // The paths list has to be materialized first, otherwise "dirStream" would be closed - // before the resulting stream would be read and would return empty result - paths = dirStream.collect(Collectors.toList()); - } catch (IOException | UncheckedIOException ex) { - LOG.warnf(ex, "Error listing %s", dataDirectory); - return Stream.empty(); - } - Stream res = paths.stream() - .filter(FileMapStorage::canParseFile) - .map(this::parse) - .filter(Objects::nonNull); - - MapModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(createCriteriaBuilder()); - - Predicate keyFilter = mcb.getKeyFilter(); - Predicate entityFilter; - - if (isExpirableEntity) { - entityFilter = mcb.getEntityFilter().and(ExpirationUtils::isNotExpired); - } else { - entityFilter = mcb.getEntityFilter(); - } - - res = res.filter(e -> keyFilter.test(e.getId()) && entityFilter.test(e)); - - if (! queryParameters.getOrderBy().isEmpty()) { - res = res.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream())); - } - - return paginatedStream(res, queryParameters.getOffset(), queryParameters.getLimit()); - } - - @Override - public V update(V value) { - String escapedId = value.getId(); - - Path sp = getPathForEscapedId(escapedId); - if (sp == null) { - throw new IllegalArgumentException("Invalid path: " + escapedId); - } - checkIsSafeToModify(sp); - // TODO: improve locking - synchronized (FileMapStorageProviderFactory.class) { - writeYamlContents(sp, value); - } - - return value; - } - - @Override - public boolean delete(String key) { - return Optional.ofNullable(key) - .map(this::getPathForEscapedId) - .map(this::removeIfExists) - .orElse(false); - } - - @Override - public long delete(QueryParameters queryParameters) { - return read(queryParameters).map(AbstractEntity::getId).map(this::delete).filter(a -> a).count(); - } - - @Override - public long getCount(QueryParameters queryParameters) { - return read(queryParameters).count(); - } - - @Override - public String getRealmId() { - return defaultRealmId; - } - - @Override - public void setRealmId(String realmId) { - this.defaultRealmId = realmId; - } - - private Path getDataDirectory() { - return dataDirectoryFunc.apply(defaultRealmId == null ? null : escapeId(defaultRealmId)); - } - - private void writeYamlContents(Path sp, V value) { - Path tempSp = sp.resolveSibling("." + getTxId() + "-" + sp.getFileName()); - try (PathWriter w = new PathWriter(tempSp)) { - final Emitter emitter = new Emitter(DUMP_SETTINGS, w); - try (YamlWritingMechanism mech = new YamlWritingMechanism(emitter::emit)) { - new MapEntityContext<>(entityClass).writeValue(value, mech); - } - registerRenameOnCommit(tempSp, sp); - } catch (IOException ex) { - throw new IllegalStateException("Cannot write " + sp, ex); - } - } - - protected abstract void touch(Path sp) throws IOException; - - protected abstract boolean removeIfExists(Path sp); - - protected abstract void registerRenameOnCommit(Path tempSp, Path sp); - - protected abstract String getTxId(); - - /** - * Hook to obtain the last modified time of the file identified by the supplied {@link Path}. - * - * @param path the {@link Path} to the file whose last modified time it to be obtained. - * @return the {@link FileTime} corresponding to the file's last modified time. - */ - protected abstract FileTime getLastModifiedTime(final Path path); - - /** - * Hook to validate that it is safe to modify the file identified by the supplied {@link Path}. The primary - * goal is to identify if other transactions have modified the file after it was read by the current transaction, - * preventing updates to a stale entity. - * - * @param path the {@link Path} to the file that is to be modified. - */ - protected abstract void checkIsSafeToModify(final Path path); + private static void silentDelete(Path p) { + silenteDelete(p, false); } + private static void silenteDelete(final Path path, final boolean checkEmpty) { + try { + if (Files.exists(path)) { + if (!checkEmpty || Files.size(path) == 0) { + Files.delete(path); + } + } + } catch(IOException e) { + // swallow the exception. + } + } + + public void touch(Path path) throws IOException { + Files.createFile(path); + createdPaths.add(path); + } + + public boolean removeIfExists(Path path) { + final boolean res = ! pathsToDelete.contains(path) && Files.exists(path); + pathsToDelete.add(path); + return res; + } + + void registerRenameOnCommit(Path from, Path to) { + this.renameOnCommit.put(from, to); + } + + /** + * Obtains and stores the last modified time of the file identified by the supplied {@link Path}. This value is used + * to determine if the file was changed by another transaction after it was read by this transaction. + * + * @param path the {@link Path} to the file. + */ + FileTime getLastModifiedTime(final Path path) { + try { + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); + FileTime lastModifiedTime = attr.lastModifiedTime(); + this.lastModified.put(path, lastModifiedTime); + return lastModifiedTime; + } catch (IOException ex) { + throw new IllegalStateException("Could not read file attributes " + path, ex); + } + } + + /** + * Checks if it is safe to modify the file identified by the supplied {@link Path}. In particular, this method + * verifies if the file was changed (removed, updated) after it was read by this transaction. Being it the case, this + * transaction should refrain from performing further updates as it must assume its data has become stale. + * + * @param path the {@link Path} to the file that will be updated. + * @throws IllegalStateException if the file was altered by another transaction. + */ + void checkIsSafeToModify(final Path path) { + try { + // path wasn't previously loaded - log a message and return. + if (this.lastModified.get(path) == null) { + LOG.debugf("File %s was not previously loaded, skipping validation prior to writing", path); + return; + } + // check if the original file was deleted by another transaction. + if (!Files.exists(path)) { + throw new IllegalStateException("File " + path + " was removed by another transaction"); + } + // check if the original file was modified by another transaction. + BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); + long lastModifiedTime = attr.lastModifiedTime().toMillis(); + if (this.lastModified.get(path).toMillis() < lastModifiedTime) { + throw new IllegalStateException("File " + path + " was changed by another transaction"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + @Override + public V registerEntityForChanges(V origEntity) { + final V watchedValue = super.registerEntityForChanges(origEntity); + return DeepCloner.DUMB_CLONER.entityFieldDelegate(watchedValue, new IdProtector(watchedValue)); + } + + private static class Crud extends FileCrudOperations { + + private FileMapStorage store; + + public Crud(Class entityClass, Function dataDirectoryFunc, Function suggestedPath, boolean isExpirableEntity) { + super(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity); + } + + @Override + protected void touch(Path sp) throws IOException { + store.touch(sp); + } + + @Override + protected void registerRenameOnCommit(Path from, Path to) { + store.registerRenameOnCommit(from, to); + } + + @Override + protected boolean removeIfExists(Path sp) { + return store.removeIfExists(sp); + } + + @Override + protected String getTxId() { + return store.txId; + } + + @Override + protected FileTime getLastModifiedTime(final Path sp) { + return store.getLastModifiedTime(sp); + } + + @Override + protected void checkIsSafeToModify(final Path sp) { + store.checkIsSafeToModify(sp); + } + } + + private class IdProtector extends EntityFieldDelegate.WithEntity { + + public IdProtector(V entity) { + super(entity); + } + + @Override + public > & org.keycloak.models.map.common.EntityField> void set(EF field, T value) { + String id = entity.getId(); + super.set(field, value); + if (! Objects.equals(id, map.determineKeyFromValue(entity, false))) { + throw new ReadOnlyException("Cannot change " + field + " as that would change primary key"); + } + } + + @Override + public String toString() { + return super.toString() + " [protected ID]"; + } + } } diff --git a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProvider.java b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProvider.java index 2eaf6f86b4..3462cc6d03 100644 --- a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProvider.java +++ b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProvider.java @@ -16,10 +16,21 @@ */ package org.keycloak.models.map.storage.file; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.ExpirableEntity; +import org.keycloak.models.map.common.SessionAttributesUtils; +import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.ModelEntityUtil; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage; + +import java.util.function.Function; + +import static org.keycloak.models.map.storage.ModelEntityUtil.getModelName; +import static org.keycloak.models.map.storage.file.FileMapStorageProviderFactory.UNIQUE_HUMAN_READABLE_NAME_FIELD; /** * File-based {@link MapStorageProvider} implementation. @@ -28,17 +39,33 @@ import org.keycloak.models.map.storage.MapStorageProviderFactory; */ public class FileMapStorageProvider implements MapStorageProvider { + private final KeycloakSession session; private final FileMapStorageProviderFactory factory; + private final int factoryId; - public FileMapStorageProvider(FileMapStorageProviderFactory factory) { + public FileMapStorageProvider(KeycloakSession session, FileMapStorageProviderFactory factory, int factoryId) { + this.session = session; this.factory = factory; + this.factoryId = factoryId; } @Override @SuppressWarnings("unchecked") - public MapStorage getStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { - FileMapStorage storage = factory.getStorage(modelType, flags); - return (MapStorage) storage; + public MapStorage getMapStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { + return (MapStorage) SessionAttributesUtils.createMapStorageIfAbsent(session, getClass(), modelType, factoryId, () -> createFileMapStorage(modelType)); + } + + private ConcurrentHashMapStorage createFileMapStorage(Class modelType) { + String areaName = getModelName(modelType, modelType.getSimpleName()); + final Class et = ModelEntityUtil.getEntityType(modelType); + Function uniqueHumanReadableField = (Function) UNIQUE_HUMAN_READABLE_NAME_FIELD.get(et); + + ConcurrentHashMapStorage mapStorage = FileMapStorage.newInstance(et, + factory.getDataDirectoryFunc(areaName), + ((uniqueHumanReadableField == null) ? v -> v.getId() == null ? null : new String[]{v.getId()} : uniqueHumanReadableField), + ExpirableEntity.class.isAssignableFrom(et)); + session.getTransactionManager().enlist(mapStorage); + return mapStorage; } @Override diff --git a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProviderFactory.java b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProviderFactory.java index 86cf263d66..06e40e4ba5 100644 --- a/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProviderFactory.java +++ b/model/map-file/src/main/java/org/keycloak/models/map/storage/file/FileMapStorageProviderFactory.java @@ -22,7 +22,6 @@ import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; -import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.MapPolicyEntity; import org.keycloak.models.map.authorization.entity.MapResourceEntity; @@ -30,20 +29,17 @@ import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; import org.keycloak.models.map.authorization.entity.MapScopeEntity; import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.clientscope.MapClientScopeEntity; -import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.common.SessionAttributesUtils; import org.keycloak.models.map.group.MapGroupEntity; import org.keycloak.models.map.realm.MapRealmEntity; import org.keycloak.models.map.role.MapRoleEntity; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; -import org.keycloak.models.map.storage.ModelEntityUtil; import org.keycloak.models.map.user.MapUserEntity; import org.keycloak.provider.EnvironmentDependentProviderFactory; + import java.io.File; -import java.nio.file.FileSystem; import java.nio.file.Path; -import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -65,9 +61,9 @@ public class FileMapStorageProviderFactory implements AmphibianProviderFactory> rootAreaDirectories = new HashMap<>(); // Function: (realmId) -> path - private final Map, FileMapStorage> storages = new HashMap<>(); + private final int factoryId = SessionAttributesUtils.grabNewFactoryIdentifier(); - private static final Map, Function> UNIQUE_HUMAN_READABLE_NAME_FIELD = Map.ofEntries( + protected static final Map, Function> UNIQUE_HUMAN_READABLE_NAME_FIELD = Map.ofEntries( entry(MapClientEntity.class, ((Function) v -> new String[] { v.getClientId() })), entry(MapClientScopeEntity.class, ((Function) v -> new String[] { v.getName() })), entry(MapGroupEntity.class, ((Function) v -> v.getParentId() == null @@ -89,7 +85,7 @@ public class FileMapStorageProviderFactory implements AmphibianProviderFactory new FileMapStorageProvider(session1, this, factoryId)); } @Override @@ -151,22 +147,8 @@ public class FileMapStorageProviderFactory implements AmphibianProviderFactory FileMapStorage initFileStorage(Class modelType) { - String name = getModelName(modelType, modelType.getSimpleName()); - final Class et = ModelEntityUtil.getEntityType(modelType); - @SuppressWarnings("unchecked") - FileMapStorage res = new FileMapStorage<>(et, (Function) UNIQUE_HUMAN_READABLE_NAME_FIELD.get(et), rootAreaDirectories.get(name)); - return res; + public Function getDataDirectoryFunc(String areaName) { + return rootAreaDirectories.get(areaName); } - FileMapStorage getStorage(Class modelType, Flag[] flags) { - try { - if (modelType == SingleUseObjectValueModel.class) { - throw new IllegalArgumentException("Unsupported file storage: " + ModelEntityUtil.getModelName(modelType)); - } - return storages.computeIfAbsent(modelType, n -> initFileStorage(modelType)); - } catch (ConcurrentModificationException ex) { - return storages.get(modelType); - } - } } 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/HotRodCrudOperations.java similarity index 87% rename from model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodCrudOperations.java index d2a7cf1929..8d0fa2b12b 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/HotRodCrudOperations.java @@ -32,12 +32,9 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.ExpirableEntity; import org.keycloak.models.map.common.StringKeyConverter; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelEntityUtil; import org.keycloak.models.map.storage.QueryParameters; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapCrudOperations; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; +import org.keycloak.models.map.storage.CrudOperations; import org.keycloak.models.map.storage.chm.MapFieldPredicates; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -47,8 +44,6 @@ import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider; import org.keycloak.models.map.storage.hotRod.locking.HotRodLocksUtils; -import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; -import org.keycloak.models.map.storage.hotRod.transaction.NoActionHotRodTransactionWrapper; import org.keycloak.storage.SearchableModelField; import org.keycloak.utils.LockObjectsForModification; @@ -68,9 +63,9 @@ import static org.keycloak.common.util.StackUtil.getShortStackTrace; 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 HotRodCrudOperations, M> implements CrudOperations { - private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class); + private static final Logger LOG = Logger.getLogger(HotRodCrudOperations.class); private final KeycloakSession session; private final RemoteCache remoteCache; @@ -79,13 +74,12 @@ public class HotRodMapStorage delegateProducer; protected final DeepCloner cloner; protected boolean isExpirableEntity; - private final AllAreasHotRodTransactionsWrapper txWrapper; private final Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates; private final Long lockTimeout; private final RemoteCache locksCache; private final Map entityVersionCache = new HashMap<>(); - public HotRodMapStorage(KeycloakSession session, RemoteCache remoteCache, StringKeyConverter keyConverter, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner, AllAreasHotRodTransactionsWrapper txWrapper, Long lockTimeout) { + public HotRodCrudOperations(KeycloakSession session, RemoteCache remoteCache, StringKeyConverter keyConverter, HotRodEntityDescriptor storedEntityDescriptor, DeepCloner cloner, Long lockTimeout) { this.session = session; this.remoteCache = remoteCache; this.keyConverter = keyConverter; @@ -93,7 +87,6 @@ public class HotRodMapStorage) storedEntityDescriptor.getModelTypeClass()); this.lockTimeout = lockTimeout; HotRodConnectionProvider cacheProvider = session.getProvider(HotRodConnectionProvider.class); @@ -155,7 +148,7 @@ public class HotRodMapStorage(storedEntityDescriptor.getEntityTypeClass()); } - @Override - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - // Here we return transaction that has no action because the returned transaction is enlisted to different - // phase than we need. Instead of tx returned by this method txWrapper is enlisted and executes all changes - // performed by the returned transaction. - return new NoActionHotRodTransactionWrapper<>((ConcurrentHashMapKeycloakTransaction) txWrapper.getOrCreateTxForModel(storedEntityDescriptor.getModelTypeClass(), () -> createTransactionInternal(session))); - } - - protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { - return new ConcurrentHashMapKeycloakTransaction<>(this, keyConverter, cloner, fieldPredicates); - } - // V must be an instance of ExpirableEntity // returns null if expiration field is not set // in certain cases can return 0 or negative number, which needs to be handled carefully when using as ISPN lifespan 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 cb101fede1..69b16c60d9 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 @@ -18,53 +18,98 @@ package org.keycloak.models.map.storage.hotRod; import org.infinispan.client.hotrod.RemoteCache; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.SingleUseObjectValueModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage; +import org.keycloak.models.map.storage.chm.MapFieldPredicates; +import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; +import org.keycloak.models.map.storage.chm.SingleUseObjectMapStorage; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider; +import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodStoresWrapper; import org.keycloak.models.map.storage.hotRod.transaction.HotRodRemoteTransactionWrapper; -import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; +import org.keycloak.models.map.storage.hotRod.userSession.HotRodUserSessionMapStorage; +import org.keycloak.storage.SearchableModelField; + +import java.util.Map; + +import static org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory.CLIENT_SESSION_PREDICATES; +import static org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory.CLONER; public class HotRodMapStorageProvider implements MapStorageProvider { private final KeycloakSession session; private final HotRodMapStorageProviderFactory factory; - private final String hotRodConfigurationIdentifier; private final boolean jtaEnabled; + private final long lockTimeout; + private AllAreasHotRodStoresWrapper storesWrapper; - public HotRodMapStorageProvider(KeycloakSession session, HotRodMapStorageProviderFactory factory, String hotRodConfigurationIdentifier, boolean jtaEnabled) { + + public HotRodMapStorageProvider(KeycloakSession session, HotRodMapStorageProviderFactory factory, boolean jtaEnabled, long lockTimeout) { this.session = session; this.factory = factory; - this.hotRodConfigurationIdentifier = hotRodConfigurationIdentifier; this.jtaEnabled = jtaEnabled; + this.lockTimeout = lockTimeout; } @Override - public MapStorage getStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { - // Check if HotRod transaction was already initialized for this configuration within this session - AllAreasHotRodTransactionsWrapper txWrapper = session.getAttribute(this.hotRodConfigurationIdentifier, AllAreasHotRodTransactionsWrapper.class); - if (txWrapper == null) { - // If not create new AllAreasHotRodTransactionsWrapper and put it into session, so it is created only once - txWrapper = new AllAreasHotRodTransactionsWrapper(); - session.setAttribute(this.hotRodConfigurationIdentifier, txWrapper); + public MapStorage getMapStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { + if (storesWrapper == null) initializeTransactionWrapper(modelType); - // Enlist the wrapper into prepare phase so it is executed before HotRod client provided transaction - session.getTransactionManager().enlistPrepare(txWrapper); + // We need to preload client session store before we load user session store to avoid recursive update of storages map + if (modelType == UserSessionModel.class) getMapStorage(AuthenticatedClientSessionModel.class, flags); - if (!jtaEnabled) { - // If there is no JTA transaction enabled control HotRod client provided transaction manually using - // HotRodRemoteTransactionWrapper - HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class); - HotRodEntityDescriptor entityDescriptor = factory.getEntityDescriptor(modelType); - RemoteCache remoteCache = connectionProvider.getRemoteCache(entityDescriptor.getCacheName()); - session.getTransactionManager().enlist(new HotRodRemoteTransactionWrapper(remoteCache.getTransactionManager())); - } + return (MapStorage) storesWrapper.getOrCreateStoreForModel(modelType, () -> createHotRodMapStorage(session, modelType, flags)); + } + + private void initializeTransactionWrapper(Class modelType) { + storesWrapper = new AllAreasHotRodStoresWrapper(); + + // Enlist the wrapper into prepare phase so the changes in the wrapper are executed before HotRod client provided transaction + session.getTransactionManager().enlistPrepare(storesWrapper); + + // If JTA is enabled, the HotRod client provided transaction is automatically enlisted into JTA and we don't need to do anything here + if (!jtaEnabled) { + // If there is no JTA transaction enabled control HotRod client provided transaction manually using + // HotRodRemoteTransactionWrapper + HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class); + HotRodEntityDescriptor entityDescriptor = factory.getEntityDescriptor(modelType); + RemoteCache remoteCache = connectionProvider.getRemoteCache(entityDescriptor.getCacheName()); + session.getTransactionManager().enlist(new HotRodRemoteTransactionWrapper(remoteCache.getTransactionManager())); } + } - return (MapStorage) factory.getHotRodStorage(session, modelType, txWrapper, flags); + private & AbstractEntity, M> ConcurrentHashMapStorage createHotRodMapStorage(KeycloakSession session, Class modelType, MapStorageProviderFactory.Flag... flags) { + HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class); + HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) factory.getEntityDescriptor(modelType); + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = MapFieldPredicates.getPredicates((Class) entityDescriptor.getModelTypeClass()); + StringKeyConverter kc = StringKeyConverter.StringKey.INSTANCE; + + // TODO: This is messy, we should refactor this so we don't need to pass kc, entityDescriptor, CLONER to both MapStorage and CrudOperations + if (modelType == SingleUseObjectValueModel.class) { + return new SingleUseObjectMapStorage(new SingleUseObjectHotRodCrudOperations(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), kc, (HotRodEntityDescriptor) entityDescriptor, CLONER, lockTimeout), kc, CLONER, fieldPredicates); + } if (modelType == AuthenticatedClientSessionModel.class) { + return new ConcurrentHashMapStorage(new HotRodCrudOperations(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), + kc, + entityDescriptor, + CLONER, lockTimeout), kc, CLONER, CLIENT_SESSION_PREDICATES); + } if (modelType == UserSessionModel.class) { + return new HotRodUserSessionMapStorage(new HotRodCrudOperations(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), + kc, + entityDescriptor, + CLONER, lockTimeout), kc, CLONER, fieldPredicates, storesWrapper.getOrCreateStoreForModel(AuthenticatedClientSessionModel.class, () -> createHotRodMapStorage(session, AuthenticatedClientSessionModel.class, flags))); + } else { + return new ConcurrentHashMapStorage(new HotRodCrudOperations<>(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), kc, entityDescriptor, CLONER, lockTimeout), kc, CLONER, fieldPredicates); + } } @Override 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 2dfe2770f8..2dd2c7141c 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 @@ -20,21 +20,22 @@ package org.keycloak.models.map.storage.hotRod; import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.component.AmphibianProviderFactory; -import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity; +import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.MapPolicyEntity; import org.keycloak.models.map.authorization.entity.MapResourceEntity; import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; import org.keycloak.models.map.authorization.entity.MapScopeEntity; -import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity; -import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity; +import org.keycloak.models.map.client.MapClientEntity; +import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.clientscope.MapClientScopeEntity; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.StringKeyConverter; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.SessionAttributesUtils; import org.keycloak.models.map.events.MapAdminEventEntity; import org.keycloak.models.map.events.MapAuthEventEntity; import org.keycloak.models.map.group.MapGroupEntity; @@ -53,8 +54,8 @@ import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntity; import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntity; import org.keycloak.models.map.role.MapRoleEntity; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorageProvider; +import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.storage.chm.MapFieldPredicates; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; import org.keycloak.models.map.storage.hotRod.authSession.HotRodAuthenticationSessionEntityDelegate; @@ -64,24 +65,15 @@ import org.keycloak.models.map.storage.hotRod.authorization.HotRodPolicyEntityDe import org.keycloak.models.map.storage.hotRod.authorization.HotRodResourceEntityDelegate; import org.keycloak.models.map.storage.hotRod.authorization.HotRodResourceServerEntityDelegate; import org.keycloak.models.map.storage.hotRod.authorization.HotRodScopeEntityDelegate; -import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; -import org.keycloak.models.map.storage.hotRod.common.AutogeneratedHotRodDescriptors; -import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; -import org.keycloak.models.map.storage.hotRod.events.HotRodAdminEventEntityDelegate; -import org.keycloak.models.map.storage.hotRod.events.HotRodAuthEventEntityDelegate; -import org.keycloak.models.map.storage.hotRod.loginFailure.HotRodUserLoginFailureEntityDelegate; -import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntityDelegate; import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate; import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityDelegate; -import org.keycloak.models.map.client.MapClientEntity; -import org.keycloak.models.map.client.MapProtocolMapperEntity; -import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.storage.hotRod.clientscope.HotRodClientScopeEntityDelegate; +import org.keycloak.models.map.storage.hotRod.common.AutogeneratedHotRodDescriptors; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; -import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider; -import org.keycloak.models.map.storage.MapStorageProvider; -import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.hotRod.events.HotRodAdminEventEntityDelegate; +import org.keycloak.models.map.storage.hotRod.events.HotRodAuthEventEntityDelegate; import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntityDelegate; +import org.keycloak.models.map.storage.hotRod.loginFailure.HotRodUserLoginFailureEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.HotRodRealmEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodAuthenticationExecutionEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodAuthenticationFlowEntityDelegate; @@ -94,9 +86,8 @@ import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodOTPPolicyEntity import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredActionProviderEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredCredentialEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodWebAuthnPolicyEntityDelegate; -import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; +import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntityDelegate; import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate; -import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; 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.HotRodUserEntityDelegate; @@ -104,7 +95,6 @@ import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEn import org.keycloak.models.map.storage.hotRod.userSession.HotRodAuthenticatedClientSessionEntity; import org.keycloak.models.map.storage.hotRod.userSession.HotRodAuthenticatedClientSessionEntityDelegate; import org.keycloak.models.map.storage.hotRod.userSession.HotRodUserSessionEntityDelegate; -import org.keycloak.models.map.storage.hotRod.userSession.HotRodUserSessionTransaction; import org.keycloak.models.map.user.MapUserConsentEntity; import org.keycloak.models.map.user.MapUserCredentialEntity; import org.keycloak.models.map.user.MapUserEntity; @@ -119,22 +109,17 @@ import org.keycloak.transaction.JtaTransactionManagerLookup; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory, MapStorageProviderFactory, EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "hotrod"; - private static final String SESSION_TX_PREFIX = "hotrod-map-tx-"; - private static final AtomicInteger ENUMERATOR = new AtomicInteger(0); - private final String sessionProviderKey; - private final String hotRodConfigurationIdentifier; - + private final int factoryId = SessionAttributesUtils.grabNewFactoryIdentifier(); private boolean jtaEnabled; - private static final Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> clientSessionPredicates = MapFieldPredicates.basePredicates(HotRodAuthenticatedClientSessionEntity.ID); + protected static final Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> CLIENT_SESSION_PREDICATES = MapFieldPredicates.basePredicates(HotRodAuthenticatedClientSessionEntity.ID); private Long lockTimeout; - private final static DeepCloner CLONER = new DeepCloner.Builder() + protected final static DeepCloner CLONER = new DeepCloner.Builder() .constructor(MapRootAuthenticationSessionEntity.class, HotRodRootAuthenticationSessionEntityDelegate::new) .constructor(MapAuthenticationSessionEntity.class, HotRodAuthenticationSessionEntityDelegate::new) @@ -183,74 +168,15 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory .build(); - public HotRodMapStorageProviderFactory() { - int index = ENUMERATOR.getAndIncrement(); - // this identifier is used to create HotRodMapProvider only once per session per factory instance - this.sessionProviderKey = HotRodMapStorageProviderFactory.class.getName() + "-" + PROVIDER_ID + "-" + index; - - // When there are more HotRod configurations available in Keycloak (for example, global/realm1/realm2 etc.) - // there will be more instances of this factory created where each holds one configuration. - // The following identifier can be used to uniquely identify instance of this factory. - // This can be later used, for example, to store provider/transaction instances inside session - // attributes without collisions between several configurations - this.hotRodConfigurationIdentifier = SESSION_TX_PREFIX + index; - } - @Override public MapStorageProvider create(KeycloakSession session) { - HotRodMapStorageProvider provider = session.getAttribute(this.sessionProviderKey, HotRodMapStorageProvider.class); - if (provider == null) { - provider = new HotRodMapStorageProvider(session, this, this.hotRodConfigurationIdentifier, this.jtaEnabled); - session.setAttribute(this.sessionProviderKey, provider); - } - return provider; + return SessionAttributesUtils.createProviderIfAbsent(session, factoryId, HotRodMapStorageProvider.class, session1 -> new HotRodMapStorageProvider(session1, this, jtaEnabled, lockTimeout)); } public HotRodEntityDescriptor getEntityDescriptor(Class c) { return AutogeneratedHotRodDescriptors.ENTITY_DESCRIPTOR_MAP.get(c); } - public & AbstractEntity, M> HotRodMapStorage getHotRodStorage(KeycloakSession session, Class modelType, AllAreasHotRodTransactionsWrapper txWrapper, MapStorageProviderFactory.Flag... flags) { - // We need to preload client session store before we load user session store to avoid recursive update of storages map - if (modelType == UserSessionModel.class) getHotRodStorage(session, AuthenticatedClientSessionModel.class, txWrapper, flags); - - return createHotRodStorage(session, modelType, txWrapper, flags); - } - - private & AbstractEntity, M> HotRodMapStorage createHotRodStorage(KeycloakSession session, Class modelType, AllAreasHotRodTransactionsWrapper txWrapper, MapStorageProviderFactory.Flag... flags) { - HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class); - HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) getEntityDescriptor(modelType); - - if (modelType == SingleUseObjectValueModel.class) { - return (HotRodMapStorage) new SingleUseObjectHotRodMapStorage(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, (HotRodEntityDescriptor) getEntityDescriptor(modelType), CLONER, txWrapper, lockTimeout); - } if (modelType == AuthenticatedClientSessionModel.class) { - return new HotRodMapStorage(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), - StringKeyConverter.StringKey.INSTANCE, - entityDescriptor, - CLONER, txWrapper, lockTimeout) { - @Override - protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { - return new ConcurrentHashMapKeycloakTransaction(this, keyConverter, cloner, clientSessionPredicates); - } - }; - } if (modelType == UserSessionModel.class) { - // Precompute client session storage to avoid recursive initialization - HotRodMapStorage clientSessionStore = getHotRodStorage(session, AuthenticatedClientSessionModel.class, txWrapper); - clientSessionStore.createTransaction(session); - return new HotRodMapStorage(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), - StringKeyConverter.StringKey.INSTANCE, - entityDescriptor, - CLONER, txWrapper, lockTimeout) { - @Override - protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { - Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass()); - return new HotRodUserSessionTransaction(this, keyConverter, cloner, fieldPredicates, clientSessionStore.createTransaction(session)); - } - }; - } - return new HotRodMapStorage<>(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER, txWrapper, lockTimeout); - } - @Override public void init(Config.Scope config) { this.lockTimeout = config.getLong("lockTimeout", 10000L); diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodCrudOperations.java similarity index 57% rename from model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodCrudOperations.java index e4999aa926..b5f65c2439 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodCrudOperations.java @@ -22,47 +22,26 @@ import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.StringKeyConverter; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; -import org.keycloak.models.map.storage.chm.SingleUseObjectKeycloakTransaction; import org.keycloak.models.map.storage.QueryParameters; -import org.keycloak.models.map.storage.chm.MapFieldPredicates; import org.keycloak.models.map.storage.chm.SingleUseObjectModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate; -import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper; -import org.keycloak.storage.SearchableModelField; -import java.util.Map; import java.util.stream.Stream; /** * @author Martin Kanis */ -public class SingleUseObjectHotRodMapStorage - extends HotRodMapStorage { +public class SingleUseObjectHotRodCrudOperations + extends HotRodCrudOperations { - private final StringKeyConverter keyConverter; - private final HotRodEntityDescriptor storedEntityDescriptor; - private final DeepCloner cloner; - - public SingleUseObjectHotRodMapStorage(KeycloakSession session, RemoteCache remoteCache, StringKeyConverter keyConverter, - HotRodEntityDescriptor storedEntityDescriptor, - DeepCloner cloner, AllAreasHotRodTransactionsWrapper txWrapper, Long lockTimeout) { - super(session, remoteCache, keyConverter, storedEntityDescriptor, cloner, txWrapper, lockTimeout); - this.keyConverter = keyConverter; - this.storedEntityDescriptor = storedEntityDescriptor; - this.cloner = cloner; - } - - @Override - protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) { - Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = - MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass()); - return new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates); + public SingleUseObjectHotRodCrudOperations(KeycloakSession session, RemoteCache remoteCache, StringKeyConverter keyConverter, + HotRodEntityDescriptor storedEntityDescriptor, + DeepCloner cloner, Long lockTimeout) { + super(session, remoteCache, keyConverter, storedEntityDescriptor, cloner, lockTimeout); } @Override diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodStoresWrapper.java similarity index 56% rename from model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodStoresWrapper.java index 20b3ff6344..bd50d872c6 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodTransactionsWrapper.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/AllAreasHotRodStoresWrapper.java @@ -18,42 +18,42 @@ package org.keycloak.models.map.storage.hotRod.transaction; import org.keycloak.models.AbstractKeycloakTransaction; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; /** - * This wrapper encapsulates transactions from all areas. This is needed because we need to control when the changes + * This wrapper encapsulates stores from all areas. This is needed because we need to control when the changes * from each area are applied to make sure it is performed before the HotRod client provided transaction is committed. */ -public class AllAreasHotRodTransactionsWrapper extends AbstractKeycloakTransaction { +public class AllAreasHotRodStoresWrapper extends AbstractKeycloakTransaction { - private final Map, MapKeycloakTransaction> MapKeycloakTransactionsMap = new ConcurrentHashMap<>(); + private final Map, ConcurrentHashMapStorage> MapKeycloakStoresMap = new ConcurrentHashMap<>(); - public MapKeycloakTransaction getOrCreateTxForModel(Class modelType, Supplier> supplier) { - MapKeycloakTransaction tx = MapKeycloakTransactionsMap.computeIfAbsent(modelType, t -> supplier.get()); - if (!tx.isActive()) { - tx.begin(); + public ConcurrentHashMapStorage getOrCreateStoreForModel(Class modelType, Supplier> supplier) { + ConcurrentHashMapStorage store = MapKeycloakStoresMap.computeIfAbsent(modelType, t -> supplier.get()); + if (!store.isActive()) { + store.begin(); } - return tx; + return store; } @Override protected void commitImpl() { - MapKeycloakTransactionsMap.values().forEach(MapKeycloakTransaction::commit); + MapKeycloakStoresMap.values().forEach(ConcurrentHashMapStorage::commit); } @Override protected void rollbackImpl() { - MapKeycloakTransactionsMap.values().forEach(MapKeycloakTransaction::rollback); + MapKeycloakStoresMap.values().forEach(ConcurrentHashMapStorage::rollback); } @Override public void setRollbackOnly() { super.setRollbackOnly(); - MapKeycloakTransactionsMap.values().forEach(MapKeycloakTransaction::setRollbackOnly); + MapKeycloakStoresMap.values().forEach(ConcurrentHashMapStorage::setRollbackOnly); } } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java deleted file mode 100644 index 58e99006b3..0000000000 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/transaction/NoActionHotRodTransactionWrapper.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.transaction; - -import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.UpdatableEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.QueryParameters; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; - -import java.util.stream.Stream; - -/** - * This is used to return ConcurrentHashMapTransaction (used for operating - * RemoteCache) functionality to providers but not enlist actualTx the way - * we need: in prepare phase. - */ -public class NoActionHotRodTransactionWrapper implements MapKeycloakTransaction { - - - private final ConcurrentHashMapKeycloakTransaction actualTx; - - public NoActionHotRodTransactionWrapper(ConcurrentHashMapKeycloakTransaction actualTx) { - this.actualTx = actualTx; - } - - @Override - public V create(V value) { - return actualTx.create(value); - } - - @Override - public V read(String key) { - return actualTx.read(key); - } - - @Override - public Stream read(QueryParameters queryParameters) { - return actualTx.read(queryParameters); - } - - @Override - public long getCount(QueryParameters queryParameters) { - return actualTx.getCount(queryParameters); - } - - @Override - public boolean delete(String key) { - return actualTx.delete(key); - } - - @Override - public long delete(QueryParameters queryParameters) { - return actualTx.delete(queryParameters); - } - - @Override - public boolean exists(String key) { - return actualTx.exists(key); - } - - @Override - public boolean exists(QueryParameters queryParameters) { - return actualTx.exists(queryParameters); - } - - @Override - public void begin() { - // Does nothing - } - - @Override - public void commit() { - // Does nothing - } - - @Override - public void rollback() { - // Does nothing - } - - @Override - public void setRollbackOnly() { - actualTx.setRollbackOnly(); - } - - @Override - public boolean getRollbackOnly() { - return actualTx.getRollbackOnly(); - } - - @Override - public boolean isActive() { - return actualTx.isActive(); - } -} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/HotRodUserSessionTransaction.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/HotRodUserSessionMapStorage.java similarity index 77% rename from model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/HotRodUserSessionTransaction.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/HotRodUserSessionMapStorage.java index a37f7e0ae2..fe44b37066 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/HotRodUserSessionTransaction.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/userSession/HotRodUserSessionMapStorage.java @@ -23,10 +23,9 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.common.delegate.SimpleDelegateProvider; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.QueryParameters; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapCrudOperations; -import org.keycloak.models.map.storage.chm.ConcurrentHashMapKeycloakTransaction; +import org.keycloak.models.map.storage.CrudOperations; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity; @@ -44,31 +43,25 @@ import java.util.stream.Stream; import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.IN; -public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTransaction { +public class HotRodUserSessionMapStorage extends ConcurrentHashMapStorage { - private final MapKeycloakTransaction clientSessionTransaction; + private final ConcurrentHashMapStorage clientSessionStore; - public HotRodUserSessionTransaction(ConcurrentHashMapCrudOperations map, - StringKeyConverter keyConverter, - DeepCloner cloner, - Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates, - MapKeycloakTransaction clientSessionTransaction + public HotRodUserSessionMapStorage(CrudOperations map, + StringKeyConverter keyConverter, + DeepCloner cloner, + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates, + ConcurrentHashMapStorage clientSessionStore ) { super(map, keyConverter, cloner, fieldPredicates); - this.clientSessionTransaction = clientSessionTransaction; - } - - @Override - public void commit() { - super.commit(); - clientSessionTransaction.commit(); + this.clientSessionStore = clientSessionStore; } private MapAuthenticatedClientSessionEntity wrapClientSessionEntityToClientSessionAwareDelegate(MapAuthenticatedClientSessionEntity d) { return new MapAuthenticatedClientSessionEntityDelegate(new HotRodAuthenticatedClientSessionEntityDelegateProvider(d) { @Override public MapAuthenticatedClientSessionEntity loadClientSessionFromDatabase() { - return clientSessionTransaction.read(d.getId()); + return clientSessionStore.read(d.getId()); } }); } @@ -79,7 +72,7 @@ public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTr return new MapUserSessionEntityDelegate(new SimpleDelegateProvider<>(entity)) { private boolean filterAndRemoveNotExpired(MapAuthenticatedClientSessionEntity clientSession) { - if (!clientSessionTransaction.exists(clientSession.getId())) { + if (!clientSessionStore.exists(clientSession.getId())) { // If client session does not exist, remove the reference to it from userSessionEntity loaded in this transaction entity.removeAuthenticatedClientSession(clientSession.getClientId()); return false; @@ -94,7 +87,7 @@ public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTr return clientSessions == null ? null : clientSessions.stream() // Find whether client session still exists in Infinispan and if not, remove the reference from user session .filter(this::filterAndRemoveNotExpired) - .map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate) + .map(HotRodUserSessionMapStorage.this::wrapClientSessionEntityToClientSessionAwareDelegate) .collect(Collectors.toSet()); } @@ -103,13 +96,13 @@ public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTr return super.getAuthenticatedClientSession(clientUUID) // Find whether client session still exists in Infinispan and if not, remove the reference from user sessionZ .filter(this::filterAndRemoveNotExpired) - .map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate); + .map(HotRodUserSessionMapStorage.this::wrapClientSessionEntityToClientSessionAwareDelegate); } @Override public void addAuthenticatedClientSession(MapAuthenticatedClientSessionEntity clientSession) { super.addAuthenticatedClientSession(clientSession); - clientSessionTransaction.create(clientSession); + clientSessionStore.create(clientSession); } @Override @@ -118,14 +111,14 @@ public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTr if (!clientSession.isPresent()) { return false; } - return super.removeAuthenticatedClientSession(clientUUID) && clientSessionTransaction.delete(clientSession.get().getId()); + return super.removeAuthenticatedClientSession(clientUUID) && clientSessionStore.delete(clientSession.get().getId()); } @Override public void clearAuthenticatedClientSessions() { Set clientSessions = super.getAuthenticatedClientSessions(); if (clientSessions != null) { - clientSessionTransaction.delete(QueryParameters.withCriteria( + clientSessionStore.delete(QueryParameters.withCriteria( DefaultModelCriteria.criteria() .compare(HotRodAuthenticatedClientSessionEntity.ID, IN, clientSessions.stream() .map(MapAuthenticatedClientSessionEntity::getId)) @@ -157,7 +150,7 @@ public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTr MapUserSessionEntity uSession = read(key); Set clientSessions = uSession.getAuthenticatedClientSessions(); if (clientSessions != null) { - clientSessionTransaction.delete(QueryParameters.withCriteria( + clientSessionStore.delete(QueryParameters.withCriteria( DefaultModelCriteria.criteria() .compare(HotRodAuthenticatedClientSessionEntity.ID, IN, clientSessions.stream() .map(MapAuthenticatedClientSessionEntity::getId)) @@ -169,7 +162,7 @@ public class HotRodUserSessionTransaction extends ConcurrentHashMapKeycloakTr @Override public long delete(QueryParameters queryParameters) { - clientSessionTransaction.delete(QueryParameters.withCriteria( + clientSessionStore.delete(QueryParameters.withCriteria( DefaultModelCriteria.criteria() .compare(HotRodAuthenticatedClientSessionEntity.ID, IN, read(queryParameters) .flatMap(userSession -> Optional.ofNullable(userSession.getAuthenticatedClientSessions()).orElse(Collections.emptySet()).stream().map(AbstractEntity::getId))) diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorage.java similarity index 93% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorage.java index 7c10bb7d37..67a5a93252 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorage.java @@ -45,7 +45,7 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.ExpirableEntity; import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.common.StringKeyConverter.UUIDKey; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.chm.MapFieldPredicates; import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; @@ -57,16 +57,16 @@ import static org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory.C import static org.keycloak.models.map.storage.jpa.PaginationUtils.paginateQuery; import static org.keycloak.utils.StreamsUtil.closing; -public abstract class JpaMapKeycloakTransaction implements MapKeycloakTransaction { +public abstract class JpaMapStorage implements MapStorage { - private static final Logger logger = Logger.getLogger(JpaMapKeycloakTransaction.class); + private static final Logger logger = Logger.getLogger(JpaMapStorage.class); private final KeycloakSession session; private final Class entityType; private final Class modelType; private final boolean isExpirableEntity; protected EntityManager em; - public JpaMapKeycloakTransaction(KeycloakSession session, Class entityType, Class modelType, EntityManager em) { + public JpaMapStorage(KeycloakSession session, Class entityType, Class modelType, EntityManager em) { this.session = session; this.em = em; this.entityType = entityType; @@ -304,36 +304,6 @@ public abstract class JpaMapKeycloakTransaction(StringKeyConverter.StringKey.INSTANCE, MapFieldPredicates.getPredicates(modelType)); } - @Override - public void begin() { - // no-op: rely on JPA transaction enlisted by the JPA storage provider. - } - - @Override - public void commit() { - // no-op: rely on JPA transaction enlisted by the JPA storage provider. - } - - @Override - public void rollback() { - // no-op: rely on JPA transaction enlisted by the JPA storage provider. - } - - @Override - public void setRollbackOnly() { - em.getTransaction().setRollbackOnly(); - } - - @Override - public boolean getRollbackOnly() { - return em.getTransaction().getRollbackOnly(); - } - - @Override - public boolean isActive() { - return em.getTransaction().isActive(); - } - private Predicate notExpired(final CriteriaBuilder cb, final JpaSubqueryProvider query, final Root root) { return cb.or(cb.greaterThan(root.get("expiration"), Time.currentTimeMillis()), cb.isNull(root.get("expiration"))); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java index 9647d8c550..1408d9242e 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java @@ -21,7 +21,7 @@ import javax.persistence.EntityManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.common.SessionAttributesUtils; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag; @@ -31,15 +31,21 @@ public class JpaMapStorageProvider implements MapStorageProvider { private final JpaMapStorageProviderFactory factory; private final KeycloakSession session; private final EntityManager em; - private final String sessionTxKey; - private final boolean jtaEnabled; - public JpaMapStorageProvider(JpaMapStorageProviderFactory factory, KeycloakSession session, EntityManager em, String sessionTxKey, boolean jtaEnabled) { + private final int factoryId; + + public JpaMapStorageProvider(JpaMapStorageProviderFactory factory, KeycloakSession session, EntityManager em, boolean jtaEnabled, int factoryId) { this.factory = factory; this.session = session; this.em = em; - this.sessionTxKey = sessionTxKey; - this.jtaEnabled = jtaEnabled; + this.factoryId = factoryId; + + // Create the JPA transaction and enlist it if needed. + // Don't enlist if JTA is enabled as it has been enlisted with JTA automatically. + if (!jtaEnabled) { + KeycloakTransaction jpaTransaction = new JpaTransactionWrapper(em.getTransaction()); + session.getTransactionManager().enlist(jpaTransaction); + } } @Override @@ -49,21 +55,10 @@ public class JpaMapStorageProvider implements MapStorageProvider { @Override @SuppressWarnings("unchecked") - public MapStorage getStorage(Class modelType, Flag... flags) { + public MapStorage getMapStorage(Class modelType, Flag... flags) { // validate and update the schema for the storage. this.factory.validateAndUpdateSchema(this.session, modelType); - // Create the JPA transaction and enlist it if needed. - // Don't enlist if JTA is enabled as it has been enlisted with JTA automatically. - if (!jtaEnabled && session.getAttribute(this.sessionTxKey) == null) { - KeycloakTransaction jpaTransaction = new JpaTransactionWrapper(em.getTransaction()); - session.getTransactionManager().enlist(jpaTransaction); - session.setAttribute(this.sessionTxKey, jpaTransaction); - } - return new MapStorage() { - @Override - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - return factory.createTransaction(session, modelType, em); - } - }; + + return SessionAttributesUtils.createMapStorageIfAbsent(session, JpaMapStorageProvider.class, modelType, factoryId, () -> factory.createMapStorage(session, modelType, em)); } } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index 6fb2b439e3..6aaf229cd3 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import javax.naming.InitialContext; @@ -82,6 +81,7 @@ import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.lock.MapLockEntity; +import org.keycloak.models.map.common.SessionAttributesUtils; import org.keycloak.models.map.realm.entity.MapAuthenticationExecutionEntity; import org.keycloak.models.map.realm.entity.MapAuthenticationExecutionEntityImpl; import org.keycloak.models.map.realm.entity.MapAuthenticationFlowEntity; @@ -102,48 +102,48 @@ import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntity; import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntityImpl; import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntity; import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntityImpl; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; -import org.keycloak.models.map.storage.jpa.authSession.JpaRootAuthenticationSessionMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.authSession.JpaRootAuthenticationSessionMapStorage; import org.keycloak.models.map.storage.jpa.authSession.entity.JpaAuthenticationSessionEntity; import org.keycloak.models.map.storage.jpa.authSession.entity.JpaRootAuthenticationSessionEntity; -import org.keycloak.models.map.storage.jpa.authorization.permission.JpaPermissionMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.authorization.permission.JpaPermissionMapStorage; import org.keycloak.models.map.storage.jpa.authorization.permission.entity.JpaPermissionEntity; -import org.keycloak.models.map.storage.jpa.authorization.policy.JpaPolicyMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.authorization.policy.JpaPolicyMapStorage; import org.keycloak.models.map.storage.jpa.authorization.policy.entity.JpaPolicyEntity; -import org.keycloak.models.map.storage.jpa.authorization.resource.JpaResourceMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.authorization.resource.JpaResourceMapStorage; import org.keycloak.models.map.storage.jpa.authorization.resource.entity.JpaResourceEntity; -import org.keycloak.models.map.storage.jpa.authorization.resourceServer.JpaResourceServerMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.authorization.resourceServer.JpaResourceServerMapStorage; import org.keycloak.models.map.storage.jpa.authorization.resourceServer.entity.JpaResourceServerEntity; -import org.keycloak.models.map.storage.jpa.authorization.scope.JpaScopeMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.authorization.scope.JpaScopeMapStorage; import org.keycloak.models.map.storage.jpa.authorization.scope.entity.JpaScopeEntity; -import org.keycloak.models.map.storage.jpa.client.JpaClientMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; -import org.keycloak.models.map.storage.jpa.clientScope.JpaClientScopeMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.clientScope.JpaClientScopeMapStorage; import org.keycloak.models.map.storage.jpa.clientScope.entity.JpaClientScopeEntity; -import org.keycloak.models.map.storage.jpa.event.admin.JpaAdminEventMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.event.admin.JpaAdminEventMapStorage; import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntity; -import org.keycloak.models.map.storage.jpa.event.auth.JpaAuthEventMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.event.auth.JpaAuthEventMapStorage; import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity; -import org.keycloak.models.map.storage.jpa.group.JpaGroupMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.group.JpaGroupMapStorage; import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity; -import org.keycloak.models.map.storage.jpa.lock.JpaLockMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.lock.JpaLockMapStorage; import org.keycloak.models.map.storage.jpa.lock.entity.JpaLockEntity; -import org.keycloak.models.map.storage.jpa.loginFailure.JpaUserLoginFailureMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.loginFailure.JpaUserLoginFailureMapStorage; import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity; -import org.keycloak.models.map.storage.jpa.realm.JpaRealmMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.realm.JpaRealmMapStorage; import org.keycloak.models.map.storage.jpa.realm.entity.JpaComponentEntity; import org.keycloak.models.map.storage.jpa.realm.entity.JpaRealmEntity; -import org.keycloak.models.map.storage.jpa.role.JpaRoleMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.role.JpaRoleMapStorage; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; -import org.keycloak.models.map.storage.jpa.singleUseObject.JpaSingleUseObjectMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.singleUseObject.JpaSingleUseObjectMapStorage; import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity; import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; -import org.keycloak.models.map.storage.jpa.userSession.JpaUserSessionMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.userSession.JpaUserSessionMapStorage; import org.keycloak.models.map.storage.jpa.userSession.entity.JpaClientSessionEntity; import org.keycloak.models.map.storage.jpa.userSession.entity.JpaUserSessionEntity; -import org.keycloak.models.map.storage.jpa.user.JpaUserMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.user.JpaUserMapStorage; import org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentEntity; import org.keycloak.models.map.storage.jpa.user.entity.JpaUserEntity; import org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityEntity; @@ -161,8 +161,6 @@ public class JpaMapStorageProviderFactory implements EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "jpa"; - private static final String SESSION_TX_PREFIX = "jpa-map-tx-"; - private static final AtomicInteger ENUMERATOR = new AtomicInteger(0); private static final Logger logger = Logger.getLogger(JpaMapStorageProviderFactory.class); public static final String HIBERNATE_DEFAULT_SCHEMA = "hibernate.default_schema"; @@ -172,8 +170,8 @@ public class JpaMapStorageProviderFactory implements private volatile EntityManagerFactory emf; private final Set> validatedModels = ConcurrentHashMap.newKeySet(); private Config.Scope config; - private final String sessionProviderKey; - private final String sessionTxKey; + + private final int factoryId = SessionAttributesUtils.grabNewFactoryIdentifier(); private String databaseShortName; // Object instances for each single JpaMapStorageProviderFactory instance per model type. @@ -231,71 +229,54 @@ public class JpaMapStorageProviderFactory implements .constructor(JpaLockEntity.class, JpaLockEntity::new) .build(); - private static final Map, BiFunction> MODEL_TO_TX = new HashMap<>(); + private static final Map, BiFunction> MODEL_TO_STORE = new HashMap<>(); static { //auth-sessions - MODEL_TO_TX.put(RootAuthenticationSessionModel.class, JpaRootAuthenticationSessionMapKeycloakTransaction::new); + MODEL_TO_STORE.put(RootAuthenticationSessionModel.class, JpaRootAuthenticationSessionMapStorage::new); //authorization - MODEL_TO_TX.put(ResourceServer.class, JpaResourceServerMapKeycloakTransaction::new); - MODEL_TO_TX.put(Resource.class, JpaResourceMapKeycloakTransaction::new); - MODEL_TO_TX.put(Scope.class, JpaScopeMapKeycloakTransaction::new); - MODEL_TO_TX.put(PermissionTicket.class, JpaPermissionMapKeycloakTransaction::new); - MODEL_TO_TX.put(Policy.class, JpaPolicyMapKeycloakTransaction::new); + MODEL_TO_STORE.put(ResourceServer.class, JpaResourceServerMapStorage::new); + MODEL_TO_STORE.put(Resource.class, JpaResourceMapStorage::new); + MODEL_TO_STORE.put(Scope.class, JpaScopeMapStorage::new); + MODEL_TO_STORE.put(PermissionTicket.class, JpaPermissionMapStorage::new); + MODEL_TO_STORE.put(Policy.class, JpaPolicyMapStorage::new); //clients - MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new); + MODEL_TO_STORE.put(ClientModel.class, JpaClientMapStorage::new); //client-scopes - MODEL_TO_TX.put(ClientScopeModel.class, JpaClientScopeMapKeycloakTransaction::new); + MODEL_TO_STORE.put(ClientScopeModel.class, JpaClientScopeMapStorage::new); //events - MODEL_TO_TX.put(AdminEvent.class, JpaAdminEventMapKeycloakTransaction::new); - MODEL_TO_TX.put(Event.class, JpaAuthEventMapKeycloakTransaction::new); + MODEL_TO_STORE.put(AdminEvent.class, JpaAdminEventMapStorage::new); + MODEL_TO_STORE.put(Event.class, JpaAuthEventMapStorage::new); //groups - MODEL_TO_TX.put(GroupModel.class, JpaGroupMapKeycloakTransaction::new); + MODEL_TO_STORE.put(GroupModel.class, JpaGroupMapStorage::new); //realms - MODEL_TO_TX.put(RealmModel.class, JpaRealmMapKeycloakTransaction::new); + MODEL_TO_STORE.put(RealmModel.class, JpaRealmMapStorage::new); //roles - MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new); + MODEL_TO_STORE.put(RoleModel.class, JpaRoleMapStorage::new); //single-use-objects - MODEL_TO_TX.put(SingleUseObjectValueModel.class, JpaSingleUseObjectMapKeycloakTransaction::new); + MODEL_TO_STORE.put(SingleUseObjectValueModel.class, JpaSingleUseObjectMapStorage::new); //user-login-failures - MODEL_TO_TX.put(UserLoginFailureModel.class, JpaUserLoginFailureMapKeycloakTransaction::new); + MODEL_TO_STORE.put(UserLoginFailureModel.class, JpaUserLoginFailureMapStorage::new); //users - MODEL_TO_TX.put(UserModel.class, JpaUserMapKeycloakTransaction::new); + MODEL_TO_STORE.put(UserModel.class, JpaUserMapStorage::new); //sessions - MODEL_TO_TX.put(UserSessionModel.class, JpaUserSessionMapKeycloakTransaction::new); + MODEL_TO_STORE.put(UserSessionModel.class, JpaUserSessionMapStorage::new); //locks - MODEL_TO_TX.put(MapLockEntity.class, JpaLockMapKeycloakTransaction::new); + MODEL_TO_STORE.put(MapLockEntity.class, JpaLockMapStorage::new); } private boolean jtaEnabled; private JtaTransactionManagerLookup jtaLookup; - public JpaMapStorageProviderFactory() { - int index = ENUMERATOR.getAndIncrement(); - // this identifier is used to create HotRodMapProvider only once per session per factory instance - this.sessionProviderKey = PROVIDER_ID + "-" + index; - - // When there are more JPA configurations available in Keycloak (for example, global/realm1/realm2 etc.) - // there will be more instances of this factory created where each holds one configuration. - // The following identifier can be used to uniquely identify instance of this factory. - // This can be later used, for example, to store provider/transaction instances inside session - // attributes without collisions between several configurations - this.sessionTxKey = SESSION_TX_PREFIX + index; - } - - public MapKeycloakTransaction createTransaction(KeycloakSession session, Class modelType, EntityManager em) { - return MODEL_TO_TX.get(modelType).apply(session, em); + public MapStorage createMapStorage(KeycloakSession session, Class modelType, EntityManager em) { + return MODEL_TO_STORE.get(modelType).apply(session, em); } @Override public MapStorageProvider create(KeycloakSession session) { lazyInit(); - // check the session for a cached provider before creating a new one. - JpaMapStorageProvider provider = session.getAttribute(this.sessionProviderKey, JpaMapStorageProvider.class); - if (provider == null) { - provider = new JpaMapStorageProvider(this, session, PersistenceExceptionConverter.create(session, getEntityManager()), this.sessionTxKey, this.jtaEnabled); - session.setAttribute(this.sessionProviderKey, provider); - } - return provider; + + return SessionAttributesUtils.createProviderIfAbsent(session, factoryId, JpaMapStorageProvider.class, + session1 -> new JpaMapStorageProvider(this, session, PersistenceExceptionConverter.create(session, getEntityManager()), this.jtaEnabled, factoryId)); } protected EntityManager getEntityManager() { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/JpaRootAuthenticationSessionMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/JpaRootAuthenticationSessionMapStorage.java similarity index 93% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/JpaRootAuthenticationSessionMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/JpaRootAuthenticationSessionMapStorage.java index ad68a6efb0..dea2c625b7 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/JpaRootAuthenticationSessionMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/JpaRootAuthenticationSessionMapStorage.java @@ -31,7 +31,7 @@ import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntityDel import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_SESSION; import org.keycloak.models.map.common.StringKeyConverter; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.authSession.delegate.JpaRootAuthenticationSessionDelegateProvider; @@ -42,9 +42,9 @@ import org.keycloak.sessions.RootAuthenticationSessionModel; import java.sql.Connection; import java.util.UUID; -public class JpaRootAuthenticationSessionMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaRootAuthenticationSessionMapStorage extends JpaMapStorage { - public JpaRootAuthenticationSessionMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaRootAuthenticationSessionMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaRootAuthenticationSessionEntity.class, RootAuthenticationSessionModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/JpaPermissionMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/JpaPermissionMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/JpaPermissionMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/JpaPermissionMapStorage.java index b63165c7fe..b5509b02d8 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/JpaPermissionMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/permission/JpaPermissionMapStorage.java @@ -25,16 +25,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntityDelegate; import org.keycloak.models.map.storage.jpa.Constants; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.authorization.permission.delegate.JpaPermissionDelegateProvider; import org.keycloak.models.map.storage.jpa.authorization.permission.entity.JpaPermissionEntity; -public class JpaPermissionMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaPermissionMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaPermissionMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaPermissionMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaPermissionEntity.class, PermissionTicket.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/JpaPolicyMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/JpaPolicyMapStorage.java similarity index 90% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/JpaPolicyMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/JpaPolicyMapStorage.java index eff5e26009..38dd5a9cb7 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/JpaPolicyMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/policy/JpaPolicyMapStorage.java @@ -25,16 +25,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.authorization.entity.MapPolicyEntity; import org.keycloak.models.map.authorization.entity.MapPolicyEntityDelegate; import org.keycloak.models.map.storage.jpa.Constants; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.authorization.policy.delegate.JpaPolicyDelegateProvider; import org.keycloak.models.map.storage.jpa.authorization.policy.entity.JpaPolicyEntity; -public class JpaPolicyMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaPolicyMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaPolicyMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaPolicyMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaPolicyEntity.class, Policy.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/JpaResourceMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/JpaResourceMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/JpaResourceMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/JpaResourceMapStorage.java index de3d3eb178..f67781579f 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/JpaResourceMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resource/JpaResourceMapStorage.java @@ -25,16 +25,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.authorization.entity.MapResourceEntity; import org.keycloak.models.map.authorization.entity.MapResourceEntityDelegate; import org.keycloak.models.map.storage.jpa.Constants; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.authorization.resource.delegate.JpaResourceDelegateProvider; import org.keycloak.models.map.storage.jpa.authorization.resource.entity.JpaResourceEntity; -public class JpaResourceMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaResourceMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaResourceMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaResourceMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaResourceEntity.class, Resource.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/JpaResourceServerMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/JpaResourceServerMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/JpaResourceServerMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/JpaResourceServerMapStorage.java index 1c8d577deb..4f3c2ba97e 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/JpaResourceServerMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/resourceServer/JpaResourceServerMapStorage.java @@ -25,16 +25,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; import org.keycloak.models.map.authorization.entity.MapResourceServerEntityDelegate; import org.keycloak.models.map.storage.jpa.Constants; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.authorization.resourceServer.delegate.JpaResourceServerDelegateProvider; import org.keycloak.models.map.storage.jpa.authorization.resourceServer.entity.JpaResourceServerEntity; -public class JpaResourceServerMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaResourceServerMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaResourceServerMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaResourceServerMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaResourceServerEntity.class, ResourceServer.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/JpaScopeMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/JpaScopeMapStorage.java similarity index 90% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/JpaScopeMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/JpaScopeMapStorage.java index c400daed76..5742f76adf 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/JpaScopeMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authorization/scope/JpaScopeMapStorage.java @@ -25,16 +25,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.authorization.entity.MapScopeEntity; import org.keycloak.models.map.authorization.entity.MapScopeEntityDelegate; import org.keycloak.models.map.storage.jpa.Constants; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.authorization.scope.delagate.JpaScopeDelegateProvider; import org.keycloak.models.map.storage.jpa.authorization.scope.entity.JpaScopeEntity; -public class JpaScopeMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaScopeMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaScopeMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaScopeMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaScopeEntity.class, Scope.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java index 3bdf887618..4c115ecae4 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java @@ -26,15 +26,15 @@ import org.keycloak.models.map.client.MapClientEntity; import org.keycloak.models.map.client.MapClientEntityDelegate; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.client.delegate.JpaClientDelegateProvider; -public class JpaClientMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaClientMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaClientMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaClientMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaClientEntity.class, ClientModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/JpaClientScopeMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/JpaClientScopeMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/JpaClientScopeMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/JpaClientScopeMapStorage.java index 61b5feb086..67788c476f 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/JpaClientScopeMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/clientScope/JpaClientScopeMapStorage.java @@ -25,16 +25,16 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.clientscope.MapClientScopeEntity; import org.keycloak.models.map.clientscope.MapClientScopeEntityDelegate; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_CLIENT_SCOPE; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.clientScope.delegate.JpaClientScopeDelegateProvider; import org.keycloak.models.map.storage.jpa.clientScope.entity.JpaClientScopeEntity; -public class JpaClientScopeMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaClientScopeMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaClientScopeMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaClientScopeMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaClientScopeEntity.class, ClientScopeModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/JpaAdminEventMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/JpaAdminEventMapStorage.java similarity index 83% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/JpaAdminEventMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/JpaAdminEventMapStorage.java index 80fc51338c..14ed4cdc12 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/JpaAdminEventMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/admin/JpaAdminEventMapStorage.java @@ -24,7 +24,8 @@ import javax.persistence.criteria.Selection; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.events.MapAdminEventEntity; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntity; @@ -32,13 +33,13 @@ import org.keycloak.models.map.storage.jpa.event.admin.entity.JpaAdminEventEntit import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ADMIN_EVENT; /** - * A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for admin event entities. + * A {@link MapStorage} implementation for admin event entities. * * @author Stefan Guilhen */ -public class JpaAdminEventMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaAdminEventMapStorage extends JpaMapStorage { - public JpaAdminEventMapKeycloakTransaction(KeycloakSession session, final EntityManager em) { + public JpaAdminEventMapStorage(KeycloakSession session, final EntityManager em) { super(session, JpaAdminEventEntity.class, AdminEvent.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/JpaAuthEventMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/JpaAuthEventMapStorage.java similarity index 83% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/JpaAuthEventMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/JpaAuthEventMapStorage.java index a014025ac0..57399a0502 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/JpaAuthEventMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/event/auth/JpaAuthEventMapStorage.java @@ -24,7 +24,8 @@ import javax.persistence.criteria.Selection; import org.keycloak.events.Event; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.events.MapAuthEventEntity; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity; @@ -32,13 +33,13 @@ import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_EVENT; /** - * A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for auth event entities. + * A {@link MapStorage} implementation for auth event entities. * * @author Stefan Guilhen */ -public class JpaAuthEventMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaAuthEventMapStorage extends JpaMapStorage { - public JpaAuthEventMapKeycloakTransaction(KeycloakSession session, final EntityManager em) { + public JpaAuthEventMapStorage(KeycloakSession session, final EntityManager em) { super(session, JpaAuthEventEntity.class, Event.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapStorage.java index c0c6a27765..70d1cc0a82 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/group/JpaGroupMapStorage.java @@ -27,14 +27,14 @@ import org.keycloak.models.map.group.MapGroupEntityDelegate; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP; import org.keycloak.models.map.storage.jpa.group.delegate.JpaGroupDelegateProvider; import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; -public class JpaGroupMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaGroupMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaGroupMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaGroupMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaGroupEntity.class, GroupModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaAutoFlushListener.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaAutoFlushListener.java index 83ca58c2d5..2521d233ad 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaAutoFlushListener.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaAutoFlushListener.java @@ -25,7 +25,7 @@ import org.hibernate.event.spi.AutoFlushEvent; import org.hibernate.event.spi.EventSource; import org.hibernate.internal.CoreMessageLogger; import org.jboss.logging.Logger; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; /** * Extends Hibernate's {@link DefaultAutoFlushEventListener} to always flush queued inserts to allow correct handling @@ -35,7 +35,7 @@ import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; * This class copies over all functionality of the base class that can't be overwritten via inheritance. * This is being tracked as part of keycloak/keycloak#11666. *

- * This also clears the JPA map store query level cache for the {@link JpaMapKeycloakTransaction} whenever there is some data written to the database. + * This also clears the JPA map store query level cache for the {@link JpaMapStorage} whenever there is some data written to the database. */ public class JpaAutoFlushListener extends DefaultAutoFlushEventListener { @@ -90,7 +90,7 @@ public class JpaAutoFlushListener extends DefaultAutoFlushEventListener { || source.getActionQueue().areTablesToBeUpdated(event.getQuerySpaces()); if (flushIsReallyNeeded) { // clear the per-session query cache, as changing an entity might change any of the cached query results - JpaMapKeycloakTransaction.clearQueryCache(source.getSession()); + JpaMapStorage.clearQueryCache(source.getSession()); } return flushIsReallyNeeded; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/JpaLockMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/JpaLockMapStorage.java similarity index 89% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/JpaLockMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/JpaLockMapStorage.java index 53afdff891..2938923ad4 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/JpaLockMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/lock/JpaLockMapStorage.java @@ -20,7 +20,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.lock.MapLockEntity; import org.keycloak.models.map.lock.MapLockEntityDelegate; import org.keycloak.models.map.storage.jpa.Constants; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.lock.delegate.JpaLockDelegateProvider; @@ -31,10 +31,10 @@ import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Root; import javax.persistence.criteria.Selection; -public class JpaLockMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaLockMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaLockMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaLockMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaLockEntity.class, MapLockEntity.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapStorage.java similarity index 86% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapStorage.java index 382c378787..f0da711ac3 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapStorage.java @@ -25,7 +25,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntityDelegate; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.loginFailure.delegate.JpaUserLoginFailureDelegateProvider; @@ -34,14 +34,14 @@ import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailu import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE; /** - * A {@link JpaMapKeycloakTransaction} implementation for user login failure entities. + * A {@link JpaMapStorage} implementation for user login failure entities. * * @author Stefan Guilhen */ -public class JpaUserLoginFailureMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaUserLoginFailureMapStorage extends JpaMapStorage { @SuppressWarnings("unchecked") - public JpaUserLoginFailureMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaUserLoginFailureMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaUserLoginFailureEntity.class, UserLoginFailureModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/JpaRealmMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/JpaRealmMapStorage.java similarity index 86% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/JpaRealmMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/JpaRealmMapStorage.java index 9c28b288cb..6c04cf779f 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/JpaRealmMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/JpaRealmMapStorage.java @@ -25,7 +25,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.map.realm.MapRealmEntity; import org.keycloak.models.map.realm.MapRealmEntityDelegate; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.realm.delegate.JpaRealmDelegateProvider; @@ -34,13 +35,13 @@ import org.keycloak.models.map.storage.jpa.realm.entity.JpaRealmEntity; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_REALM; /** - * A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for realm entities. + * A {@link MapStorage} implementation for realm entities. * * @author Stefan Guilhen */ -public class JpaRealmMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaRealmMapStorage extends JpaMapStorage { - public JpaRealmMapKeycloakTransaction(KeycloakSession session, final EntityManager em) { + public JpaRealmMapStorage(KeycloakSession session, final EntityManager em) { super(session, JpaRealmEntity.class, RealmModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapStorage.java similarity index 90% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapStorage.java index 4606ee3f86..209dfcc60d 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/JpaRoleMapStorage.java @@ -28,18 +28,18 @@ import org.keycloak.models.map.role.MapRoleEntity; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ROLE; import static org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory.CLONER; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.role.delegate.JpaMapRoleEntityDelegate; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; -public class JpaRoleMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaRoleMapStorage extends JpaMapStorage { - private static final Logger logger = Logger.getLogger(JpaRoleMapKeycloakTransaction.class); + private static final Logger logger = Logger.getLogger(JpaRoleMapStorage.class); @SuppressWarnings("unchecked") - public JpaRoleMapKeycloakTransaction(KeycloakSession session, EntityManager em) { + public JpaRoleMapStorage(KeycloakSession session, EntityManager em) { super(session, JpaRoleEntity.class, RoleModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaMapRoleEntityDelegate.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaMapRoleEntityDelegate.java index 8568def834..42b78568a1 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaMapRoleEntityDelegate.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/role/delegate/JpaMapRoleEntityDelegate.java @@ -19,6 +19,7 @@ package org.keycloak.models.map.storage.jpa.role.delegate; import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.role.MapRoleEntityDelegate; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleCompositeEntity; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleCompositeEntityKey; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; @@ -34,7 +35,7 @@ import java.util.stream.Collectors; * It will delegate all access to the composite roles to a separate table. * * For performance reasons, it caches the composite roles within the session if they have already been retrieved. - * This relies on the behavior of {@link org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction} that + * This relies on the behavior of {@link JpaMapStorage} that * each entity is created only once within each session. * * @author Alexander Schwartz diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapStorage.java similarity index 82% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapStorage.java index 87e90e261a..449a179d6f 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapStorage.java @@ -24,7 +24,8 @@ import javax.persistence.criteria.Selection; import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity; @@ -32,13 +33,13 @@ import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseOb import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT; /** - * A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for single-use object entities. + * A {@link MapStorage} implementation for single-use object entities. * * @author Stefan Guilhen */ -public class JpaSingleUseObjectMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaSingleUseObjectMapStorage extends JpaMapStorage { - public JpaSingleUseObjectMapKeycloakTransaction(KeycloakSession session, final EntityManager em) { + public JpaSingleUseObjectMapStorage(KeycloakSession session, final EntityManager em) { super(session, JpaSingleUseObjectEntity.class, SingleUseObjectValueModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserMapStorage.java similarity index 88% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserMapStorage.java index 83ef0de5c6..96410da061 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/JpaUserMapStorage.java @@ -23,7 +23,8 @@ import javax.persistence.criteria.Selection; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.user.delegate.JpaUserDelegateProvider; @@ -34,13 +35,13 @@ import org.keycloak.models.map.user.MapUserEntityDelegate; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER; /** - * A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for user entities. + * A {@link MapStorage} implementation for user entities. * * @author Stefan Guilhen */ -public class JpaUserMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaUserMapStorage extends JpaMapStorage { - public JpaUserMapKeycloakTransaction(KeycloakSession session,final EntityManager em) { + public JpaUserMapStorage(KeycloakSession session, final EntityManager em) { super(session, JpaUserEntity.class, UserModel.class, em); } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/JpaUserSessionMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/JpaUserSessionMapStorage.java similarity index 86% rename from model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/JpaUserSessionMapKeycloakTransaction.java rename to model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/JpaUserSessionMapStorage.java index 46adb4eaf1..64df96db97 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/JpaUserSessionMapKeycloakTransaction.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/JpaUserSessionMapStorage.java @@ -23,7 +23,7 @@ import javax.persistence.criteria.Selection; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaMapStorage; import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.userSession.entity.JpaUserSessionEntity; @@ -31,9 +31,9 @@ import org.keycloak.models.map.userSession.MapUserSessionEntity; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_SESSION; -public class JpaUserSessionMapKeycloakTransaction extends JpaMapKeycloakTransaction { +public class JpaUserSessionMapStorage extends JpaMapStorage { - public JpaUserSessionMapKeycloakTransaction(KeycloakSession session, final EntityManager em) { + public JpaUserSessionMapStorage(KeycloakSession session, final EntityManager em) { super(session, JpaUserSessionEntity.class, UserSessionModel.class, em); } diff --git a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapKeycloakTransaction.java b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorage.java similarity index 87% rename from model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapKeycloakTransaction.java rename to model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorage.java index e399393395..b70ebb6d18 100644 --- a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapKeycloakTransaction.java +++ b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorage.java @@ -19,22 +19,20 @@ package org.keycloak.models.map.storage.ldap; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; -import org.keycloak.Config; -import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.UpdatableEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.QueryParameters; -public abstract class LdapMapKeycloakTransaction implements MapKeycloakTransaction { +public abstract class LdapMapStorage implements MapStorage, KeycloakTransaction { private boolean active; private boolean rollback; - public LdapMapKeycloakTransaction() { + public LdapMapStorage() { } protected abstract static class MapTaskWithValue { diff --git a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProvider.java b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProvider.java index 1c5002b6b8..72669c117a 100644 --- a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProvider.java +++ b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProvider.java @@ -18,19 +18,21 @@ package org.keycloak.models.map.storage.ldap; import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.common.SessionAttributesUtils; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag; public class LdapMapStorageProvider implements MapStorageProvider { + private final KeycloakSession session; private final LdapMapStorageProviderFactory factory; - private final String sessionTxPrefix; + private final int factoryId; - public LdapMapStorageProvider(LdapMapStorageProviderFactory factory, String sessionTxPrefix) { + public LdapMapStorageProvider(KeycloakSession session, LdapMapStorageProviderFactory factory, int factoryId) { + this.session = session; this.factory = factory; - this.sessionTxPrefix = sessionTxPrefix; + this.factoryId = factoryId; } @Override @@ -38,21 +40,12 @@ public class LdapMapStorageProvider implements MapStorageProvider { } @Override - @SuppressWarnings("unchecked") - public MapStorage getStorage(Class modelType, Flag... flags) { - // MapStorage is not a functional interface, therefore don't try to convert it to a lambda as additional methods might be added in the future - //noinspection Convert2Lambda - return new MapStorage() { - @Override - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - MapKeycloakTransaction sessionTx = session.getAttribute(sessionTxPrefix + modelType.hashCode(), MapKeycloakTransaction.class); - if (sessionTx == null) { - sessionTx = factory.createTransaction(session, modelType); - session.setAttribute(sessionTxPrefix + modelType.hashCode(), sessionTx); - } - return sessionTx; - } - }; + public MapStorage getMapStorage(Class modelType, Flag... flags) { + return SessionAttributesUtils.createMapStorageIfAbsent(session, getClass(), modelType, factoryId, () -> { + LdapMapStorage store = (LdapMapStorage) factory.createMapStorage(session, modelType); + session.getTransactionManager().enlist(store); + return store; + }); } } diff --git a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProviderFactory.java b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProviderFactory.java index 985f209050..9979d4d506 100644 --- a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProviderFactory.java +++ b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/LdapMapStorageProviderFactory.java @@ -18,7 +18,6 @@ package org.keycloak.models.map.storage.ldap; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; import org.keycloak.Config; import org.keycloak.common.Profile; @@ -27,11 +26,12 @@ import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RoleModel; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.common.SessionAttributesUtils; +import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.storage.ldap.config.LdapMapConfig; -import org.keycloak.models.map.storage.ldap.role.LdapRoleMapKeycloakTransaction; +import org.keycloak.models.map.storage.ldap.role.LdapRoleMapStorage; import org.keycloak.provider.EnvironmentDependentProviderFactory; public class LdapMapStorageProviderFactory implements @@ -40,29 +40,23 @@ public class LdapMapStorageProviderFactory implements EnvironmentDependentProviderFactory { public static final String PROVIDER_ID = "ldap-map-storage"; - private static final AtomicInteger SESSION_TX_PREFIX_ENUMERATOR = new AtomicInteger(0); - private static final String SESSION_TX_PREFIX = "ldap-map-tx-"; - private final String sessionTxPrefixForFactoryInstance; + private final int factoryId = SessionAttributesUtils.grabNewFactoryIdentifier(); private Config.Scope config; @SuppressWarnings("rawtypes") - private static final Map, LdapRoleMapKeycloakTransaction.LdapRoleMapKeycloakTransactionFunction> MODEL_TO_TX = new HashMap<>(); + private static final Map, LdapRoleMapStorage.LdapRoleMapKeycloakTransactionFunction> MODEL_TO_STORE = new HashMap<>(); static { - MODEL_TO_TX.put(RoleModel.class, LdapRoleMapKeycloakTransaction::new); + MODEL_TO_STORE.put(RoleModel.class, LdapRoleMapStorage::new); } - public LdapMapStorageProviderFactory() { - sessionTxPrefixForFactoryInstance = SESSION_TX_PREFIX + SESSION_TX_PREFIX_ENUMERATOR.getAndIncrement() + "-"; - } - - public MapKeycloakTransaction createTransaction(KeycloakSession session, Class modelType) { - return MODEL_TO_TX.get(modelType).apply(session, config); + public MapStorage createMapStorage(KeycloakSession session, Class modelType) { + return MODEL_TO_STORE.get(modelType).apply(session, config); } @Override public MapStorageProvider create(KeycloakSession session) { - return new LdapMapStorageProvider(this, sessionTxPrefixForFactoryInstance); + return SessionAttributesUtils.createProviderIfAbsent(session, factoryId, LdapMapStorageProvider.class, session1 -> new LdapMapStorageProvider(session1, this, factoryId)); } @Override diff --git a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/LdapRoleMapKeycloakTransaction.java b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/LdapRoleMapStorage.java similarity index 97% rename from model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/LdapRoleMapKeycloakTransaction.java rename to model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/LdapRoleMapStorage.java index 02cf6d6f42..0dedd6cb9a 100644 --- a/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/LdapRoleMapKeycloakTransaction.java +++ b/model/map-ldap/src/main/java/org/keycloak/models/map/storage/ldap/role/LdapRoleMapStorage.java @@ -33,7 +33,7 @@ import org.keycloak.models.map.storage.ldap.MapModelCriteriaBuilderAssumingEqual import org.keycloak.models.map.storage.ldap.role.entity.LdapMapRoleEntityFieldDelegate; import org.keycloak.models.map.storage.ldap.store.LdapMapIdentityStore; import org.keycloak.models.map.storage.ldap.config.LdapMapConfig; -import org.keycloak.models.map.storage.ldap.LdapMapKeycloakTransaction; +import org.keycloak.models.map.storage.ldap.LdapMapStorage; import org.keycloak.models.map.storage.ldap.model.LdapMapDn; import org.keycloak.models.map.storage.ldap.model.LdapMapObject; import org.keycloak.models.map.storage.ldap.model.LdapMapQuery; @@ -53,9 +53,8 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.keycloak.models.map.storage.ldap.role.config.LdapMapRoleMapperConfig.COMMON_ROLES_DN; -public class LdapRoleMapKeycloakTransaction extends LdapMapKeycloakTransaction implements Provider { +public class LdapRoleMapStorage extends LdapMapStorage implements Provider { private final StringKeyConverter keyConverter = new StringKeyConverter.StringKey(); private final Set deletedKeys = new HashSet<>(); @@ -63,7 +62,7 @@ public class LdapRoleMapKeycloakTransaction extends LdapMapKeycloakTransaction { private final LdapMapObject ldapMapObject; private final LdapMapRoleMapperConfig roleMapperConfig; - private final LdapRoleMapKeycloakTransaction transaction; + private final LdapRoleMapStorage store; private final String clientId; private static final EnumMap> SETTERS = new EnumMap<>(MapRoleEntityFields.class); @@ -83,19 +83,19 @@ public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldD REMOVERS.put(MapRoleEntityFields.COMPOSITE_ROLES, (e, v) -> { e.removeCompositeRole((String) v); return null; }); } - public LdapRoleEntity(DeepCloner cloner, LdapMapRoleMapperConfig roleMapperConfig, LdapRoleMapKeycloakTransaction transaction, String clientId) { + public LdapRoleEntity(DeepCloner cloner, LdapMapRoleMapperConfig roleMapperConfig, LdapRoleMapStorage store, String clientId) { ldapMapObject = new LdapMapObject(); ldapMapObject.setObjectClasses(Arrays.asList("top", "groupOfNames")); ldapMapObject.setRdnAttributeName(roleMapperConfig.getRoleNameLdapAttribute()); this.roleMapperConfig = roleMapperConfig; - this.transaction = transaction; + this.store = store; this.clientId = clientId; } - public LdapRoleEntity(LdapMapObject ldapMapObject, LdapMapRoleMapperConfig roleMapperConfig, LdapRoleMapKeycloakTransaction transaction, String clientId) { + public LdapRoleEntity(LdapMapObject ldapMapObject, LdapMapRoleMapperConfig roleMapperConfig, LdapRoleMapStorage store, String clientId) { this.ldapMapObject = ldapMapObject; this.roleMapperConfig = roleMapperConfig; - this.transaction = transaction; + this.store = store; this.clientId = clientId; } @@ -224,7 +224,7 @@ public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldD // TODO: this will not work if users and role use the same! continue; } - String roleId = transaction.readIdByDn(member); + String roleId = store.readIdByDn(member); if (roleId == null) { throw new NotImplementedException(); } @@ -237,7 +237,7 @@ public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldD HashSet translatedCompositeRoles = new HashSet<>(); if (compositeRoles != null) { for (String compositeRole : compositeRoles) { - LdapRoleEntity ldapRole = transaction.readLdap(compositeRole); + LdapRoleEntity ldapRole = store.readLdap(compositeRole); translatedCompositeRoles.add(ldapRole.getLdapMapObject().getDn().toString()); } } @@ -259,7 +259,7 @@ public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldD } public void addCompositeRole(String roleId) { - LdapRoleEntity ldapRole = transaction.readLdap(roleId); + LdapRoleEntity ldapRole = store.readLdap(roleId); Set members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute()); if (members == null) { members = new HashSet<>(); @@ -270,7 +270,7 @@ public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldD } public void removeCompositeRole(String roleId) { - LdapRoleEntity ldapRole = transaction.readLdap(roleId); + LdapRoleEntity ldapRole = store.readLdap(roleId); Set members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute()); if (members == null) { members = new HashSet<>(); diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java index 037999c80a..12e1437bfc 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java @@ -25,7 +25,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; import org.keycloak.models.map.common.TimeAdapter; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -53,25 +52,23 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi private static final Logger LOG = Logger.getLogger(MapRootAuthenticationSessionProvider.class); private final KeycloakSession session; - protected final MapKeycloakTransaction tx; + protected final MapStorage store; private int authSessionsLimit; - private final boolean txHasRealmId; + private final boolean storeHasRealmId; public MapRootAuthenticationSessionProvider(KeycloakSession session, MapStorage sessionStore, int authSessionsLimit) { this.session = session; - this.tx = sessionStore.createTransaction(session); + this.store = sessionStore; this.authSessionsLimit = authSessionsLimit; - - session.getTransactionManager().enlistAfterCompletion(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm) { return origEntity -> { if (isExpired(origEntity, true)) { - txInRealm(realm).delete(origEntity.getId()); + storeWithRealm(realm).delete(origEntity.getId()); return null; } else { return new MapRootAuthenticationSessionAdapter(session, realm, origEntity, authSessionsLimit); @@ -79,11 +76,11 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi }; } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private Predicate entityRealmFilter(String realmId) { @@ -115,11 +112,11 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi int authSessionLifespanSeconds = getAuthSessionLifespan(realm); entity.setExpiration(timestamp + TimeAdapter.fromSecondsToMilliseconds(authSessionLifespanSeconds)); - if (id != null && txInRealm(realm).exists(id)) { + if (id != null && storeWithRealm(realm).exists(id)) { throw new ModelDuplicateException("Root authentication session exists: " + entity.getId()); } - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entityToAdapterFunc(realm).apply(entity); } @@ -133,7 +130,7 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi LOG.tracef("getRootAuthenticationSession(%s, %s)%s", realm.getName(), authenticationSessionId, getShortStackTrace()); - MapRootAuthenticationSessionEntity entity = txInRealm(realm).read(authenticationSessionId); + MapRootAuthenticationSessionEntity entity = storeWithRealm(realm).read(authenticationSessionId); return (entity == null || !entityRealmFilter(realm.getId()).test(entity)) ? null : entityToAdapterFunc(realm).apply(entity); @@ -142,7 +139,7 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi @Override public void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession) { Objects.requireNonNull(authenticationSession, "The provided root authentication session can't be null!"); - txInRealm(realm).delete(authenticationSession.getId()); + storeWithRealm(realm).delete(authenticationSession.getId()); } @Override @@ -163,7 +160,7 @@ public class MapRootAuthenticationSessionProvider implements AuthenticationSessi DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java index aa309b87bd..e03aa48c85 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java @@ -67,7 +67,7 @@ public class MapRootAuthenticationSessionProviderFactory extends AbstractMapProv @Override public MapRootAuthenticationSessionProvider createNew(KeycloakSession session) { - return new MapRootAuthenticationSessionProvider(session, getStorage(session), authSessionsLimit); + return new MapRootAuthenticationSessionProvider(session, getMapStorage(session), authSessionsLimit); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java index 0a078d04f1..542d000209 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStore.java @@ -24,7 +24,6 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.store.StoreFactory; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.MapPolicyEntity; import org.keycloak.models.map.authorization.entity.MapResourceEntity; @@ -45,14 +44,17 @@ public class MapAuthorizationStore implements StoreFactory { private final MapPermissionTicketStore permissionTicketStore; private boolean readOnly; - public MapAuthorizationStore(KeycloakSession session, MapStorage permissionTicketStore, - MapStorage policyStore, MapStorage resourceServerStore, - MapStorage resourceStore, MapStorage scopeStore, AuthorizationProvider provider) { - this.permissionTicketStore = new MapPermissionTicketStore(session, permissionTicketStore, provider); - this.policyStore = new MapPolicyStore(session, policyStore, provider); - this.resourceServerStore = new MapResourceServerStore(session, resourceServerStore, provider); - this.resourceStore = new MapResourceStore(session, resourceStore, provider); - this.scopeStore = new MapScopeStore(session, scopeStore, provider); + public MapAuthorizationStore(MapStorage permissionTicketStore, + MapStorage policyStore, + MapStorage resourceServerStore, + MapStorage resourceStore, + MapStorage scopeStore, + AuthorizationProvider provider) { + this.permissionTicketStore = new MapPermissionTicketStore(permissionTicketStore, provider); + this.policyStore = new MapPolicyStore(policyStore, provider); + this.resourceServerStore = new MapResourceServerStore(resourceServerStore, provider); + this.resourceStore = new MapResourceStore(resourceStore, provider); + this.scopeStore = new MapScopeStore(scopeStore, provider); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java index df2547db29..3b587e9159 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapAuthorizationStoreFactory.java @@ -64,13 +64,13 @@ public class MapAuthorizationStoreFactory implements AmphibianProviderFactory permissionTicketStore = mapStorageProvider.getStorage(PermissionTicket.class); - MapStorage policyStore = mapStorageProvider.getStorage(Policy.class); - MapStorage resourceServerStore = mapStorageProvider.getStorage(ResourceServer.class); - MapStorage resourceStore = mapStorageProvider.getStorage(Resource.class); - MapStorage scopeStore = mapStorageProvider.getStorage(Scope.class); + MapStorage permissionTicketStore = mapStorageProvider.getMapStorage(PermissionTicket.class); + MapStorage policyStore = mapStorageProvider.getMapStorage(Policy.class); + MapStorage resourceServerStore = mapStorageProvider.getMapStorage(ResourceServer.class); + MapStorage resourceStore = mapStorageProvider.getMapStorage(Resource.class); + MapStorage scopeStore = mapStorageProvider.getMapStorage(Scope.class); - authzStore = new MapAuthorizationStore(session, + authzStore = new MapAuthorizationStore( permissionTicketStore, policyStore, resourceServerStore, diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java index 732c0d21b5..98c31d8816 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPermissionTicketStore.java @@ -29,14 +29,12 @@ import org.keycloak.authorization.store.PermissionTicketStore; import org.keycloak.authorization.store.ResourceServerStore; import org.keycloak.authorization.store.ResourceStore; import org.keycloak.common.util.Time; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.map.authorization.adapter.MapPermissionTicketAdapter; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -60,25 +58,24 @@ public class MapPermissionTicketStore implements PermissionTicketStore { private static final Logger LOG = Logger.getLogger(MapPermissionTicketStore.class); private final AuthorizationProvider authorizationProvider; - final MapKeycloakTransaction tx; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; - public MapPermissionTicketStore(KeycloakSession session, MapStorage permissionTicketStore, AuthorizationProvider provider) { + public MapPermissionTicketStore(MapStorage permissionTicketStore, AuthorizationProvider provider) { this.authorizationProvider = provider; - this.tx = permissionTicketStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = permissionTicketStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm, ResourceServer resourceServer) { return origEntity -> new MapPermissionTicketAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory()); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private DefaultModelCriteria forRealmAndResourceServer(RealmModel realm, ResourceServer resourceServer) { @@ -100,7 +97,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { ); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).getCount(withCriteria(mcb)); + return storeWithRealm(realm).getCount(withCriteria(mcb)); } @Override @@ -121,7 +118,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { mcb = mcb.compare(SearchableFields.SCOPE_ID, Operator.EQ, scope.getId()); } - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("Permission ticket for resource server: '" + resourceServer.getId() + ", Resource: " + resource + ", owner: " + owner + ", scopeId: " + scope + " already exists."); } @@ -139,7 +136,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { entity.setResourceServerId(resourceServer.getId()); entity.setRealmId(realm.getId()); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entity == null ? null : entityToAdapterFunc(realm, resourceServer).apply(entity); } @@ -151,7 +148,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { PermissionTicket permissionTicket = findById(realm, null, id); if (permissionTicket == null) return; - txInRealm(realm).delete(id); + storeWithRealm(realm).delete(id); UserManagedPermissionUtil.removePolicy(permissionTicket, authorizationProvider.getStoreFactory()); } @@ -161,7 +158,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { if (id == null) return null; - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.ID, Operator.EQ, id))) .findFirst() .map(entityToAdapterFunc(realm, resourceServer)) @@ -174,7 +171,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.RESOURCE_ID, Operator.EQ, resource.getId()))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); @@ -186,7 +183,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.SCOPE_ID, Operator.EQ, scope.getId()))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); @@ -216,7 +213,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { .toArray(DefaultModelCriteria[]::new) ); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResult, SearchableFields.ID)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResult, SearchableFields.ID)) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -299,7 +296,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { .findById(realm, resourceServerStore.findById(realm, ticket.getResourceServerId()), ticket.getResourceId()); } - return paginatedStream(txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.RESOURCE_ID, ASCENDING)) + return paginatedStream(storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.RESOURCE_ID, ASCENDING)) .filter(distinctByKey(MapPermissionTicketEntity::getResourceId)) .map(ticketResourceMapper) .filter(Objects::nonNull), first, max) @@ -315,7 +312,7 @@ public class MapPermissionTicketStore implements PermissionTicketStore { ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore(); ResourceServerStore resourceServerStore = authorizationProvider.getStoreFactory().getResourceServerStore(); - return paginatedStream(txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.RESOURCE_ID, ASCENDING)) + return paginatedStream(storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.RESOURCE_ID, ASCENDING)) .filter(distinctByKey(MapPermissionTicketEntity::getResourceId)), firstResult, maxResults) .map(ticket -> resourceStore.findById(realm, resourceServerStore.findById(realm, ticket.getResourceServerId()), ticket.getResourceId())) .collect(Collectors.toList()); @@ -327,12 +324,12 @@ public class MapPermissionTicketStore implements PermissionTicketStore { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } public void preRemove(RealmModel realm, ResourceServer resourceServer) { LOG.tracef("preRemove(%s, %s)%s", realm, resourceServer, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); + storeWithRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java index ac04bd1666..907c2ec76e 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapPolicyStore.java @@ -25,14 +25,12 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.store.PolicyStore; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.map.authorization.adapter.MapPolicyAdapter; import org.keycloak.models.map.authorization.entity.MapPolicyEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -54,25 +52,24 @@ public class MapPolicyStore implements PolicyStore { private static final Logger LOG = Logger.getLogger(MapPolicyStore.class); private final AuthorizationProvider authorizationProvider; - final MapKeycloakTransaction tx; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; - public MapPolicyStore(KeycloakSession session, MapStorage policyStore, AuthorizationProvider provider) { + public MapPolicyStore(MapStorage policyStore, AuthorizationProvider provider) { this.authorizationProvider = provider; - this.tx = policyStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = policyStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm, ResourceServer resourceServer) { return origEntity -> new MapPolicyAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory()); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private DefaultModelCriteria forRealmAndResourceServer(RealmModel realm, ResourceServer resourceServer) { @@ -94,7 +91,7 @@ public class MapPolicyStore implements PolicyStore { DefaultModelCriteria mcb = forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.NAME, Operator.EQ, representation.getName()); - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("Policy with name '" + representation.getName() + "' for " + resourceServer.getId() + " already exists"); } @@ -106,7 +103,7 @@ public class MapPolicyStore implements PolicyStore { entity.setResourceServerId(resourceServer.getId()); entity.setRealmId(resourceServer.getRealm().getId()); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entity == null ? null : entityToAdapterFunc(realm, resourceServer).apply(entity); } @@ -118,7 +115,7 @@ public class MapPolicyStore implements PolicyStore { Policy policyEntity = findById(realm, null, id); if (policyEntity == null) return; - txInRealm(realm).delete(id); + storeWithRealm(realm).delete(id); } @Override @@ -127,7 +124,7 @@ public class MapPolicyStore implements PolicyStore { if (id == null) return null; - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.ID, Operator.EQ, id))) .findFirst() .map(entityToAdapterFunc(realm, resourceServer)) @@ -139,7 +136,7 @@ public class MapPolicyStore implements PolicyStore { LOG.tracef("findByName(%s, %s)%s", name, resourceServer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.NAME, Operator.EQ, name))) .findFirst() .map(entityToAdapterFunc(realm, resourceServer)) @@ -151,7 +148,7 @@ public class MapPolicyStore implements PolicyStore { LOG.tracef("findByResourceServer(%s)%s", resourceServer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer))) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -171,7 +168,7 @@ public class MapPolicyStore implements PolicyStore { mcb = mcb.compare(SearchableFields.OWNER, Operator.NOT_EXISTS); } - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -189,11 +186,11 @@ public class MapPolicyStore implements PolicyStore { return mcb.compare(name.getSearchableModelField(), Operator.IN, Arrays.asList(value)); case PERMISSION: { mcb = mcb.compare(SearchableFields.TYPE, Operator.IN, Arrays.asList("resource", "scope", "uma")); - + if (!Boolean.parseBoolean(value[0])) { mcb = DefaultModelCriteria.criteria().not(mcb); // TODO: create NOT_IN operator } - + return mcb; } case ANY_OWNER: @@ -202,7 +199,7 @@ public class MapPolicyStore implements PolicyStore { if (value.length != 2) { throw new IllegalArgumentException("Config filter option requires value with two items: [config_name, expected_config_value]"); } - + value[1] = "%" + value[1] + "%"; return mcb.compare(SearchableFields.CONFIG, Operator.LIKE, (Object[]) value); case TYPE: @@ -218,7 +215,7 @@ public class MapPolicyStore implements PolicyStore { public void findByResource(ResourceServer resourceServer, Resource resource, Consumer consumer) { LOG.tracef("findByResource(%s, %s, %s)%s", resourceServer, resource, consumer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.RESOURCE_ID, Operator.EQ, resource.getId()))) .map(entityToAdapterFunc(realm, resourceServer)) .forEach(consumer); @@ -229,7 +226,7 @@ public class MapPolicyStore implements PolicyStore { LOG.tracef("findByResourceType(%s, %s)%s", resourceServer, type, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.CONFIG, Operator.LIKE, (Object[]) new String[]{"defaultResourceType", type}))) .map(entityToAdapterFunc(realm, resourceServer)) .forEach(policyConsumer); @@ -240,7 +237,7 @@ public class MapPolicyStore implements PolicyStore { LOG.tracef("findByScopes(%s, %s)%s", resourceServer, scopes, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.SCOPE_ID, Operator.IN, scopes.stream().map(Scope::getId)))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); @@ -262,7 +259,7 @@ public class MapPolicyStore implements PolicyStore { .compare(SearchableFields.CONFIG, Operator.NOT_EXISTS, (Object[]) new String[] {"defaultResourceType"}); } - txInRealm(realm).read(withCriteria(mcb)).map(entityToAdapterFunc(realm, resourceServer)).forEach(consumer); + storeWithRealm(realm).read(withCriteria(mcb)).map(entityToAdapterFunc(realm, resourceServer)).forEach(consumer); } @Override @@ -270,7 +267,7 @@ public class MapPolicyStore implements PolicyStore { LOG.tracef("findByType(%s, %s)%s", resourceServer, type, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.TYPE, Operator.EQ, type))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); @@ -279,7 +276,7 @@ public class MapPolicyStore implements PolicyStore { @Override public List findDependentPolicies(ResourceServer resourceServer, String id) { RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.ASSOCIATED_POLICY_ID, Operator.EQ, id))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); @@ -291,12 +288,12 @@ public class MapPolicyStore implements PolicyStore { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } public void preRemove(RealmModel realm, ResourceServer resourceServer) { LOG.tracef("preRemove(%s, %s)%s", realm, resourceServer, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); + storeWithRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java index 5f91f43a64..2edb3bf6d6 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceServerStore.java @@ -23,7 +23,6 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer.SearchableFields; import org.keycloak.authorization.store.ResourceServerStore; import org.keycloak.models.ClientModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; @@ -31,7 +30,6 @@ import org.keycloak.models.map.authorization.adapter.MapResourceServerAdapter; import org.keycloak.models.map.authorization.entity.MapResourceServerEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -50,25 +48,24 @@ public class MapResourceServerStore implements ResourceServerStore { private static final Logger LOG = Logger.getLogger(MapResourceServerStore.class); private final AuthorizationProvider authorizationProvider; - final MapKeycloakTransaction tx; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; - public MapResourceServerStore(KeycloakSession session, MapStorage resourceServerStore, AuthorizationProvider provider) { - this.tx = resourceServerStore.createTransaction(session); + public MapResourceServerStore(MapStorage resourceServerStore, AuthorizationProvider provider) { this.authorizationProvider = provider; - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = resourceServerStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realmModel) { return origEntity -> new MapResourceServerAdapter(realmModel, origEntity, authorizationProvider.getStoreFactory()); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } @Override @@ -91,7 +88,7 @@ public class MapResourceServerStore implements ResourceServerStore { entity.setClientId(clientId); entity.setRealmId(realm.getId()); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entity == null ? null : entityToAdapterFunc(realm).apply(entity); } @@ -105,7 +102,7 @@ public class MapResourceServerStore implements ResourceServerStore { final RealmModel realm = client.getRealm(); authorizationProvider.getKeycloakSession().invalidate(RESOURCE_SERVER_BEFORE_REMOVE, realm, resourceServer); - txInRealm(realm).delete(resourceServer.getId()); + storeWithRealm(realm).delete(resourceServer.getId()); authorizationProvider.getKeycloakSession().invalidate(RESOURCE_SERVER_AFTER_REMOVE, resourceServer); } @@ -118,7 +115,7 @@ public class MapResourceServerStore implements ResourceServerStore { return null; } - MapResourceServerEntity entity = txInRealm(realm).read(id); + MapResourceServerEntity entity = storeWithRealm(realm).read(id); return (entity == null || !Objects.equals(realm.getId(), entity.getRealmId())) ? null : entityToAdapterFunc(realm).apply(entity); } @@ -131,7 +128,7 @@ public class MapResourceServerStore implements ResourceServerStore { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, client.getRealm().getId()); final RealmModel realm = client.getRealm(); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(entityToAdapterFunc(client.getRealm())) .findFirst() .orElse(null); @@ -143,6 +140,6 @@ public class MapResourceServerStore implements ResourceServerStore { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java index 7215946f2d..5b1cc1e0f7 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapResourceStore.java @@ -24,14 +24,12 @@ import org.keycloak.authorization.model.Resource.SearchableFields; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.store.ResourceStore; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.map.authorization.adapter.MapResourceAdapter; import org.keycloak.models.map.authorization.entity.MapResourceEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -52,27 +50,24 @@ public class MapResourceStore implements ResourceStore { private static final Logger LOG = Logger.getLogger(MapResourceStore.class); private final AuthorizationProvider authorizationProvider; - final MapKeycloakTransaction tx; - private final KeycloakSession session; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; - public MapResourceStore(KeycloakSession session, MapStorage resourceStore, AuthorizationProvider provider) { - this.tx = resourceStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - authorizationProvider = provider; - this.session = session; - this.txHasRealmId = tx instanceof HasRealmId; + public MapResourceStore(MapStorage resourceStore, AuthorizationProvider provider) { + this.authorizationProvider = provider; + this.store = resourceStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm, final ResourceServer resourceServer) { return origEntity -> new MapResourceAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory()); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private DefaultModelCriteria forRealmAndResourceServer(RealmModel realm, ResourceServer resourceServer) { @@ -95,7 +90,7 @@ public class MapResourceStore implements ResourceStore { .compare(SearchableFields.NAME, Operator.EQ, name) .compare(SearchableFields.OWNER, Operator.EQ, owner); - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("Resource with name '" + name + "' for " + resourceServer.getId() + " already exists for request owner " + owner); } @@ -106,7 +101,7 @@ public class MapResourceStore implements ResourceStore { entity.setOwner(owner); entity.setRealmId(realm.getId()); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entity == null ? null : entityToAdapterFunc(realm, resourceServer).apply(entity); } @@ -117,7 +112,7 @@ public class MapResourceStore implements ResourceStore { Resource resource = findById(realm, null, id); if (resource == null) return; - txInRealm(realm).delete(id); + storeWithRealm(realm).delete(id); } @Override @@ -126,7 +121,7 @@ public class MapResourceStore implements ResourceStore { if (id == null) return null; - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.ID, Operator.EQ, id))) .findFirst() .map(entityToAdapterFunc(realm, resourceServer)) @@ -137,7 +132,7 @@ public class MapResourceStore implements ResourceStore { public void findByOwner(RealmModel realm, ResourceServer resourceServer, String ownerId, Consumer consumer) { LOG.tracef("findByOwner(%s, %s, %s)%s", realm, resourceServer, resourceServer, ownerId, getShortStackTrace()); - txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.OWNER, Operator.EQ, ownerId))) .map(entityToAdapterFunc(realm, resourceServer)) .forEach(consumer); @@ -148,7 +143,7 @@ public class MapResourceStore implements ResourceStore { LOG.tracef("findByResourceServer(%s)%s", resourceServer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer))) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -162,7 +157,7 @@ public class MapResourceStore implements ResourceStore { .toArray(DefaultModelCriteria[]::new) ); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -199,7 +194,7 @@ public class MapResourceStore implements ResourceStore { LOG.tracef("findByScope(%s, %s, %s)%s", scopes, resourceServer, consumer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.SCOPE_ID, Operator.IN, scopes.stream().map(Scope::getId)))) .map(entityToAdapterFunc(realm, resourceServer)) .forEach(consumer); @@ -210,7 +205,7 @@ public class MapResourceStore implements ResourceStore { LOG.tracef("findByName(%s, %s, %s)%s", name, ownerId, resourceServer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.OWNER, Operator.EQ, ownerId) .compare(SearchableFields.NAME, Operator.EQ, name))) .findFirst() @@ -223,7 +218,7 @@ public class MapResourceStore implements ResourceStore { LOG.tracef("findByType(%s, %s, %s)%s", type, resourceServer, consumer, getShortStackTrace()); RealmModel realm = authorizationProvider.getRealm(); - txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.TYPE, Operator.EQ, type))) .map(entityToAdapterFunc(realm, resourceServer)) .forEach(consumer); @@ -241,7 +236,7 @@ public class MapResourceStore implements ResourceStore { mcb = mcb.compare(SearchableFields.OWNER, Operator.EQ, owner); } - txInRealm(realm).read(withCriteria(mcb)) + storeWithRealm(realm).read(withCriteria(mcb)) .map(entityToAdapterFunc(realm, resourceServer)) .forEach(consumer); } @@ -250,7 +245,7 @@ public class MapResourceStore implements ResourceStore { public void findByTypeInstance(ResourceServer resourceServer, String type, Consumer consumer) { LOG.tracef("findByTypeInstance(%s, %s, %s)%s", type, resourceServer, consumer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.OWNER, Operator.NE, resourceServer.getClientId()) .compare(SearchableFields.TYPE, Operator.EQ, type))) .map(entityToAdapterFunc(realm, resourceServer)) @@ -263,12 +258,12 @@ public class MapResourceStore implements ResourceStore { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } public void preRemove(RealmModel realm, ResourceServer resourceServer) { LOG.tracef("preRemove(%s, %s)%s", realm, resourceServer, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); + storeWithRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java b/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java index be5ef5d12a..7e6c54e14f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java +++ b/model/map/src/main/java/org/keycloak/models/map/authorization/MapScopeStore.java @@ -23,14 +23,12 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.model.Scope.SearchableFields; import org.keycloak.authorization.store.ScopeStore; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.map.authorization.adapter.MapScopeAdapter; import org.keycloak.models.map.authorization.entity.MapScopeEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -49,27 +47,24 @@ public class MapScopeStore implements ScopeStore { private static final Logger LOG = Logger.getLogger(MapScopeStore.class); private final AuthorizationProvider authorizationProvider; - final MapKeycloakTransaction tx; - private final KeycloakSession session; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; - public MapScopeStore(KeycloakSession session, MapStorage scopeStore, AuthorizationProvider provider) { + public MapScopeStore(MapStorage scopeStore, AuthorizationProvider provider) { this.authorizationProvider = provider; - this.tx = scopeStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.session = session; - this.txHasRealmId = tx instanceof HasRealmId; + this.store = scopeStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm, ResourceServer resourceServer) { return origEntity -> new MapScopeAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory()); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private DefaultModelCriteria forRealmAndResourceServer(RealmModel realm, ResourceServer resourceServer) { @@ -91,7 +86,7 @@ public class MapScopeStore implements ScopeStore { DefaultModelCriteria mcb = forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.NAME, Operator.EQ, name); - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("Scope with name '" + name + "' for " + resourceServer.getId() + " already exists"); } @@ -101,7 +96,7 @@ public class MapScopeStore implements ScopeStore { entity.setResourceServerId(resourceServer.getId()); entity.setRealmId(resourceServer.getRealm().getId()); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entity == null ? null : entityToAdapterFunc(realm, resourceServer).apply(entity); } @@ -112,7 +107,7 @@ public class MapScopeStore implements ScopeStore { Scope scope = findById(realm, null, id); if (scope == null) return; - txInRealm(realm).delete(id); + storeWithRealm(realm).delete(id); } @Override @@ -121,7 +116,7 @@ public class MapScopeStore implements ScopeStore { if (id == null) return null; - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer) .compare(SearchableFields.ID, Operator.EQ, id))) .findFirst() .map(entityToAdapterFunc(realm, resourceServer)) @@ -133,7 +128,7 @@ public class MapScopeStore implements ScopeStore { LOG.tracef("findByName(%s, %s)%s", name, resourceServer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer).compare(SearchableFields.NAME, + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer).compare(SearchableFields.NAME, Operator.EQ, name))) .findFirst() .map(entityToAdapterFunc(realm, resourceServer)) @@ -144,7 +139,7 @@ public class MapScopeStore implements ScopeStore { public List findByResourceServer(ResourceServer resourceServer) { LOG.tracef("findByResourceServer(%s)%s", resourceServer, getShortStackTrace()); RealmModel realm = resourceServer.getRealm(); - return txInRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer))) + return storeWithRealm(realm).read(withCriteria(forRealmAndResourceServer(realm, resourceServer))) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -169,7 +164,7 @@ public class MapScopeStore implements ScopeStore { } } - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) .map(entityToAdapterFunc(realm, resourceServer)) .collect(Collectors.toList()); } @@ -180,12 +175,12 @@ public class MapScopeStore implements ScopeStore { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } public void preRemove(RealmModel realm, ResourceServer resourceServer) { LOG.tracef("preRemove(%s, %s)%s", realm, resourceServer, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); + storeWithRealm(realm).delete(withCriteria(forRealmAndResourceServer(resourceServer.getRealm(), resourceServer))); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java index 84a1d43c94..8245c4a520 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java @@ -42,9 +42,8 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; import org.keycloak.models.map.common.TimeAdapter; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import static org.keycloak.common.util.StackUtil.getShortStackTrace; @@ -58,16 +57,15 @@ public class MapClientProvider implements ClientProvider { private static final Logger LOG = Logger.getLogger(MapClientProvider.class); private final KeycloakSession session; - final MapKeycloakTransaction tx; + final MapStorage store; private final ConcurrentMap> clientRegisteredNodesStore; - private final boolean txHasRealmId; + private final boolean storeHasRealmId; public MapClientProvider(KeycloakSession session, MapStorage clientStore, ConcurrentMap> clientRegisteredNodesStore) { this.session = session; this.clientRegisteredNodesStore = clientRegisteredNodesStore; - this.tx = clientStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = clientStore; + this.storeHasRealmId = store instanceof HasRealmId; } private ClientUpdatedEvent clientUpdatedEvent(ClientModel c) { @@ -121,11 +119,11 @@ public class MapClientProvider implements ClientProvider { }; } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private Predicate entityRealmFilter(RealmModel realm) { @@ -141,7 +139,7 @@ public class MapClientProvider implements ClientProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.CLIENT_ID)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.CLIENT_ID)) .map(entityToAdapterFunc(realm)); } @@ -150,7 +148,7 @@ public class MapClientProvider implements ClientProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - return txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.CLIENT_ID, ASCENDING)) + return storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.CLIENT_ID, ASCENDING)) .map(entityToAdapterFunc(realm)); } @@ -158,7 +156,7 @@ public class MapClientProvider implements ClientProvider { public ClientModel addClient(RealmModel realm, String id, String clientId) { LOG.tracef("addClient(%s, %s, %s)%s", realm, id, clientId, getShortStackTrace()); - if (id != null && txInRealm(realm).exists(id)) { + if (id != null && storeWithRealm(realm).exists(id)) { throw new ModelDuplicateException("Client with same id exists: " + id); } if (clientId != null && getClientByClientId(realm, clientId) != null) { @@ -171,7 +169,7 @@ public class MapClientProvider implements ClientProvider { entity.setClientId(clientId); entity.setEnabled(true); entity.setStandardFlowEnabled(true); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); if (clientId == null) { clientId = entity.getId(); entity.setClientId(clientId); @@ -190,7 +188,7 @@ public class MapClientProvider implements ClientProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ALWAYS_DISPLAY_IN_CONSOLE, Operator.EQ, Boolean.TRUE); - return txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.CLIENT_ID, ASCENDING)) + return storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.CLIENT_ID, ASCENDING)) .map(entityToAdapterFunc(realm)); } @@ -215,7 +213,7 @@ public class MapClientProvider implements ClientProvider { session.invalidate(CLIENT_BEFORE_REMOVE, realm, client); - txInRealm(realm).delete(id); + storeWithRealm(realm).delete(id); session.invalidate(CLIENT_AFTER_REMOVE, client); @@ -227,7 +225,7 @@ public class MapClientProvider implements ClientProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - return txInRealm(realm).getCount(withCriteria(mcb)); + return storeWithRealm(realm).getCount(withCriteria(mcb)); } @Override @@ -238,7 +236,7 @@ public class MapClientProvider implements ClientProvider { LOG.tracef("getClientById(%s, %s)%s", realm, id, getShortStackTrace()); - MapClientEntity entity = txInRealm(realm).read(id); + MapClientEntity entity = storeWithRealm(realm).read(id); return (entity == null || ! entityRealmFilter(realm).test(entity)) ? null : entityToAdapterFunc(realm).apply(entity); @@ -255,7 +253,7 @@ public class MapClientProvider implements ClientProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.CLIENT_ID, Operator.EQ, clientId); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(entityToAdapterFunc(realm)) .findFirst() .orElse(null) @@ -272,7 +270,7 @@ public class MapClientProvider implements ClientProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.CLIENT_ID, Operator.ILIKE, "%" + clientId + "%"); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.CLIENT_ID)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.CLIENT_ID)) .map(entityToAdapterFunc(realm)); } @@ -285,14 +283,14 @@ public class MapClientProvider implements ClientProvider { mcb = mcb.compare(SearchableFields.ATTRIBUTE, Operator.EQ, entry.getKey(), entry.getValue()); } - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.CLIENT_ID)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.CLIENT_ID)) .map(entityToAdapterFunc(realm)); } @Override public void addClientScopes(RealmModel realm, ClientModel client, Set clientScopes, boolean defaultScope) { final String id = client.getId(); - MapClientEntity entity = txInRealm(realm).read(id); + MapClientEntity entity = storeWithRealm(realm).read(id); if (entity == null) return; @@ -313,7 +311,7 @@ public class MapClientProvider implements ClientProvider { @Override public void removeClientScope(RealmModel realm, ClientModel client, ClientScopeModel clientScope) { final String id = client.getId(); - MapClientEntity entity = txInRealm(realm).read(id); + MapClientEntity entity = storeWithRealm(realm).read(id); if (entity == null) return; @@ -325,7 +323,7 @@ public class MapClientProvider implements ClientProvider { @Override public Map getClientScopes(RealmModel realm, ClientModel client, boolean defaultScopes) { final String id = client.getId(); - MapClientEntity entity = txInRealm(realm).read(id); + MapClientEntity entity = storeWithRealm(realm).read(id); if (entity == null) return null; @@ -347,7 +345,7 @@ public class MapClientProvider implements ClientProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ENABLED, Operator.EQ, Boolean.TRUE); - try (Stream st = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream st = storeWithRealm(realm).read(withCriteria(mcb))) { return st .filter(mce -> mce.getRedirectUris() != null && ! mce.getRedirectUris().isEmpty()) .collect(Collectors.toMap( @@ -362,7 +360,7 @@ public class MapClientProvider implements ClientProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.SCOPE_MAPPING_ROLE, Operator.EQ, role.getId()); - try (Stream toRemove = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream toRemove = storeWithRealm(realm).read(withCriteria(mcb))) { toRemove .forEach(clientEntity -> clientEntity.removeScopeMapping(role.getId())); } @@ -373,7 +371,7 @@ public class MapClientProvider implements ClientProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProviderFactory.java index 14ad9193c7..82df00ad31 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProviderFactory.java @@ -25,7 +25,6 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.provider.InvalidationHandler; -import org.keycloak.provider.InvalidationHandler.InvalidableObjectType; import static org.keycloak.models.map.common.AbstractMapProviderFactory.MapProviderObjectType.CLIENT_AFTER_REMOVE; import static org.keycloak.models.map.common.AbstractMapProviderFactory.MapProviderObjectType.REALM_BEFORE_REMOVE; @@ -45,7 +44,7 @@ public class MapClientProviderFactory extends AbstractMapProviderFactory tx; - private final boolean txHasRealmId; + private final MapStorage store; + private final boolean storeHasRealmId; public MapClientScopeProvider(KeycloakSession session, MapStorage clientScopeStore) { this.session = session; - this.tx = clientScopeStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = clientScopeStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm) { @@ -64,11 +62,11 @@ public class MapClientScopeProvider implements ClientScopeProvider { return origEntity -> new MapClientScopeAdapter(session, realm, origEntity); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private Predicate entityRealmFilter(RealmModel realm) { @@ -84,7 +82,7 @@ public class MapClientScopeProvider implements ClientScopeProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - return txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) + return storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) .map(entityToAdapterFunc(realm)); } @@ -94,11 +92,11 @@ public class MapClientScopeProvider implements ClientScopeProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.NAME, Operator.EQ, name); - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("Client scope with name '" + name + "' in realm " + realm.getName()); } - if (id != null && txInRealm(realm).exists(id)) { + if (id != null && storeWithRealm(realm).exists(id)) { throw new ModelDuplicateException("Client scope exists: " + id); } @@ -109,7 +107,7 @@ public class MapClientScopeProvider implements ClientScopeProvider { entity.setRealmId(realm.getId()); entity.setName(KeycloakModelUtils.convertClientScopeName(name)); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entityToAdapterFunc(realm).apply(entity); } @@ -121,7 +119,7 @@ public class MapClientScopeProvider implements ClientScopeProvider { session.invalidate(CLIENT_SCOPE_BEFORE_REMOVE, realm, clientScope); - txInRealm(realm).delete(id); + storeWithRealm(realm).delete(id); session.invalidate(CLIENT_SCOPE_AFTER_REMOVE, clientScope); @@ -146,7 +144,7 @@ public class MapClientScopeProvider implements ClientScopeProvider { LOG.tracef("getClientScopeById(%s, %s)%s", realm, id, getShortStackTrace()); - MapClientScopeEntity entity = txInRealm(realm).read(id); + MapClientScopeEntity entity = storeWithRealm(realm).read(id); return (entity == null || ! entityRealmFilter(realm).test(entity)) ? null : entityToAdapterFunc(realm).apply(entity); @@ -157,7 +155,7 @@ public class MapClientScopeProvider implements ClientScopeProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java index ad35cc55b9..10b3abd1d6 100644 --- a/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/clientscope/MapClientScopeProviderFactory.java @@ -35,7 +35,7 @@ public class MapClientScopeProviderFactory extends AbstractMapProviderFactory modelType; private final Class providerType; @@ -92,13 +94,7 @@ public abstract class AbstractMapProviderFactory getStorage(KeycloakSession session) { + public MapStorage getMapStorage(KeycloakSession session) { ProviderFactory storageProviderFactory = getProviderFactoryOrComponentFactory(session, storageConfigScope); final MapStorageProvider factory = storageProviderFactory.create(session); session.enlistForClose(factory); - return factory.getStorage(modelType); + return factory.getMapStorage(modelType); } public static ProviderFactory getProviderFactoryOrComponentFactory(KeycloakSession session, Scope storageConfigScope) { diff --git a/model/map/src/main/java/org/keycloak/models/map/common/SessionAttributesUtils.java b/model/map/src/main/java/org/keycloak/models/map/common/SessionAttributesUtils.java new file mode 100644 index 0000000000..993a17ce75 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/common/SessionAttributesUtils.java @@ -0,0 +1,114 @@ +/* + * Copyright 2023 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.common; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; +import org.keycloak.provider.Provider; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; + +public class SessionAttributesUtils { + private static final AtomicInteger COUNTER_TX = new AtomicInteger(); + + /** + * Returns a new unique counter across whole Keycloak instance + * + * @return unique number + */ + public static int grabNewFactoryIdentifier() { + return COUNTER_TX.getAndIncrement(); + } + + /** + * Used for creating a provider instance only once within one + * KeycloakSession. + *

+ * Checks whether there already exists a provider withing session + * attributes for given {@code providerClass} and + * {@code factoryIdentifier}. If exists returns existing provider, + * otherwise creates a new instance using {@code createNew} function. + * + * @param session current Keycloak session + * @param factoryIdentifier unique factory identifier. + * {@link SessionAttributesUtils#grabNewFactoryIdentifier()} + * can be used for obtaining new identifiers. + * @param providerClass class of the requested provider + * @param createNew function that creates a new instance of the provider + * @return an instance of the provider either from session attributes or freshly created. + * @param type of the provider + */ + public static T createProviderIfAbsent(KeycloakSession session, + int factoryIdentifier, + Class providerClass, + Function createNew) { + String uniqueKey = providerClass.getName() + factoryIdentifier; + T provider = session.getAttribute(uniqueKey, providerClass); + + if (provider != null) { + return provider; + } + provider = createNew.apply(session); + + session.setAttribute(uniqueKey, provider); + return provider; + } + + /** + * Used for creating a store instance only once within one + * KeycloakSession. + *

+ * Checks whether there already is a store within session attributes + * for given {@code providerClass}, {@code modelType} and + * {@code factoryIdentifier}. If exists returns existing provider, + * otherwise creates a new instance using {@code createNew} supplier. + * + * @param session current Keycloak session + * @param providerType map storage provider class + * @param modelType model class. Can be null if the store is the same + * for all models. + * @param factoryId unique factory identifier. + * {@link SessionAttributesUtils#grabNewFactoryIdentifier()} + * can be used for obtaining new identifiers. + * @param createNew supplier that creates a new instance of the store + * @return an instance of the store either from session attributes or + * freshly created. + * @param entity type + * @param model type + * @param store type + */ + public static > T createMapStorageIfAbsent( + KeycloakSession session, + Class providerType, + Class modelType, + int factoryId, + Supplier createNew) { + String sessionAttributeName = providerType.getName() + "-" + (modelType != null ? modelType.getName() : "") + "-" + factoryId; + + T sessionTransaction = (T) session.getAttribute(sessionAttributeName, MapStorage.class); + if (sessionTransaction == null) { + sessionTransaction = createNew.get(); + session.setAttribute(sessionAttributeName, sessionTransaction); + } + + return sessionTransaction; + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java index e1ea8c3662..1e2cc79283 100644 --- a/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java +++ b/model/map/src/main/java/org/keycloak/models/map/datastore/MapExportImportManager.java @@ -61,7 +61,7 @@ import org.keycloak.models.WebAuthnPolicy; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.AbstractMapProviderFactory; import org.keycloak.models.map.realm.MapRealmEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.models.utils.DefaultAuthenticationFlows; @@ -577,23 +577,23 @@ public class MapExportImportManager implements ExportImportManager { } private void copyRealm(String realmId, KeycloakSession sessionChm) { - MapRealmEntity realmEntityChm = (MapRealmEntity) getTransaction(sessionChm, RealmProvider.class).read(realmId); - getTransaction(session, RealmProvider.class).create(realmEntityChm); + MapRealmEntity realmEntityChm = (MapRealmEntity) getMapStorage(sessionChm, RealmProvider.class).read(realmId); + getMapStorage(session, RealmProvider.class).create(realmEntityChm); } - private static

MapKeycloakTransaction getTransaction(KeycloakSession session, Class

provider) { + private static

MapStorage getMapStorage(KeycloakSession session, Class

provider) { ProviderFactory

factoryChm = session.getKeycloakSessionFactory().getProviderFactory(provider); - return ((AbstractMapProviderFactory) factoryChm).getStorage(session).createTransaction(session); + return ((AbstractMapProviderFactory) factoryChm).getMapStorage(session); } private

void copyEntities(String realmId, KeycloakSession sessionChm, Class

provider, Class model, SearchableModelField field) { - MapKeycloakTransaction txChm = getTransaction(sessionChm, provider); - MapKeycloakTransaction txOrig = getTransaction(session, provider); + MapStorage storeChm = getMapStorage(sessionChm, provider); + MapStorage storeOrig = getMapStorage(session, provider); DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(field, ModelCriteriaBuilder.Operator.EQ, realmId); - txChm.read(withCriteria(mcb)).forEach(txOrig::create); + storeChm.read(withCriteria(mcb)).forEach(storeOrig::create); } private static void fillRealm(KeycloakSession session, String id, RealmRepresentation rep) { diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java index b08561122d..2998250e59 100644 --- a/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProvider.java @@ -29,7 +29,6 @@ import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.map.common.ExpirableEntity; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder; import org.keycloak.models.map.storage.QueryParameters; @@ -45,41 +44,38 @@ public class MapEventStoreProvider implements EventStoreProvider { private static final Logger LOG = Logger.getLogger(MapEventStoreProvider.class); private final KeycloakSession session; - private final MapKeycloakTransaction authEventsTX; - private final MapKeycloakTransaction adminEventsTX; + private final MapStorage authEventsTX; + private final MapStorage adminEventsTX; private final boolean adminTxHasRealmId; private final boolean authTxHasRealmId; public MapEventStoreProvider(KeycloakSession session, MapStorage loginEventsStore, MapStorage adminEventsStore) { this.session = session; - this.authEventsTX = loginEventsStore.createTransaction(session); - this.adminEventsTX = adminEventsStore.createTransaction(session); - - session.getTransactionManager().enlistAfterCompletion(this.authEventsTX); - session.getTransactionManager().enlistAfterCompletion(this.adminEventsTX); + this.authEventsTX = loginEventsStore; + this.adminEventsTX = adminEventsStore; this.authTxHasRealmId = this.authEventsTX instanceof HasRealmId; this.adminTxHasRealmId = this.adminEventsTX instanceof HasRealmId; } - private MapKeycloakTransaction adminTxInRealm(String realmId) { + private MapStorage adminTxInRealm(String realmId) { if (adminTxHasRealmId) { ((HasRealmId) adminEventsTX).setRealmId(realmId); } return adminEventsTX; } - private MapKeycloakTransaction adminTxInRealm(RealmModel realm) { + private MapStorage adminTxInRealm(RealmModel realm) { return adminTxInRealm(realm == null ? null : realm.getId()); } - private MapKeycloakTransaction authTxInRealm(String realmId) { + private MapStorage authTxInRealm(String realmId) { if (authTxHasRealmId) { ((HasRealmId) authEventsTX).setRealmId(realmId); } return authEventsTX; } - private MapKeycloakTransaction authTxInRealm(RealmModel realm) { + private MapStorage authTxInRealm(RealmModel realm) { return authTxInRealm(realm == null ? null : realm.getId()); } diff --git a/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java index 5833362eb4..42fedca80a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/events/MapEventStoreProviderFactory.java @@ -60,10 +60,10 @@ public class MapEventStoreProviderFactory implements AmphibianProviderFactory adminEventsStore = factoryAe.getStorage(AdminEvent.class); + MapStorage adminEventsStore = factoryAe.getMapStorage(AdminEvent.class); final MapStorageProvider factoryLe = AbstractMapProviderFactory.getProviderFactoryOrComponentFactory(session, storageConfigScopeLoginEvents).create(session); - MapStorage loginEventsStore = factoryLe.getStorage(Event.class); + MapStorage loginEventsStore = factoryLe.getMapStorage(Event.class); provider = new MapEventStoreProvider(session, loginEventsStore, adminEventsStore); session.setAttribute(uniqueKey, provider); diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java index 99731bf1b5..a4c32b7734 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java @@ -27,7 +27,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; @@ -53,21 +52,20 @@ public class MapGroupProvider implements GroupProvider { private static final Logger LOG = Logger.getLogger(MapGroupProvider.class); private final KeycloakSession session; - final MapKeycloakTransaction tx; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; public MapGroupProvider(KeycloakSession session, MapStorage groupStore) { this.session = session; - this.tx = groupStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = groupStore; + this.storeHasRealmId = store instanceof HasRealmId; } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private Function entityToAdapterFunc(RealmModel realm) { @@ -89,7 +87,7 @@ public class MapGroupProvider implements GroupProvider { LOG.tracef("getGroupById(%s, %s)%s", realm, id, getShortStackTrace()); String realmId = realm.getId(); - MapGroupEntity entity = txInRealm(realm).read(id); + MapGroupEntity entity = storeWithRealm(realm).read(id); return (entity == null || ! Objects.equals(realmId, entity.getRealmId())) ? null : entityToAdapterFunc(realm).apply(entity); @@ -114,7 +112,7 @@ public class MapGroupProvider implements GroupProvider { queryParameters = queryParametersModifier.apply(queryParameters); } - return txInRealm(realm).read(queryParameters) + return storeWithRealm(realm).read(queryParameters) .map(entityToAdapterFunc(realm)) ; } @@ -129,7 +127,7 @@ public class MapGroupProvider implements GroupProvider { mcb = mcb.compare(SearchableFields.NAME, Operator.ILIKE, "%" + search + "%"); } - return txInRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -143,7 +141,7 @@ public class MapGroupProvider implements GroupProvider { mcb = mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS); } - return txInRealm(realm).getCount(withCriteria(mcb)); + return storeWithRealm(realm).getCount(withCriteria(mcb)); } @Override @@ -193,7 +191,7 @@ public class MapGroupProvider implements GroupProvider { } - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) .map(MapGroupEntity::getId) .map(id -> { GroupModel groupById = session.groups().getGroupById(realm, id); @@ -212,7 +210,7 @@ public class MapGroupProvider implements GroupProvider { mcb = mcb.compare(GroupModel.SearchableFields.ATTRIBUTE, Operator.EQ, entry.getKey(), entry.getValue()); } - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -228,7 +226,7 @@ public class MapGroupProvider implements GroupProvider { mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS) : mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, toParent.getId()); - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("Group with name '" + name + "' in realm " + realm.getName() + " already exists for requested parent" ); } @@ -237,10 +235,10 @@ public class MapGroupProvider implements GroupProvider { entity.setRealmId(realm.getId()); entity.setName(name); entity.setParentId(toParent == null ? null : toParent.getId()); - if (id != null && txInRealm(realm).exists(id)) { + if (id != null && storeWithRealm(realm).exists(id)) { throw new ModelDuplicateException("Group exists: " + id); } - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entityToAdapterFunc(realm).apply(entity); } @@ -252,7 +250,7 @@ public class MapGroupProvider implements GroupProvider { session.invalidate(GROUP_BEFORE_REMOVE, realm, group); - txInRealm(realm).delete(group.getId()); + storeWithRealm(realm).delete(group.getId()); session.invalidate(GROUP_AFTER_REMOVE, realm, group); @@ -279,7 +277,7 @@ public class MapGroupProvider implements GroupProvider { mcb.compare(SearchableFields.PARENT_ID, Operator.NOT_EXISTS) : mcb.compare(SearchableFields.PARENT_ID, Operator.EQ, toParent.getId()); - try (Stream possibleSiblings = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream possibleSiblings = storeWithRealm(realm).read(withCriteria(mcb))) { if (possibleSiblings.findAny().isPresent()) { throw new ModelDuplicateException("Parent already contains subgroup named '" + group.getName() + "'"); } @@ -328,7 +326,7 @@ public class MapGroupProvider implements GroupProvider { .compare(SearchableFields.PARENT_ID, Operator.EQ, (Object) null) .compare(SearchableFields.NAME, Operator.EQ, subGroup.getName()); - try (Stream possibleSiblings = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream possibleSiblings = storeWithRealm(realm).read(withCriteria(mcb))) { if (possibleSiblings.findAny().isPresent()) { throw new ModelDuplicateException("There is already a top level group named '" + subGroup.getName() + "'"); } @@ -342,7 +340,7 @@ public class MapGroupProvider implements GroupProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ASSIGNED_ROLE, Operator.EQ, role.getId()); - try (Stream toRemove = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream toRemove = storeWithRealm(realm).read(withCriteria(mcb))) { toRemove .map(groupEntity -> session.groups().getGroupById(realm, groupEntity.getId())) .forEach(groupModel -> groupModel.deleteRoleMapping(role)); @@ -354,7 +352,7 @@ public class MapGroupProvider implements GroupProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override @@ -368,6 +366,6 @@ public class MapGroupProvider implements GroupProvider { .compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.PARENT_ID, Operator.EQ, parentId); - return txInRealm(realm).read(withCriteria(mcb)).map(entityToAdapterFunc(realm)); + return storeWithRealm(realm).read(withCriteria(mcb)).map(entityToAdapterFunc(realm)); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProviderFactory.java index a7133304f7..a2d95eb1ef 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProviderFactory.java @@ -42,7 +42,7 @@ public class MapGroupProviderFactory extends AbstractMapProviderFactory tx; + private MapStorage store; /** * The lockStoreSupplier allows the store to be initialized lazily and only when needed: As this provider is initialized @@ -120,9 +119,8 @@ public class MapGlobalLockProvider implements GlobalLockProvider { } private void prepareTx() { - if (tx == null) { - this.tx = lockStoreSupplier.get().createTransaction(session); - session.getTransactionManager().enlist(tx); + if (store == null) { + this.store = lockStoreSupplier.get(); } } @@ -140,14 +138,14 @@ public class MapGlobalLockProvider implements GlobalLockProvider { prepareTx(); DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(MapLockEntity.SearchableFields.NAME, ModelCriteriaBuilder.Operator.EQ, lockName); - Optional entry = tx.read(QueryParameters.withCriteria(mcb)).findFirst(); + Optional entry = store.read(QueryParameters.withCriteria(mcb)).findFirst(); if (entry.isEmpty()) { MapLockEntity entity = DeepCloner.DUMB_CLONER.newInstance(MapLockEntity.class); entity.setName(lockName); entity.setKeycloakInstanceIdentifier(getKeycloakInstanceIdentifier()); entity.setTimeAcquired(Time.currentTimeMillis()); - return tx.create(entity); + return store.create(entity); } else { throw new LockAcquiringTimeoutException(lockName, entry.get().getKeycloakInstanceIdentifier(), Instant.ofEpochMilli(entry.get().getTimeAcquired())); } @@ -159,7 +157,7 @@ public class MapGlobalLockProvider implements GlobalLockProvider { */ private void unlock(MapLockEntity lockEntity) { prepareTx(); - MapLockEntity readLockEntity = tx.read(lockEntity.getId()); + MapLockEntity readLockEntity = store.read(lockEntity.getId()); if (readLockEntity == null) { throw new RuntimeException("didn't find lock - someone else unlocked it?"); @@ -168,14 +166,14 @@ public class MapGlobalLockProvider implements GlobalLockProvider { throw new RuntimeException(String.format("Lock owned by different instance: Lock [%s] acquired by keycloak instance [%s] at the time [%s]", readLockEntity.getName(), readLockEntity.getKeycloakInstanceIdentifier(), readLockEntity.getTimeAcquired())); } else { - tx.delete(readLockEntity.getId()); + store.delete(readLockEntity.getId()); } } private void releaseAllLocks() { prepareTx(); DefaultModelCriteria mcb = criteria(); - tx.delete(QueryParameters.withCriteria(mcb)); + store.delete(QueryParameters.withCriteria(mcb)); } private static String getKeycloakInstanceIdentifier() { diff --git a/model/map/src/main/java/org/keycloak/models/map/lock/MapGlobalLockProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/lock/MapGlobalLockProviderFactory.java index 04e4020bfb..be47b4a925 100644 --- a/model/map/src/main/java/org/keycloak/models/map/lock/MapGlobalLockProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/lock/MapGlobalLockProviderFactory.java @@ -47,7 +47,7 @@ public class MapGlobalLockProviderFactory extends AbstractMapProviderFactory getStorage(session)); + return new MapGlobalLockProvider(session, defaultTimeoutMilliseconds, () -> getMapStorage(session)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java index ea02d676ac..2c5a053b3b 100644 --- a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProvider.java @@ -22,7 +22,6 @@ import org.keycloak.models.UserLoginFailureProvider; import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.map.common.DeepCloner; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; @@ -40,13 +39,11 @@ public class MapUserLoginFailureProvider implements UserLoginFailureProvider { private static final Logger LOG = Logger.getLogger(MapUserLoginFailureProvider.class); private final KeycloakSession session; - protected final MapKeycloakTransaction userLoginFailureTx; + protected final MapStorage userLoginFailureTx; public MapUserLoginFailureProvider(KeycloakSession session, MapStorage userLoginFailureStore) { this.session = session; - - userLoginFailureTx = userLoginFailureStore.createTransaction(session); - session.getTransactionManager().enlistAfterCompletion(userLoginFailureTx); + this.userLoginFailureTx = userLoginFailureStore; } private Function userLoginFailureEntityToAdapterFunc(RealmModel realm) { diff --git a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java index 63b4c9a70e..c4ad13ed3d 100644 --- a/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/loginFailure/MapUserLoginFailureProviderFactory.java @@ -39,7 +39,7 @@ public class MapUserLoginFailureProviderFactory extends AbstractMapProviderFacto @Override public MapUserLoginFailureProvider createNew(KeycloakSession session) { - return new MapUserLoginFailureProvider(session, getStorage(session)); + return new MapUserLoginFailureProvider(session, getMapStorage(session)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java index 946c5943cc..37ac62bff1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java @@ -32,7 +32,6 @@ import org.keycloak.models.RealmModel.SearchableFields; import org.keycloak.models.RealmProvider; import org.keycloak.models.RoleModel; import org.keycloak.models.map.common.DeepCloner; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -49,12 +48,11 @@ public class MapRealmProvider implements RealmProvider { private static final Logger LOG = Logger.getLogger(MapRealmProvider.class); private final KeycloakSession session; - final MapKeycloakTransaction tx; + final MapStorage store; public MapRealmProvider(KeycloakSession session, MapStorage realmStore) { this.session = session; - this.tx = realmStore.createTransaction(session); - session.getTransactionManager().enlist(tx); + this.store = realmStore; } private RealmModel entityToAdapter(MapRealmEntity entity) { @@ -72,7 +70,7 @@ public class MapRealmProvider implements RealmProvider { throw new ModelDuplicateException("Realm with given name exists: " + name); } - if (id != null && tx.exists(id)) { + if (id != null && store.exists(id)) { throw new ModelDuplicateException("Realm exists: " + id); } @@ -82,7 +80,7 @@ public class MapRealmProvider implements RealmProvider { entity.setId(id); entity.setName(name); - entity = tx.create(entity); + entity = store.create(entity); return entityToAdapter(entity); } @@ -92,7 +90,7 @@ public class MapRealmProvider implements RealmProvider { LOG.tracef("getRealm(%s)%s", id, getShortStackTrace()); - MapRealmEntity entity = tx.read(id); + MapRealmEntity entity = store.read(id); return entity == null ? null : entityToAdapter(entity); } @@ -105,7 +103,7 @@ public class MapRealmProvider implements RealmProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.NAME, Operator.EQ, name); - String realmId = tx.read(withCriteria(mcb)) + String realmId = store.read(withCriteria(mcb)) .findFirst() .map(MapRealmEntity::getId) .orElse(null); @@ -127,7 +125,7 @@ public class MapRealmProvider implements RealmProvider { } private Stream getRealmsStream(DefaultModelCriteria mcb) { - return tx.read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) + return store.read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) .map(this::entityToAdapter); } @@ -141,7 +139,7 @@ public class MapRealmProvider implements RealmProvider { session.invalidate(REALM_BEFORE_REMOVE, realm); - tx.delete(id); + store.delete(id); session.invalidate(REALM_AFTER_REMOVE, realm); @@ -153,7 +151,7 @@ public class MapRealmProvider implements RealmProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.CLIENT_INITIAL_ACCESS, Operator.EXISTS); - tx.read(withCriteria(mcb)) + store.read(withCriteria(mcb)) .forEach(MapRealmEntity::removeExpiredClientInitialAccesses); } diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProviderFactory.java index 7c98202eac..8868f1c353 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProviderFactory.java @@ -32,7 +32,7 @@ public class MapRealmProviderFactory extends AbstractMapProviderFactory tx; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; public MapRoleProvider(KeycloakSession session, MapStorage roleStore) { this.session = session; - this.tx = roleStore.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = roleStore; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm) { @@ -61,11 +59,11 @@ public class MapRoleProvider implements RoleProvider { return origEntity -> new MapRoleAdapter(session, realm, origEntity); } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } @Override @@ -80,10 +78,10 @@ public class MapRoleProvider implements RoleProvider { entity.setId(id); entity.setRealmId(realm.getId()); entity.setName(name); - if (entity.getId() != null && txInRealm(realm).exists(entity.getId())) { + if (entity.getId() != null && storeWithRealm(realm).exists(entity.getId())) { throw new ModelDuplicateException("Role exists: " + id); } - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entityToAdapterFunc(realm).apply(entity); } @@ -94,7 +92,7 @@ public class MapRoleProvider implements RoleProvider { // filter realm roles only .compare(SearchableFields.CLIENT_ID, Operator.NOT_EXISTS); - return txInRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -111,7 +109,7 @@ public class MapRoleProvider implements RoleProvider { mcb = mcb.compare(RoleModel.SearchableFields.NAME, Operator.ILIKE, "%" + search + "%"); } - return txInRealm(realm).read(withCriteria(mcb).pagination(first, max, RoleModel.SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(first, max, RoleModel.SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -122,7 +120,7 @@ public class MapRoleProvider implements RoleProvider { // filter realm roles only .compare(SearchableFields.CLIENT_ID, Operator.NOT_EXISTS); - return txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) + return storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) .map(entityToAdapterFunc(realm)); } @@ -140,10 +138,10 @@ public class MapRoleProvider implements RoleProvider { entity.setRealmId(realm.getId()); entity.setName(name); entity.setClientId(client.getId()); - if (entity.getId() != null && txInRealm(realm).exists(entity.getId())) { + if (entity.getId() != null && storeWithRealm(realm).exists(entity.getId())) { throw new ModelDuplicateException("Role exists: " + id); } - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); return entityToAdapterFunc(realm).apply(entity); } @@ -154,7 +152,7 @@ public class MapRoleProvider implements RoleProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.CLIENT_ID, Operator.EQ, client.getId()); - return txInRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -165,7 +163,7 @@ public class MapRoleProvider implements RoleProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.CLIENT_ID, Operator.EQ, client.getId()); - return txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) + return storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.NAME, ASCENDING)) .map(entityToAdapterFunc(realm)); } @Override @@ -176,7 +174,7 @@ public class MapRoleProvider implements RoleProvider { session.invalidate(ROLE_BEFORE_REMOVE, realm, role); - txInRealm(realm).delete(role.getId()); + storeWithRealm(realm).delete(role.getId()); session.invalidate(ROLE_AFTER_REMOVE, realm, role); @@ -206,7 +204,7 @@ public class MapRoleProvider implements RoleProvider { .compare(SearchableFields.CLIENT_ID, Operator.NOT_EXISTS) .compare(SearchableFields.NAME, Operator.EQ, name); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(entityToAdapterFunc(realm)) .findFirst() .orElse(null); @@ -225,7 +223,7 @@ public class MapRoleProvider implements RoleProvider { .compare(SearchableFields.CLIENT_ID, Operator.EQ, client.getId()) .compare(SearchableFields.NAME, Operator.EQ, name); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(entityToAdapterFunc(realm)) .findFirst() .orElse(null); @@ -239,7 +237,7 @@ public class MapRoleProvider implements RoleProvider { LOG.tracef("getRoleById(%s, %s)%s", realm, id, getShortStackTrace()); - MapRoleEntity entity = txInRealm(realm).read(id); + MapRoleEntity entity = storeWithRealm(realm).read(id); String realmId = realm.getId(); // when a store doesn't store information about all realms, it doesn't have the information about return (entity == null || (entity.getRealmId() != null && !Objects.equals(realmId, entity.getRealmId()))) @@ -261,7 +259,7 @@ public class MapRoleProvider implements RoleProvider { mcb.compare(SearchableFields.DESCRIPTION, Operator.ILIKE, "%" + search + "%") ); - return txInRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -279,7 +277,7 @@ public class MapRoleProvider implements RoleProvider { mcb.compare(SearchableFields.DESCRIPTION, Operator.ILIKE, "%" + search + "%") ); - return txInRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(first, max, SearchableFields.NAME)) .map(entityToAdapterFunc(realm)); } @@ -288,7 +286,7 @@ public class MapRoleProvider implements RoleProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } public void preRemove(RealmModel realm, RoleModel role) { @@ -296,7 +294,7 @@ public class MapRoleProvider implements RoleProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.COMPOSITE_ROLE, Operator.EQ, role.getId()); - txInRealm(realm).read(withCriteria(mcb)).forEach(mapRoleEntity -> mapRoleEntity.removeCompositeRole(role.getId())); + storeWithRealm(realm).read(withCriteria(mcb)).forEach(mapRoleEntity -> mapRoleEntity.removeCompositeRole(role.getId())); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java index 7f729caf63..8271a5d0ca 100644 --- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProviderFactory.java @@ -38,7 +38,7 @@ public class MapRoleProviderFactory extends AbstractMapProviderFactory singleUseObjectTx; + protected final MapStorage singleUseObjectTx; - public MapSingleUseObjectProvider(KeycloakSession session, MapStorage storage) { - this.session = session; - singleUseObjectTx = storage.createTransaction(session); - - session.getTransactionManager().enlistAfterCompletion(singleUseObjectTx); + public MapSingleUseObjectProvider(MapStorage storage) { + this.singleUseObjectTx = storage; } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java index 17f856e5e1..9cf290b451 100644 --- a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java @@ -34,7 +34,7 @@ public class MapSingleUseObjectProviderFactory extends AbstractMapProviderFactor @Override public MapSingleUseObjectProvider createNew(KeycloakSession session) { - return new MapSingleUseObjectProvider(session, getStorage(session)); + return new MapSingleUseObjectProvider(getMapStorage(session)); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/CrudOperations.java b/model/map/src/main/java/org/keycloak/models/map/storage/CrudOperations.java new file mode 100644 index 0000000000..10a25651b6 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/CrudOperations.java @@ -0,0 +1,140 @@ +/* + * Copyright 2023 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; + +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.QueryParameters; + + +import java.util.stream.Stream; + +/** + * Interface for CRUD operations on the storage. The operations may not respect transactional boundaries + * if the underlying storage does not support it. + * + * @param Type of the value stored in the storage + * @param Type of the model object + */ +public interface CrudOperations { + + /** + * Creates an object in the storage. + *
+ * ID of the {@code value} may be prescribed in id of the {@code value}. + * If the id is {@code null} or its format is not matching the store internal format for ID, then + * the {@code value}'s ID will be generated and returned in the id of the return value. + * + * @param value Entity to create in the store + * @throws NullPointerException if {@code value} is {@code null} + * @see AbstractEntity#getId() + * @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value} + */ + V create(V value); + + /** + * Returns object with the given {@code key} from the storage or {@code null} if object does not exist. + *
+ * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return + * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. + * + * TODO: Consider returning {@code Optional} instead. + * @param key Key of the object. Must not be {@code null}. + * @return See description + * @throws NullPointerException if the {@code key} is {@code null} + */ + V read(String key); + + /** + * Updates the object with the key of the {@code value}'s ID in the storage if it already exists. + * + * @param value Updated value + * @return the previous value associated with the specified key, or null if there was no mapping for the key. + * (A null return can also indicate that the map previously associated null with the key, + * if the implementation supports null values.) + * @throws NullPointerException if the object or its {@code id} is {@code null} + * @see AbstractEntity#getId() + */ + V update(V value); + + /** + * Deletes object with the given {@code key} from the storage, if exists, no-op otherwise. + * @param key + * @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise. + */ + boolean delete(String key); + + /** + * Deletes objects that match the given criteria. + * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. + * @return Number of removed objects (might return {@code -1} if not supported) + */ + long delete(QueryParameters queryParameters); + + /** + * Returns stream of objects satisfying given {@code criteria} from the storage. + * The criteria are specified in the given criteria builder based on model properties. + *
+ * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return + * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. + * + * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. + * @return Stream of objects. Never returns {@code null}. + */ + Stream read(QueryParameters queryParameters); + + /** + * Returns the number of objects satisfying given {@code criteria} from the storage. + * The criteria are specified in the given criteria builder based on model properties. + * + * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. + * @return Number of objects. Never returns {@code null}. + */ + long getCount(QueryParameters queryParameters); + + /** + * Returns {@code true} if the object with the given {@code key} exists in the storage. {@code false} otherwise. + * + * @param key Key of the object. Must not be {@code null}. + * @return See description + * @throws NullPointerException if the {@code key} is {@code null} + */ + default boolean exists(String key) { + return read(key) != null; + } + + /** + * Returns {@code true} if at least one object is satisfying given {@code criteria} from the storage. {@code false} otherwise. + * The criteria are specified in the given criteria builder based on model properties. + * + * @param queryParameters parameters for the query + * @return See description + */ + default boolean exists(QueryParameters queryParameters) { + return getCount(queryParameters) > 0; + } + + /** + * Determines first available key from the value upon creation. + * @param value + * @return + */ + default String determineKeyFromValue(V value, boolean forCreate) { + return value == null ? null : value.getId(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java deleted file mode 100644 index 4e664d5fc0..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2020 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; - -import org.keycloak.models.KeycloakTransaction; -import org.keycloak.models.map.common.AbstractEntity; - -import java.util.stream.Stream; - -public interface MapKeycloakTransaction extends KeycloakTransaction { - - /** - * Instructs this transaction to add a new value into the underlying store on commit. - *

- * Updates to the returned instances of {@code V} would be visible in the current transaction - * and will propagate into the underlying store upon commit. - * - * The ID of the entity passed in the parameter might change to a different value in the returned value - * if the underlying storage decided this was necessary. - * If the ID of the entity was null before, it will be set on the returned value. - * - * @param value the value - * @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}. - */ - V create(V value); - - /** - * Provides possibility to lookup for values by a {@code key} in the underlying store with respect to changes done - * in current transaction. Updates to the returned instance would be visible in the current transaction - * and will propagate into the underlying store upon commit. - * - * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return - * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. - * - * @param key identifier of a value - * @return a value associated with the given {@code key} - */ - V read(String key); - - /** - * Returns a stream of values from underlying storage that are updated based on the current transaction changes; - * i.e. the result contains updates and excludes of records that have been created, updated or deleted in this - * transaction by methods {@link MapKeycloakTransaction#create}, {@link MapKeycloakTransaction#create}, - * {@link MapKeycloakTransaction#delete}, etc. - *

- * Updates to the returned instances of {@code V} would be visible in the current transaction - * and will propagate into the underlying store upon commit. - * - * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return - * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. - * - * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. - * @return values that fulfill the given criteria, that are updated based on changes in the current transaction - */ - Stream read(QueryParameters queryParameters); - - /** - * Returns a number of values present in the underlying storage that fulfill the given criteria with respect to - * changes done in the current transaction. - * - * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. - * @return number of values present in the storage that fulfill the given criteria - */ - long getCount(QueryParameters queryParameters); - - /** - * Instructs this transaction to delete a value associated with the identifier {@code key} from the underlying store - * on commit. - * - * @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise. - * @param key identifier of a value - */ - boolean delete(String key); - - /** - * Instructs this transaction to remove values (identified by {@code mcb} filter) from the underlying store on commit. - * - * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. - * @return number of removed objects (might return {@code -1} if not supported) - */ - long delete(QueryParameters queryParameters); - - /** - * Returns {@code true} if the object with the given {@code key} exists in the underlying storage with respect to changes done - * in current transaction. {@code false} otherwise. - * - * @param key Key of the object. Must not be {@code null}. - * @return See description - * @throws NullPointerException if the {@code key} is {@code null} - */ - default boolean exists(String key) { - return read(key) != null; - } - - /** - * Returns {@code true} if at least one object is satisfying given {@code criteria} from the underlying storage with respect to changes done - * in current transaction. {@code false} otherwise. - * The criteria are specified in the given criteria builder based on model properties. - * - * @param queryParameters parameters for the query - * @return See description - */ - default boolean exists(QueryParameters queryParameters) { - return getCount(queryParameters) > 0; - } -} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransactionWithAuth.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransactionWithAuth.java deleted file mode 100644 index eee9344a08..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransactionWithAuth.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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; - -import org.keycloak.credential.CredentialInput; -import org.keycloak.models.RealmModel; -import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.user.MapCredentialValidationOutput; - -/** - * A map store transaction that can authenticate the credentials provided by a user. - * - * @author Alexander Schwartz - */ -public interface MapKeycloakTransactionWithAuth extends MapKeycloakTransaction { - - /** - * Authenticate a user with the provided input credentials. Use this, for example, for Kerberos SPNEGO - * authentication, where the user will be determined at the end of the interaction with the client. - * @param realm realm against which to authenticate against - * @param input information provided by the user - * @return Information on how to continue the conversion with the client, or a terminal result. For a successful - * authentication, will also contain information about the user. - */ - MapCredentialValidationOutput authenticate(RealmModel realm, CredentialInput input); -} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java index 4debbd73b7..a74b27404f 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorage.java @@ -16,30 +16,108 @@ */ package org.keycloak.models.map.storage; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; +import java.util.stream.Stream; + /** - * Implementation of this interface interacts with a persistence storage storing various entities, e.g. users, realms. - * - * @author hmlnarik - * @param Type of the stored values that contains all the data stripped of session state. In other words, in the entities - * there are only IDs and mostly primitive types / {@code String}, never references to {@code *Model} instances. - * See the {@code Abstract*Entity} classes in this module. - * @param Type of the {@code *Model} corresponding to the stored value, e.g. {@code UserModel}. This is used for - * filtering via model fields in {@link ModelCriteriaBuilder} which is necessary to abstract from physical - * layout and thus to support no-downtime upgrade. + * A storage for entities that is based on a map and operates in the context of transaction + * managed by current {@code KeycloakSession}. Implementations of its methods should respect + * transactional boundaries of that transaction. */ public interface MapStorage { - - /** - * Creates a {@code MapKeycloakTransaction} object that tracks a new transaction related to this storage. - * In case of JPA or similar, the transaction object might be supplied by the container (via JTA) or - * shared same across storages accessing the same database within the same session; in other cases - * (e.g. plain map) a separate transaction handler might be created per each storage. - * - * @return See description. Never returns {@code null} - */ - MapKeycloakTransaction createTransaction(KeycloakSession session); + /** + * Instructs this storage to add a new value into the underlying store on commit in the context of the current transaction. + *

+ * Updates to the returned instances of {@code V} would be visible in the current transaction + * and will propagate into the underlying store upon commit. + * + * The ID of the entity passed in the parameter might change to a different value in the returned value + * if the underlying storage decided this was necessary. + * If the ID of the entity was null before, it will be set on the returned value. + * + * @param value the value + * @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}. + */ + V create(V value); + + /** + * Provides possibility to lookup for values by a {@code key} in the underlying store with respect to changes done + * in current transaction. Updates to the returned instance would be visible in the current transaction + * and will propagate into the underlying store upon commit. + * + * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return + * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. + * + * @param key identifier of a value + * @return a value associated with the given {@code key} + */ + V read(String key); + + /** + * Returns a stream of values from underlying storage that are updated based on the current transaction changes; + * i.e. the result contains updates and excludes of records that have been created, updated or deleted in this + * transaction by respective methods of this interface. + *

+ * Updates to the returned instances of {@code V} would be visible in the current transaction + * and will propagate into the underlying store upon commit. + * + * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return + * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. + * + * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. + * @return values that fulfill the given criteria, that are updated based on changes in the current transaction + */ + Stream read(QueryParameters queryParameters); + + /** + * Returns a number of values present in the underlying storage that fulfill the given criteria with respect to + * changes done in the current transaction. + * + * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. + * @return number of values present in the storage that fulfill the given criteria + */ + long getCount(QueryParameters queryParameters); + + /** + * Instructs this storage to delete a value associated with the identifier {@code key} from the underlying store + * upon commit. + * + * @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise. + * @param key identifier of a value + */ + boolean delete(String key); + + /** + * Instructs this transaction to remove values (identified by {@code mcb} filter) from the underlying store upon commit. + * + * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. + * @return number of removed objects (might return {@code -1} if not supported) + */ + long delete(QueryParameters queryParameters); + + /** + * Returns {@code true} if the object with the given {@code key} exists in the underlying storage with respect to changes done + * in the current transaction. {@code false} otherwise. + * + * @param key Key of the object. Must not be {@code null}. + * @return See description + * @throws NullPointerException if the {@code key} is {@code null} + */ + default boolean exists(String key) { + return read(key) != null; + } + + /** + * Returns {@code true} if at least one object is satisfying given {@code criteria} from the underlying storage with respect to changes done + * in the current transaction. {@code false} otherwise. + * The criteria are specified in the given criteria builder based on model properties. + * + * @param queryParameters parameters for the query + * @return See description + */ + default boolean exists(QueryParameters queryParameters) { + return getCount(queryParameters) > 0; + } } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageProvider.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageProvider.java index 7703dfbca4..a8ad8550f1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageProvider.java @@ -28,12 +28,13 @@ public interface MapStorageProvider extends Provider { /** * Returns a key-value storage implementation for the given types. - * @param type of the value - * @param type of the corresponding model (e.g. {@code UserModel}) + * + * @param type of the value + * @param type of the corresponding model (e.g. {@code UserModel}) * @param modelType Model type - * @param flags Flags of the returned storage. Best effort, flags may be not honored by underlying implementation + * @param flags Flags of the returned storage. Best effort, flags may be not honored by underlying implementation * @return * @throws IllegalArgumentException If some of the types is not supported by the underlying implementation. */ - MapStorage getStorage(Class modelType, Flag... flags); + MapStorage getMapStorage(Class modelType, Flag... flags); } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageWithAuth.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageWithAuth.java index 5a9779d923..c957b00577 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageWithAuth.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapStorageWithAuth.java @@ -17,28 +17,25 @@ package org.keycloak.models.map.storage; -import org.keycloak.credential.CredentialModel; -import org.keycloak.models.KeycloakSession; +import org.keycloak.credential.CredentialInput; +import org.keycloak.models.RealmModel; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.user.MapCredentialValidationOutput; /** - * Implementing this interface signals that the store can validate credentials. - * This will be implemented, for example, by a store that supports SPNEGO for Kerberos authentication. + * A map store that can authenticate the credentials provided by a user. * * @author Alexander Schwartz */ -public interface MapStorageWithAuth extends MapStorage { +public interface MapStorageWithAuth extends MapStorage { /** - * Determine which credential types a store supports. - * This method should be a cheap way to query the store before creating a more expensive transaction and performing an authentication. - * - * @param type supported credential type by this store, for example {@link CredentialModel#KERBEROS}. - * @return true if the credential type is supported by this storage + * Authenticate a user with the provided input credentials. Use this, for example, for Kerberos SPNEGO + * authentication, where the user will be determined at the end of the interaction with the client. + * @param realm realm against which to authenticate against + * @param input information provided by the user + * @return Information on how to continue the conversion with the client, or a terminal result. For a successful + * authentication, will also contain information about the user. */ - boolean supportsCredentialType(String type); - - @Override - MapKeycloakTransactionWithAuth createTransaction(KeycloakSession session); + MapCredentialValidationOutput authenticate(RealmModel realm, CredentialInput input); } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java index 9f8ed62e0e..d5c4ac05c9 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java @@ -1,112 +1,178 @@ +/* + * Copyright 2020 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.chm; +import org.keycloak.models.map.common.ExpirableEntity; +import org.keycloak.models.map.common.ExpirationUtils; +import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.CrudOperations; +import org.keycloak.models.map.storage.ModelEntityUtil; import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; +import org.keycloak.storage.SearchableModelField; - +import java.util.Comparator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; +import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; +import java.util.Objects; +import java.util.function.Predicate; -public interface ConcurrentHashMapCrudOperations { - /** - * Creates an object in the store. ID of the {@code value} may be prescribed in id of the {@code value}. - * If the id is {@code null} or its format is not matching the store internal format for ID, then - * the {@code value}'s ID will be generated and returned in the id of the return value. - * @param value Entity to create in the store - * @throws NullPointerException if {@code value} is {@code null} - * @see AbstractEntity#getId() - * @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value} - */ - V create(V value); +import static org.keycloak.models.map.common.ExpirationUtils.isExpired; +import static org.keycloak.utils.StreamsUtil.paginatedStream; - /** - * Returns object with the given {@code key} from the storage or {@code null} if object does not exist. - *
- * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return - * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. - * - * TODO: Consider returning {@code Optional} instead. - * @param key Key of the object. Must not be {@code null}. - * @return See description - * @throws NullPointerException if the {@code key} is {@code null} - */ - public V read(String key); +/** + * + * It contains basic object CRUD operations as well as bulk {@link #read(org.keycloak.models.map.storage.QueryParameters)} + * and bulk {@link #delete(org.keycloak.models.map.storage.QueryParameters)} operations, + * and operation for determining the number of the objects satisfying given criteria + * ({@link #getCount(org.keycloak.models.map.storage.QueryParameters)}). + * + * @author hmlnarik + */ +public class ConcurrentHashMapCrudOperations implements CrudOperations { - /** - * Updates the object with the key of the {@code value}'s ID in the storage if it already exists. - * - * @param value Updated value - * @return the previous value associated with the specified key, or null if there was no mapping for the key. - * (A null return can also indicate that the map previously associated null with the key, - * if the implementation supports null values.) - * @throws NullPointerException if the object or its {@code id} is {@code null} - * @see AbstractEntity#getId() - */ - V update(V value); + protected final ConcurrentMap store = new ConcurrentHashMap<>(); - /** - * Deletes object with the given {@code key} from the storage, if exists, no-op otherwise. - * @param key - * @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise. - */ - boolean delete(String key); + protected final Map, UpdatePredicatesFunc> fieldPredicates; + protected final StringKeyConverter keyConverter; + protected final DeepCloner cloner; + private final boolean isExpirableEntity; - /** - * Deletes objects that match the given criteria. - * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. - * @return Number of removed objects (might return {@code -1} if not supported) - */ - long delete(QueryParameters queryParameters); - - /** - * Returns stream of objects satisfying given {@code criteria} from the storage. - * The criteria are specified in the given criteria builder based on model properties. - * - * If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return - * entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details. - * - * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. - * @return Stream of objects. Never returns {@code null}. - */ - Stream read(QueryParameters queryParameters); - - /** - * Returns the number of objects satisfying given {@code criteria} from the storage. - * The criteria are specified in the given criteria builder based on model properties. - * - * @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc. - * @return Number of objects. Never returns {@code null}. - */ - long getCount(QueryParameters queryParameters); - - /** - * Returns {@code true} if the object with the given {@code key} exists in the storage. {@code false} otherwise. - * - * @param key Key of the object. Must not be {@code null}. - * @return See description - * @throws NullPointerException if the {@code key} is {@code null} - */ - default boolean exists(String key) { - return read(key) != null; + @SuppressWarnings("unchecked") + public ConcurrentHashMapCrudOperations(Class modelClass, StringKeyConverter keyConverter, DeepCloner cloner) { + this.fieldPredicates = MapFieldPredicates.getPredicates(modelClass); + this.keyConverter = keyConverter; + this.cloner = cloner; + this.isExpirableEntity = ExpirableEntity.class.isAssignableFrom(ModelEntityUtil.getEntityType(modelClass)); } - /** - * Returns {@code true} if at least one object is satisfying given {@code criteria} from the storage. {@code false} otherwise. - * The criteria are specified in the given criteria builder based on model properties. - * - * @param queryParameters parameters for the query - * @return See description - */ - default boolean exists(QueryParameters queryParameters) { - return getCount(queryParameters) > 0; + @Override + public V create(V value) { + K key = keyConverter.fromStringSafe(value.getId()); + if (key == null) { + key = keyConverter.yieldNewUniqueKey(); + value = cloner.from(keyConverter.keyToString(key), value); + } + store.putIfAbsent(key, value); + return value; } - /** - * Determines first available key from the value upon creation. - * @param value - * @return - */ - default String determineKeyFromValue(V value, boolean forCreate) { - return value == null ? null : value.getId(); + @Override + public V read(String key) { + Objects.requireNonNull(key, "Key must be non-null"); + K k = keyConverter.fromStringSafe(key); + + V v = store.get(k); + if (v == null) return null; + return isExpirableEntity && isExpired((ExpirableEntity) v, true) ? null : v; } + + @Override + public V update(V value) { + K key = getKeyConverter().fromStringSafe(value.getId()); + return store.replace(key, value); + } + + @Override + public boolean delete(String key) { + K k = getKeyConverter().fromStringSafe(key); + return store.remove(k) != null; + } + + @Override + public long delete(QueryParameters queryParameters) { + DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); + + if (criteria == null) { + long res = store.size(); + store.clear(); + return res; + } + + @SuppressWarnings("unchecked") + MapModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder()); + Predicate keyFilter = mcb.getKeyFilter(); + Predicate entityFilter = mcb.getEntityFilter(); + Stream> storeStream = store.entrySet().stream(); + final AtomicLong res = new AtomicLong(0); + + if (!queryParameters.getOrderBy().isEmpty()) { + Comparator comparator = MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream()); + storeStream = storeStream.sorted((entry1, entry2) -> comparator.compare(entry1.getValue(), entry2.getValue())); + } + + paginatedStream(storeStream.filter(next -> keyFilter.test(next.getKey()) && entityFilter.test(next.getValue())) + , queryParameters.getOffset(), queryParameters.getLimit()) + .peek(item -> {res.incrementAndGet();}) + .map(Entry::getKey) + .forEach(store::remove); + + return res.get(); + } + + public MapModelCriteriaBuilder createCriteriaBuilder() { + return new MapModelCriteriaBuilder<>(keyConverter, fieldPredicates); + } + + public StringKeyConverter getKeyConverter() { + return keyConverter; + } + + @Override + public Stream read(QueryParameters queryParameters) { + DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); + + if (criteria == null) { + return Stream.empty(); + } + + MapModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder()); + Stream> stream = store.entrySet().stream(); + + Predicate keyFilter = mcb.getKeyFilter(); + Predicate entityFilter; + + if (isExpirableEntity) { + entityFilter = mcb.getEntityFilter().and(ExpirationUtils::isNotExpired); + } else { + entityFilter = mcb.getEntityFilter(); + } + + Stream valueStream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue())) + .map(Map.Entry::getValue); + + if (!queryParameters.getOrderBy().isEmpty()) { + valueStream = valueStream.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream())); + } + + return paginatedStream(valueStream, queryParameters.getOffset(), queryParameters.getLimit()); + } + + @Override + public long getCount(QueryParameters queryParameters) { + return read(queryParameters).count(); + } + } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java deleted file mode 100644 index b60a8e6368..0000000000 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java +++ /dev/null @@ -1,514 +0,0 @@ -/* - * Copyright 2020 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.chm; - -import org.keycloak.models.map.common.StringKeyConverter; -import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.common.DeepCloner; -import org.keycloak.models.map.common.EntityField; -import org.keycloak.models.map.common.HasRealmId; -import org.keycloak.models.map.common.UpdatableEntity; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.jboss.logging.Logger; -import org.keycloak.models.map.storage.MapKeycloakTransaction; -import org.keycloak.models.map.storage.ModelEntityUtil; -import org.keycloak.models.map.storage.QueryParameters; -import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; -import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; -import org.keycloak.storage.SearchableModelField; -import java.util.function.Consumer; - -public class ConcurrentHashMapKeycloakTransaction implements MapKeycloakTransaction, HasRealmId { - - private final static Logger log = Logger.getLogger(ConcurrentHashMapKeycloakTransaction.class); - - protected boolean active; - protected boolean rollback; - protected final Map tasks = new LinkedHashMap<>(); - protected final ConcurrentHashMapCrudOperations map; - protected final StringKeyConverter keyConverter; - protected final DeepCloner cloner; - protected final Map, UpdatePredicatesFunc> fieldPredicates; - protected final EntityField realmIdEntityField; - private String realmId; - private final boolean mapHasRealmId; - - enum MapOperation { - CREATE, UPDATE, DELETE, - } - - public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations map, StringKeyConverter keyConverter, DeepCloner cloner, Map, UpdatePredicatesFunc> fieldPredicates) { - this(map, keyConverter, cloner, fieldPredicates, null); - } - - public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations map, StringKeyConverter keyConverter, DeepCloner cloner, Map, UpdatePredicatesFunc> fieldPredicates, EntityField realmIdEntityField) { - this.map = map; - this.keyConverter = keyConverter; - this.cloner = cloner; - this.fieldPredicates = fieldPredicates; - this.realmIdEntityField = realmIdEntityField; - this.mapHasRealmId = map instanceof HasRealmId; - } - - @Override - public void begin() { - active = true; - } - - @Override - public void commit() { - if (rollback) { - throw new RuntimeException("Rollback only!"); - } - - final Consumer setRealmId = mapHasRealmId ? ((HasRealmId) map)::setRealmId : a -> {}; - if (! tasks.isEmpty()) { - log.tracef("Commit - %s", map); - for (MapTaskWithValue value : tasks.values()) { - setRealmId.accept(value.getRealmId()); - value.execute(); - } - } - } - - @Override - public void rollback() { - tasks.clear(); - } - - @Override - public void setRollbackOnly() { - rollback = true; - } - - @Override - public boolean getRollbackOnly() { - return rollback; - } - - @Override - public boolean isActive() { - return active; - } - - private MapModelCriteriaBuilder createCriteriaBuilder() { - return new MapModelCriteriaBuilder<>(keyConverter, fieldPredicates); - } - - /** - * Adds a given task if not exists for the given key - */ - protected void addTask(String key, MapTaskWithValue task) { - log.tracef("Adding operation %s for %s @ %08x", task.getOperation(), key, System.identityHashCode(task.getValue())); - - tasks.merge(key, task, MapTaskCompose::new); - } - - /** - * Returns a deep clone of an entity. If the clone is already in the transaction, returns this one. - *

- * Usually used before giving an entity from a source back to the caller, - * to prevent changing it directly in the data store, but to keep transactional properties. - * @param origEntity Original entity - * @return - */ - public V registerEntityForChanges(V origEntity) { - final String key = origEntity.getId(); - // If the entity is listed in the transaction already, return it directly - if (tasks.containsKey(key)) { - MapTaskWithValue current = tasks.get(key); - return current.getValue(); - } - // Else enlist its copy in the transaction. Never return direct reference to the underlying map - final V res = cloner.from(origEntity); - return updateIfChanged(res, e -> e.isUpdated()); - } - - @Override - public V read(String sKey) { - try { - // TODO: Consider using Optional rather than handling NPE - final V entity = read(sKey, map::read); - if (entity == null) { - log.debugf("Could not read object for key %s", sKey); - return null; - } - return postProcess(registerEntityForChanges(entity)); - } catch (NullPointerException ex) { - return null; - } - } - - private V read(String key, Function defaultValueFunc) { - MapTaskWithValue current = tasks.get(key); - // If the key exists, then it has entered the "tasks" after bulk delete that could have - // removed it, so looking through bulk deletes is irrelevant - if (tasks.containsKey(key)) { - return current.getValue(); - } - - // If the key does not exist, then it would be read fresh from the storage, but then it - // could have been removed by some bulk delete in the existing tasks. Check it. - final V value = defaultValueFunc.apply(key); - for (MapTaskWithValue val : tasks.values()) { - if (val instanceof ConcurrentHashMapKeycloakTransaction.BulkDeleteOperation) { - final BulkDeleteOperation delOp = (BulkDeleteOperation) val; - if (! delOp.getFilterForNonDeletedObjects().test(value)) { - return null; - } - } - } - - return value; - } - - /** - * Returns the stream of records that match given criteria and includes changes made in this transaction, i.e. - * the result contains updates and excludes records that have been deleted in this transaction. - * - * @param queryParameters - * @return - */ - @Override - public Stream read(QueryParameters queryParameters) { - DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder(); - MapModelCriteriaBuilder mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder()); - - Predicate filterOutAllBulkDeletedObjects = tasks.values().stream() - .filter(BulkDeleteOperation.class::isInstance) - .map(BulkDeleteOperation.class::cast) - .map(BulkDeleteOperation::getFilterForNonDeletedObjects) - .reduce(Predicate::and) - .orElse(v -> true); - - Stream updatedAndNotRemovedObjectsStream = this.map.read(queryParameters) - .filter(filterOutAllBulkDeletedObjects) - .map(this::getUpdated) // If the object has been removed, tx.get will return null, otherwise it will return me.getValue() - .filter(Objects::nonNull) - .map(this::registerEntityForChanges); - - updatedAndNotRemovedObjectsStream = postProcess(updatedAndNotRemovedObjectsStream); - - if (mapMcb != null) { - // Add explicit filtering for the case when the map returns raw stream of untested values (ie. realize sequential scan) - updatedAndNotRemovedObjectsStream = updatedAndNotRemovedObjectsStream - .filter(e -> mapMcb.getKeyFilter().test(keyConverter.fromStringSafe(e.getId()))) - .filter(mapMcb.getEntityFilter()); - } - - // In case of created values stored in MapKeycloakTransaction, we need filter those according to the filter - Stream res = mapMcb == null - ? updatedAndNotRemovedObjectsStream - : Stream.concat( - createdValuesStream(mapMcb.getKeyFilter(), mapMcb.getEntityFilter()), - updatedAndNotRemovedObjectsStream - ); - - if (!queryParameters.getOrderBy().isEmpty()) { - res = res.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream())); - } - - - return res; - } - - @Override - public long getCount(QueryParameters queryParameters) { - return read(queryParameters).count(); - } - - private V getUpdated(V orig) { - MapTaskWithValue current = orig == null ? null : tasks.get(orig.getId()); - return current == null ? orig : current.getValue(); - } - - @Override - public V create(V value) { - String key = map.determineKeyFromValue(value, true); - if (key == null) { - K newKey = keyConverter.yieldNewUniqueKey(); - key = keyConverter.keyToString(newKey); - value = cloner.from(key, value); - } else if (! key.equals(value.getId())) { - value = cloner.from(key, value); - } else { - value = cloner.from(value); - } - addTask(key, new CreateOperation(value)); - return postProcess(value); - } - - public V updateIfChanged(V value, Predicate shouldPut) { - String key = value.getId(); - log.tracef("Adding operation UPDATE_IF_CHANGED for %s @ %08x", key, System.identityHashCode(value)); - - String taskKey = key; - MapTaskWithValue op = new MapTaskWithValue(value) { - @Override - public void execute() { - if (shouldPut.test(getValue())) { - map.update(getValue()); - } - } - @Override public MapOperation getOperation() { return MapOperation.UPDATE; } - }; - return tasks.merge(taskKey, op, this::merge).getValue(); - } - - @Override - public boolean delete(String key) { - tasks.merge(key, new DeleteOperation(key), this::merge); - return true; - } - - @Override - public long delete(QueryParameters queryParameters) { - log.tracef("Adding operation DELETE_BULK"); - - K artificialKey = keyConverter.yieldNewUniqueKey(); - - // Remove all tasks that create / update / delete objects deleted by the bulk removal. - final BulkDeleteOperation bdo = new BulkDeleteOperation(queryParameters); - Predicate filterForNonDeletedObjects = bdo.getFilterForNonDeletedObjects(); - long res = 0; - for (Iterator> it = tasks.entrySet().iterator(); it.hasNext();) { - Entry me = it.next(); - if (! filterForNonDeletedObjects.test(me.getValue().getValue())) { - log.tracef(" [DELETE_BULK] removing %s", me.getKey()); - it.remove(); - res++; - } - } - - tasks.put(keyConverter.keyToString(artificialKey), bdo); - - return res + bdo.getCount(); - } - - @Override - public boolean exists(String key) { - if (tasks.containsKey(key)) { - MapTaskWithValue o = tasks.get(key); - return o.getValue() != null; - } - - // Check if there is a bulk delete operation in which case read the full entity - for (MapTaskWithValue val : tasks.values()) { - if (val instanceof ConcurrentHashMapKeycloakTransaction.BulkDeleteOperation) { - return read(key) != null; - } - } - - return map.exists(key); - } - - private Stream createdValuesStream(Predicate keyFilter, Predicate entityFilter) { - return this.tasks.entrySet().stream() - .filter(me -> keyFilter.test(keyConverter.fromStringSafe(me.getKey()))) - .map(Map.Entry::getValue) - .filter(v -> v.containsCreate() && ! v.isReplace()) - .map(MapTaskWithValue::getValue) - .filter(Objects::nonNull) - .filter(entityFilter) - // make a snapshot - .collect(Collectors.toList()).stream(); - } - - private MapTaskWithValue merge(MapTaskWithValue oldValue, MapTaskWithValue newValue) { - switch (newValue.getOperation()) { - case DELETE: - return newValue; - default: - return new MapTaskCompose(oldValue, newValue); - } - } - - protected abstract class MapTaskWithValue { - protected final V value; - private final String realmId; - - public MapTaskWithValue(V value) { - this.value = value; - this.realmId = ConcurrentHashMapKeycloakTransaction.this.realmId; - } - - public V getValue() { - return value; - } - - public boolean containsCreate() { - return MapOperation.CREATE == getOperation(); - } - - public boolean containsRemove() { - return MapOperation.DELETE == getOperation(); - } - - public boolean isReplace() { - return false; - } - - public String getRealmId() { - return realmId; - } - - public abstract MapOperation getOperation(); - public abstract void execute(); - } - - private class MapTaskCompose extends MapTaskWithValue { - - private final MapTaskWithValue oldValue; - private final MapTaskWithValue newValue; - - public MapTaskCompose(MapTaskWithValue oldValue, MapTaskWithValue newValue) { - super(null); - this.oldValue = oldValue; - this.newValue = newValue; - } - - @Override - public void execute() { - oldValue.execute(); - newValue.execute(); - } - - @Override - public V getValue() { - return newValue.getValue(); - } - - @Override - public MapOperation getOperation() { - return null; - } - - @Override - public boolean containsCreate() { - return oldValue.containsCreate() || newValue.containsCreate(); - } - - @Override - public boolean containsRemove() { - return oldValue.containsRemove() || newValue.containsRemove(); - } - - @Override - public boolean isReplace() { - return (newValue.getOperation() == MapOperation.CREATE && oldValue.containsRemove()) || - (oldValue instanceof ConcurrentHashMapKeycloakTransaction.MapTaskCompose && ((MapTaskCompose) oldValue).isReplace()); - } - } - - private class CreateOperation extends MapTaskWithValue { - public CreateOperation(V value) { - super(value); - } - - @Override public void execute() { map.create(getValue()); } - @Override public MapOperation getOperation() { return MapOperation.CREATE; } - } - - private class DeleteOperation extends MapTaskWithValue { - private final String key; - - public DeleteOperation(String key) { - super(null); - this.key = key; - } - - @Override public void execute() { map.delete(key); } - @Override public MapOperation getOperation() { return MapOperation.DELETE; } - } - - private class BulkDeleteOperation extends MapTaskWithValue { - - private final QueryParameters queryParameters; - - public BulkDeleteOperation(QueryParameters queryParameters) { - super(null); - this.queryParameters = queryParameters; - } - - @Override - @SuppressWarnings("unchecked") - public void execute() { - map.delete(queryParameters); - } - - public Predicate getFilterForNonDeletedObjects() { - DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder(); - MapModelCriteriaBuilder mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder()); - - Predicate entityFilter = mmcb.getEntityFilter(); - Predicate keyFilter = mmcb.getKeyFilter(); - return v -> v == null || ! (keyFilter.test(keyConverter.fromStringSafe(v.getId())) && entityFilter.test(v)); - } - - @Override - public MapOperation getOperation() { - return MapOperation.DELETE; - } - - private long getCount() { - return map.getCount(queryParameters); - } - } - - @Override - public String getRealmId() { - if (mapHasRealmId) { - return ((HasRealmId) map).getRealmId(); - } - return null; - } - - @Override - @SuppressWarnings("unchecked") - public void setRealmId(String realmId) { - if (mapHasRealmId) { - ((HasRealmId) map).setRealmId(realmId); - this.realmId = realmId; - } else { - this.realmId = null; - } - } - - private V postProcess(V value) { - return (realmId == null || value == null) - ? value - : ModelEntityUtil.supplyReadOnlyFieldValueIfUnset(value, realmIdEntityField, realmId); - } - - private Stream postProcess(Stream stream) { - if (this.realmId == null) { - return stream; - } - - String localRealmId = this.realmId; - return stream.map((V value) -> ModelEntityUtil.supplyReadOnlyFieldValueIfUnset(value, realmIdEntityField, localRealmId)); - } - -} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java index e28cee31bd..565ef3c599 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java @@ -1,13 +1,13 @@ /* * Copyright 2020 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. @@ -16,172 +16,224 @@ */ package org.keycloak.models.map.storage.chm; -import org.keycloak.models.map.common.ExpirableEntity; -import org.keycloak.models.map.common.ExpirationUtils; +import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.map.common.StringKeyConverter; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.EntityField; +import org.keycloak.models.map.common.HasRealmId; import org.keycloak.models.map.common.UpdatableEntity; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jboss.logging.Logger; +import org.keycloak.models.map.storage.CrudOperations; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelEntityUtil; import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.storage.SearchableModelField; +import java.util.function.Consumer; -import java.util.Comparator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Stream; -import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc; -import java.util.Objects; -import java.util.function.Predicate; +public class ConcurrentHashMapStorage implements MapStorage, KeycloakTransaction, HasRealmId { -import static org.keycloak.models.map.common.ExpirationUtils.isExpired; -import static org.keycloak.utils.StreamsUtil.paginatedStream; + private final static Logger log = Logger.getLogger(ConcurrentHashMapStorage.class); -/** - * - * It contains basic object CRUD operations as well as bulk {@link #read(org.keycloak.models.map.storage.QueryParameters)} - * and bulk {@link #delete(org.keycloak.models.map.storage.QueryParameters)} operations, - * and operation for determining the number of the objects satisfying given criteria - * ({@link #getCount(org.keycloak.models.map.storage.QueryParameters)}). - * - * @author hmlnarik - */ -public class ConcurrentHashMapStorage implements MapStorage, ConcurrentHashMapCrudOperations { - - protected final ConcurrentMap store = new ConcurrentHashMap<>(); - - protected final Map, UpdatePredicatesFunc> fieldPredicates; + protected boolean active; + protected boolean rollback; + protected final Map tasks = new LinkedHashMap<>(); + protected final CrudOperations map; protected final StringKeyConverter keyConverter; protected final DeepCloner cloner; - private final boolean isExpirableEntity; + protected final Map, UpdatePredicatesFunc> fieldPredicates; + protected final EntityField realmIdEntityField; + private String realmId; + private final boolean mapHasRealmId; - @SuppressWarnings("unchecked") - public ConcurrentHashMapStorage(Class modelClass, StringKeyConverter keyConverter, DeepCloner cloner) { - this.fieldPredicates = MapFieldPredicates.getPredicates(modelClass); + enum MapOperation { + CREATE, UPDATE, DELETE, + } + + public ConcurrentHashMapStorage(CrudOperations map, StringKeyConverter keyConverter, DeepCloner cloner, Map, UpdatePredicatesFunc> fieldPredicates) { + this(map, keyConverter, cloner, fieldPredicates, null); + } + + public ConcurrentHashMapStorage(CrudOperations map, StringKeyConverter keyConverter, DeepCloner cloner, Map, UpdatePredicatesFunc> fieldPredicates, EntityField realmIdEntityField) { + this.map = map; this.keyConverter = keyConverter; this.cloner = cloner; - this.isExpirableEntity = ExpirableEntity.class.isAssignableFrom(ModelEntityUtil.getEntityType(modelClass)); + this.fieldPredicates = fieldPredicates; + this.realmIdEntityField = realmIdEntityField; + this.mapHasRealmId = map instanceof HasRealmId; } @Override - public V create(V value) { - K key = keyConverter.fromStringSafe(value.getId()); - if (key == null) { - key = keyConverter.yieldNewUniqueKey(); - value = cloner.from(keyConverter.keyToString(key), value); - } - store.putIfAbsent(key, value); - return value; + public void begin() { + active = true; } @Override - public V read(String key) { - Objects.requireNonNull(key, "Key must be non-null"); - K k = keyConverter.fromStringSafe(key); - - V v = store.get(k); - if (v == null) return null; - return isExpirableEntity && isExpired((ExpirableEntity) v, true) ? null : v; - } - - @Override - public V update(V value) { - K key = getKeyConverter().fromStringSafe(value.getId()); - return store.replace(key, value); - } - - @Override - public boolean delete(String key) { - K k = getKeyConverter().fromStringSafe(key); - return store.remove(k) != null; - } - - @Override - public long delete(QueryParameters queryParameters) { - DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); - - if (criteria == null) { - long res = store.size(); - store.clear(); - return res; + public void commit() { + if (rollback) { + throw new RuntimeException("Rollback only!"); } - @SuppressWarnings("unchecked") - MapModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder()); - Predicate keyFilter = mcb.getKeyFilter(); - Predicate entityFilter = mcb.getEntityFilter(); - Stream> storeStream = store.entrySet().stream(); - final AtomicLong res = new AtomicLong(0); - - if (!queryParameters.getOrderBy().isEmpty()) { - Comparator comparator = MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream()); - storeStream = storeStream.sorted((entry1, entry2) -> comparator.compare(entry1.getValue(), entry2.getValue())); + final Consumer setRealmId = mapHasRealmId ? ((HasRealmId) map)::setRealmId : a -> {}; + if (! tasks.isEmpty()) { + log.tracef("Commit - %s", map); + for (MapTaskWithValue value : tasks.values()) { + setRealmId.accept(value.getRealmId()); + value.execute(); + } } - - paginatedStream(storeStream.filter(next -> keyFilter.test(next.getKey()) && entityFilter.test(next.getValue())) - , queryParameters.getOffset(), queryParameters.getLimit()) - .peek(item -> {res.incrementAndGet();}) - .map(Entry::getKey) - .forEach(store::remove); - - return res.get(); } @Override - @SuppressWarnings("unchecked") - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - MapKeycloakTransaction sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); - - if (sessionTransaction == null) { - sessionTransaction = new ConcurrentHashMapKeycloakTransaction<>(this, keyConverter, cloner, fieldPredicates); - session.setAttribute("map-transaction-" + hashCode(), sessionTransaction); - } - return sessionTransaction; + public void rollback() { + tasks.clear(); } - public MapModelCriteriaBuilder createCriteriaBuilder() { + @Override + public void setRollbackOnly() { + rollback = true; + } + + @Override + public boolean getRollbackOnly() { + return rollback; + } + + @Override + public boolean isActive() { + return active; + } + + private MapModelCriteriaBuilder createCriteriaBuilder() { return new MapModelCriteriaBuilder<>(keyConverter, fieldPredicates); } - public StringKeyConverter getKeyConverter() { - return keyConverter; + /** + * Adds a given task if not exists for the given key + */ + protected void addTask(String key, MapTaskWithValue task) { + log.tracef("Adding operation %s for %s @ %08x", task.getOperation(), key, System.identityHashCode(task.getValue())); + + tasks.merge(key, task, MapTaskCompose::new); + } + + /** + * Returns a deep clone of an entity. If the clone is already in the transaction, returns this one. + *

+ * Usually used before giving an entity from a source back to the caller, + * to prevent changing it directly in the data store, but to keep transactional properties. + * @param origEntity Original entity + * @return + */ + public V registerEntityForChanges(V origEntity) { + final String key = origEntity.getId(); + // If the entity is listed in the transaction already, return it directly + if (tasks.containsKey(key)) { + MapTaskWithValue current = tasks.get(key); + return current.getValue(); + } + // Else enlist its copy in the transaction. Never return direct reference to the underlying map + final V res = cloner.from(origEntity); + return updateIfChanged(res, e -> e.isUpdated()); } + @Override + public V read(String sKey) { + try { + // TODO: Consider using Optional rather than handling NPE + final V entity = read(sKey, map::read); + if (entity == null) { + log.debugf("Could not read object for key %s", sKey); + return null; + } + return postProcess(registerEntityForChanges(entity)); + } catch (NullPointerException ex) { + return null; + } + } + + private V read(String key, Function defaultValueFunc) { + MapTaskWithValue current = tasks.get(key); + // If the key exists, then it has entered the "tasks" after bulk delete that could have + // removed it, so looking through bulk deletes is irrelevant + if (tasks.containsKey(key)) { + return current.getValue(); + } + + // If the key does not exist, then it would be read fresh from the storage, but then it + // could have been removed by some bulk delete in the existing tasks. Check it. + final V value = defaultValueFunc.apply(key); + for (MapTaskWithValue val : tasks.values()) { + if (val instanceof ConcurrentHashMapStorage.BulkDeleteOperation) { + final BulkDeleteOperation delOp = (BulkDeleteOperation) val; + if (! delOp.getFilterForNonDeletedObjects().test(value)) { + return null; + } + } + } + + return value; + } + + /** + * Returns the stream of records that match given criteria and includes changes made in this transaction, i.e. + * the result contains updates and excludes records that have been deleted in this transaction. + * + * @param queryParameters + * @return + */ @Override public Stream read(QueryParameters queryParameters) { - DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); + DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder(); + MapModelCriteriaBuilder mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder()); - if (criteria == null) { - return Stream.empty(); + Predicate filterOutAllBulkDeletedObjects = tasks.values().stream() + .filter(BulkDeleteOperation.class::isInstance) + .map(BulkDeleteOperation.class::cast) + .map(BulkDeleteOperation::getFilterForNonDeletedObjects) + .reduce(Predicate::and) + .orElse(v -> true); + + Stream updatedAndNotRemovedObjectsStream = this.map.read(queryParameters) + .filter(filterOutAllBulkDeletedObjects) + .map(this::getUpdated) // If the object has been removed, store.get will return null, otherwise it will return me.getValue() + .filter(Objects::nonNull) + .map(this::registerEntityForChanges); + + updatedAndNotRemovedObjectsStream = postProcess(updatedAndNotRemovedObjectsStream); + + if (mapMcb != null) { + // Add explicit filtering for the case when the map returns raw stream of untested values (ie. realize sequential scan) + updatedAndNotRemovedObjectsStream = updatedAndNotRemovedObjectsStream + .filter(e -> mapMcb.getKeyFilter().test(keyConverter.fromStringSafe(e.getId()))) + .filter(mapMcb.getEntityFilter()); } - MapModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder()); - Stream> stream = store.entrySet().stream(); - - Predicate keyFilter = mcb.getKeyFilter(); - Predicate entityFilter; - - if (isExpirableEntity) { - entityFilter = mcb.getEntityFilter().and(ExpirationUtils::isNotExpired); - } else { - entityFilter = mcb.getEntityFilter(); - } - - Stream valueStream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue())) - .map(Map.Entry::getValue); + // In case of created values stored in MapKeycloakTransaction, we need filter those according to the filter + Stream res = mapMcb == null + ? updatedAndNotRemovedObjectsStream + : Stream.concat( + createdValuesStream(mapMcb.getKeyFilter(), mapMcb.getEntityFilter()), + updatedAndNotRemovedObjectsStream + ); if (!queryParameters.getOrderBy().isEmpty()) { - valueStream = valueStream.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream())); + res = res.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream())); } - return paginatedStream(valueStream, queryParameters.getOffset(), queryParameters.getLimit()); + + return res; } @Override @@ -189,4 +241,276 @@ public class ConcurrentHashMapStorage shouldPut) { + String key = value.getId(); + log.tracef("Adding operation UPDATE_IF_CHANGED for %s @ %08x", key, System.identityHashCode(value)); + + String taskKey = key; + MapTaskWithValue op = new MapTaskWithValue(value) { + @Override + public void execute() { + if (shouldPut.test(getValue())) { + map.update(getValue()); + } + } + @Override public MapOperation getOperation() { return MapOperation.UPDATE; } + }; + return tasks.merge(taskKey, op, this::merge).getValue(); + } + + @Override + public boolean delete(String key) { + tasks.merge(key, new DeleteOperation(key), this::merge); + return true; + } + + @Override + public long delete(QueryParameters queryParameters) { + log.tracef("Adding operation DELETE_BULK"); + + K artificialKey = keyConverter.yieldNewUniqueKey(); + + // Remove all tasks that create / update / delete objects deleted by the bulk removal. + final BulkDeleteOperation bdo = new BulkDeleteOperation(queryParameters); + Predicate filterForNonDeletedObjects = bdo.getFilterForNonDeletedObjects(); + long res = 0; + for (Iterator> it = tasks.entrySet().iterator(); it.hasNext();) { + Entry me = it.next(); + if (! filterForNonDeletedObjects.test(me.getValue().getValue())) { + log.tracef(" [DELETE_BULK] removing %s", me.getKey()); + it.remove(); + res++; + } + } + + tasks.put(keyConverter.keyToString(artificialKey), bdo); + + return res + bdo.getCount(); + } + + @Override + public boolean exists(String key) { + if (tasks.containsKey(key)) { + MapTaskWithValue o = tasks.get(key); + return o.getValue() != null; + } + + // Check if there is a bulk delete operation in which case read the full entity + for (MapTaskWithValue val : tasks.values()) { + if (val instanceof ConcurrentHashMapStorage.BulkDeleteOperation) { + return read(key) != null; + } + } + + return map.exists(key); + } + + private Stream createdValuesStream(Predicate keyFilter, Predicate entityFilter) { + return this.tasks.entrySet().stream() + .filter(me -> keyFilter.test(keyConverter.fromStringSafe(me.getKey()))) + .map(Map.Entry::getValue) + .filter(v -> v.containsCreate() && ! v.isReplace()) + .map(MapTaskWithValue::getValue) + .filter(Objects::nonNull) + .filter(entityFilter) + // make a snapshot + .collect(Collectors.toList()).stream(); + } + + private MapTaskWithValue merge(MapTaskWithValue oldValue, MapTaskWithValue newValue) { + switch (newValue.getOperation()) { + case DELETE: + return newValue; + default: + return new MapTaskCompose(oldValue, newValue); + } + } + + protected abstract class MapTaskWithValue { + protected final V value; + private final String realmId; + + public MapTaskWithValue(V value) { + this.value = value; + this.realmId = ConcurrentHashMapStorage.this.realmId; + } + + public V getValue() { + return value; + } + + public boolean containsCreate() { + return MapOperation.CREATE == getOperation(); + } + + public boolean containsRemove() { + return MapOperation.DELETE == getOperation(); + } + + public boolean isReplace() { + return false; + } + + public String getRealmId() { + return realmId; + } + + public abstract MapOperation getOperation(); + public abstract void execute(); + } + + private class MapTaskCompose extends MapTaskWithValue { + + private final MapTaskWithValue oldValue; + private final MapTaskWithValue newValue; + + public MapTaskCompose(MapTaskWithValue oldValue, MapTaskWithValue newValue) { + super(null); + this.oldValue = oldValue; + this.newValue = newValue; + } + + @Override + public void execute() { + oldValue.execute(); + newValue.execute(); + } + + @Override + public V getValue() { + return newValue.getValue(); + } + + @Override + public MapOperation getOperation() { + return null; + } + + @Override + public boolean containsCreate() { + return oldValue.containsCreate() || newValue.containsCreate(); + } + + @Override + public boolean containsRemove() { + return oldValue.containsRemove() || newValue.containsRemove(); + } + + @Override + public boolean isReplace() { + return (newValue.getOperation() == MapOperation.CREATE && oldValue.containsRemove()) || + (oldValue instanceof ConcurrentHashMapStorage.MapTaskCompose && ((MapTaskCompose) oldValue).isReplace()); + } + } + + private class CreateOperation extends MapTaskWithValue { + public CreateOperation(V value) { + super(value); + } + + @Override public void execute() { map.create(getValue()); } + @Override public MapOperation getOperation() { return MapOperation.CREATE; } + } + + private class DeleteOperation extends MapTaskWithValue { + private final String key; + + public DeleteOperation(String key) { + super(null); + this.key = key; + } + + @Override public void execute() { map.delete(key); } + @Override public MapOperation getOperation() { return MapOperation.DELETE; } + } + + private class BulkDeleteOperation extends MapTaskWithValue { + + private final QueryParameters queryParameters; + + public BulkDeleteOperation(QueryParameters queryParameters) { + super(null); + this.queryParameters = queryParameters; + } + + @Override + @SuppressWarnings("unchecked") + public void execute() { + map.delete(queryParameters); + } + + public Predicate getFilterForNonDeletedObjects() { + DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder(); + MapModelCriteriaBuilder mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder()); + + Predicate entityFilter = mmcb.getEntityFilter(); + Predicate keyFilter = mmcb.getKeyFilter(); + return v -> v == null || ! (keyFilter.test(keyConverter.fromStringSafe(v.getId())) && entityFilter.test(v)); + } + + @Override + public MapOperation getOperation() { + return MapOperation.DELETE; + } + + private long getCount() { + return map.getCount(queryParameters); + } + } + + @Override + public String getRealmId() { + if (mapHasRealmId) { + return ((HasRealmId) map).getRealmId(); + } + return null; + } + + @Override + @SuppressWarnings("unchecked") + public void setRealmId(String realmId) { + if (mapHasRealmId) { + ((HasRealmId) map).setRealmId(realmId); + this.realmId = realmId; + } else { + this.realmId = null; + } + } + + private V postProcess(V value) { + return (realmId == null || value == null) + ? value + : ModelEntityUtil.supplyReadOnlyFieldValueIfUnset(value, realmIdEntityField, realmId); + } + + private Stream postProcess(Stream stream) { + if (this.realmId == null) { + return stream; + } + + String localRealmId = this.realmId; + return stream.map((V value) -> ModelEntityUtil.supplyReadOnlyFieldValueIfUnset(value, realmIdEntityField, localRealmId)); + } + } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java index cfc552ae17..9ce7253d03 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProvider.java @@ -16,21 +16,32 @@ */ package org.keycloak.models.map.storage.chm; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.SessionAttributesUtils; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.CrudOperations; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag; +import static org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory.CLONER; + /** * * @author hmlnarik */ public class ConcurrentHashMapStorageProvider implements MapStorageProvider { + private final KeycloakSession session; private final ConcurrentHashMapStorageProviderFactory factory; + private final int factoryId; - public ConcurrentHashMapStorageProvider(ConcurrentHashMapStorageProviderFactory factory) { + public ConcurrentHashMapStorageProvider(KeycloakSession session, ConcurrentHashMapStorageProviderFactory factory, int factoryId) { + this.session = session; this.factory = factory; + this.factoryId = factoryId; } @Override @@ -39,8 +50,18 @@ public class ConcurrentHashMapStorageProvider implements MapStorageProvider { @Override @SuppressWarnings("unchecked") - public MapStorage getStorage(Class modelType, Flag... flags) { - ConcurrentHashMapStorage storage = factory.getStorage(modelType, flags); - return (MapStorage) storage; + public MapStorage getMapStorage(Class modelType, Flag... flags) { + return SessionAttributesUtils.createMapStorageIfAbsent(session, getClass(), modelType, factoryId, () -> { + ConcurrentHashMapStorage store = getMapStorage(modelType, factory.getStorage(modelType, flags)); + session.getTransactionManager().enlist(store); + return store; + }); + } + + private ConcurrentHashMapStorage getMapStorage(Class modelType, CrudOperations crud) { + if (modelType == SingleUseObjectValueModel.class) { + return new SingleUseObjectMapStorage(crud, factory.getKeyConverter(modelType), CLONER, MapFieldPredicates.getPredicates(modelType)); + } + return new ConcurrentHashMapStorage(crud, factory.getKeyConverter(modelType), CLONER, MapFieldPredicates.getPredicates(modelType)); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java index 5d78b863e1..992ba88bf3 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java @@ -17,6 +17,7 @@ package org.keycloak.models.map.storage.chm; import org.keycloak.models.SingleUseObjectValueModel; +import org.keycloak.models.map.common.SessionAttributesUtils; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity; import org.keycloak.models.map.authSession.MapAuthenticationSessionEntityImpl; @@ -65,6 +66,7 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntityImpl; +import org.keycloak.models.map.storage.CrudOperations; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.user.MapUserConsentEntityImpl; @@ -84,6 +86,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.Map; +import static org.keycloak.models.map.common.SessionAttributesUtils.grabNewFactoryIdentifier; import static org.keycloak.models.map.storage.ModelEntityUtil.getModelName; import static org.keycloak.models.map.storage.ModelEntityUtil.getModelNames; import static org.keycloak.models.map.storage.QueryParameters.withCriteria; @@ -99,7 +102,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide private static final Logger LOG = Logger.getLogger(ConcurrentHashMapStorageProviderFactory.class); - private final ConcurrentHashMap> storages = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> storages = new ConcurrentHashMap<>(); private final Map keyConverters = new HashMap<>(); @@ -109,7 +112,9 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide private StringKeyConverter defaultKeyConverter; - private final static DeepCloner CLONER = new DeepCloner.Builder() + private final int factoryId = grabNewFactoryIdentifier(); + + protected final static DeepCloner CLONER = new DeepCloner.Builder() .genericCloner(Serialization::from) .constructor(MapClientEntityImpl.class, MapClientEntityImpl::new) .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) @@ -156,7 +161,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide @Override public MapStorageProvider create(KeycloakSession session) { - return new ConcurrentHashMapStorageProvider(this); + return SessionAttributesUtils.createProviderIfAbsent(session, factoryId, ConcurrentHashMapStorageProvider.class, session1 -> new ConcurrentHashMapStorageProvider(session, this, factoryId)); } @@ -213,7 +218,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide } @SuppressWarnings("unchecked") - private void storeMap(String mapName, ConcurrentHashMapStorage store) { + private void storeMap(String mapName, CrudOperations store) { if (mapName != null) { File f = getFile(mapName); try { @@ -231,22 +236,23 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide } @SuppressWarnings("unchecked") - private ConcurrentHashMapStorage loadMap(String mapName, - Class modelType, EnumSet flags) { + private CrudOperations loadMap(String mapName, + Class modelType, + EnumSet flags) { final StringKeyConverter kc = keyConverters.getOrDefault(mapName, defaultKeyConverter); Class valueType = ModelEntityUtil.getEntityType(modelType); LOG.debugf("Initializing new map storage: %s", mapName); - ConcurrentHashMapStorage store; + CrudOperations store; if(modelType == SingleUseObjectValueModel.class) { - store = new SingleUseObjectConcurrentHashMapStorage(kc, CLONER) { + store = new SingleUseObjectConcurrentHashMapCrudOperations(kc, CLONER) { @Override public String toString() { return "ConcurrentHashMapStorage(" + mapName + suffix + ")"; } }; } else { - store = new ConcurrentHashMapStorage(modelType, kc, CLONER) { + store = new ConcurrentHashMapCrudOperations(modelType, kc, CLONER) { @Override public String toString() { return "ConcurrentHashMapStorage(" + mapName + suffix + ")"; @@ -282,7 +288,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide } @SuppressWarnings("unchecked") - public ConcurrentHashMapStorage getStorage( + public CrudOperations getStorage( Class modelType, Flag... flags) { EnumSet f = flags == null || flags.length == 0 ? EnumSet.noneOf(Flag.class) : EnumSet.of(flags[0], flags); String name = getModelName(modelType, modelType.getSimpleName()); @@ -290,7 +296,12 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide * * "... the computation [...] must not attempt to update any other mappings of this map." */ - return (ConcurrentHashMapStorage) storages.computeIfAbsent(name, n -> loadMap(name, modelType, f)); + + return (CrudOperations) storages.computeIfAbsent(name, n -> loadMap(name, modelType, f)); + } + + public StringKeyConverter getKeyConverter(Class modelType) { + return keyConverters.getOrDefault(getModelName(modelType, modelType.getSimpleName()), defaultKeyConverter); } private File getFile(String fileName) { diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapCrudOperations.java similarity index 68% rename from model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java rename to model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapCrudOperations.java index 95e5ecbae9..90695f0fd1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapCrudOperations.java @@ -18,12 +18,10 @@ package org.keycloak.models.map.storage.chm; import org.keycloak.models.SingleUseObjectValueModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -32,25 +30,12 @@ import java.util.stream.Stream; /** * @author Martin Kanis */ -public class SingleUseObjectConcurrentHashMapStorage extends ConcurrentHashMapStorage { +public class SingleUseObjectConcurrentHashMapCrudOperations extends ConcurrentHashMapCrudOperations { - public SingleUseObjectConcurrentHashMapStorage(StringKeyConverter keyConverter, DeepCloner cloner) { + public SingleUseObjectConcurrentHashMapCrudOperations(StringKeyConverter keyConverter, DeepCloner cloner) { super(SingleUseObjectValueModel.class, keyConverter, cloner); } - @Override - @SuppressWarnings("unchecked") - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - MapKeycloakTransaction singleUseObjectTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); - - if (singleUseObjectTransaction == null) { - singleUseObjectTransaction = new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates); - session.setAttribute("map-transaction-" + hashCode(), singleUseObjectTransaction); - } - - return singleUseObjectTransaction; - } - @Override public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) { if (value.getId() == null) { diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectMapStorage.java similarity index 75% rename from model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectKeycloakTransaction.java rename to model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectMapStorage.java index 408d1c87ee..fa835ba4b1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectKeycloakTransaction.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectMapStorage.java @@ -21,6 +21,7 @@ import org.keycloak.models.SingleUseObjectValueModel; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.StringKeyConverter; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; +import org.keycloak.models.map.storage.CrudOperations; import org.keycloak.storage.SearchableModelField; import java.util.Map; @@ -28,12 +29,12 @@ import java.util.Map; /** * @author Martin Kanis */ -public class SingleUseObjectKeycloakTransaction extends ConcurrentHashMapKeycloakTransaction { +public class SingleUseObjectMapStorage extends ConcurrentHashMapStorage { - public SingleUseObjectKeycloakTransaction(ConcurrentHashMapCrudOperations map, - StringKeyConverter keyConverter, - DeepCloner cloner, - Map, + public SingleUseObjectMapStorage(CrudOperations map, + StringKeyConverter keyConverter, + DeepCloner cloner, + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates) { super(map, keyConverter, cloner, fieldPredicates); } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java index 5fe1a268bb..99ca093d30 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/tree/EmptyMapStorage.java @@ -16,9 +16,7 @@ */ package org.keycloak.models.map.storage.tree; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.QueryParameters; import java.util.stream.Stream; @@ -27,18 +25,10 @@ import java.util.stream.Stream; * * @author hmlnarik */ -public class EmptyMapStorage implements MapStorage { +public class EmptyMapStorage { - private static final EmptyMapStorage INSTANCE = new EmptyMapStorage<>(); - - @SuppressWarnings("unchecked") - public static EmptyMapStorage getInstance() { - return (EmptyMapStorage) INSTANCE; - } - - @Override - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - return new MapKeycloakTransaction() { + public static MapStorage getInstance() { + return new MapStorage<>() { @Override public V create(V value) { return null; @@ -68,33 +58,6 @@ public class EmptyMapStorage implements MapStorage< public long delete(QueryParameters queryParameters) { return 0; } - - @Override - public void begin() { - } - - @Override - public void commit() { - } - - @Override - public void rollback() { - } - - @Override - public void setRollbackOnly() { - } - - @Override - public boolean getRollbackOnly() { - return false; - } - - @Override - public boolean isActive() { - return true; - } }; } - } 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 9b8798250d..83cef98a55 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 @@ -50,8 +50,7 @@ import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.HasRealmId; import org.keycloak.models.map.common.TimeAdapter; import org.keycloak.models.map.credential.MapUserCredentialManager; -import org.keycloak.models.map.storage.MapKeycloakTransactionWithAuth; -import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorageWithAuth; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; @@ -88,14 +87,13 @@ public class MapUserProvider implements UserProvider { private static final Logger LOG = Logger.getLogger(MapUserProvider.class); private final KeycloakSession session; - final MapKeycloakTransaction tx; - private final boolean txHasRealmId; + final MapStorage store; + private final boolean storeHasRealmId; public MapUserProvider(KeycloakSession session, MapStorage store) { this.session = session; - this.tx = store.createTransaction(session); - session.getTransactionManager().enlist(tx); - this.txHasRealmId = tx instanceof HasRealmId; + this.store = store; + this.storeHasRealmId = store instanceof HasRealmId; } private Function entityToAdapterFunc(RealmModel realm) { @@ -118,11 +116,11 @@ public class MapUserProvider implements UserProvider { }; } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { - ((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId()); + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { + ((HasRealmId) store).setRealmId(realm == null ? null : realm.getId()); } - return tx; + return store; } private Predicate entityRealmFilter(RealmModel realm) { @@ -139,7 +137,7 @@ public class MapUserProvider implements UserProvider { private Optional getEntityById(RealmModel realm, String id) { try { - MapUserEntity mapUserEntity = txInRealm(realm).read(id); + MapUserEntity mapUserEntity = storeWithRealm(realm).read(id); if (mapUserEntity != null && entityRealmFilter(realm).test(mapUserEntity)) { return Optional.of(mapUserEntity); } @@ -186,7 +184,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.IDP_AND_USER, Operator.EQ, socialProvider); - txInRealm(realm).read(withCriteria(mcb)) + storeWithRealm(realm).read(withCriteria(mcb)) .forEach(userEntity -> userEntity.removeFederatedIdentity(socialProvider)); } @@ -228,7 +226,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.IDP_AND_USER, Operator.EQ, socialLink.getIdentityProvider(), socialLink.getUserId()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .collect(Collectors.collectingAndThen( Collectors.toList(), list -> { @@ -322,7 +320,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.SERVICE_ACCOUNT_CLIENT, Operator.EQ, client.getId()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .collect(Collectors.collectingAndThen(Collectors.toList(), list -> { if (list.isEmpty()) { @@ -346,11 +344,11 @@ public class MapUserProvider implements UserProvider { SearchableFields.USERNAME : SearchableFields.USERNAME_CASE_INSENSITIVE, Operator.EQ, username); - if (txInRealm(realm).exists(withCriteria(mcb))) { + if (storeWithRealm(realm).exists(withCriteria(mcb))) { throw new ModelDuplicateException("User with username '" + username + "' in realm " + realm.getName() + " already exists" ); } - if (id != null && txInRealm(realm).exists(id)) { + if (id != null && storeWithRealm(realm).exists(id)) { throw new ModelDuplicateException("User exists: " + id); } @@ -361,7 +359,7 @@ public class MapUserProvider implements UserProvider { entity.setUsername(username); entity.setCreatedTimestamp(Time.currentTimeMillis()); - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); final UserModel userModel = entityToAdapterFunc(realm).apply(entity); if (addDefaultRoles) { @@ -388,7 +386,7 @@ public class MapUserProvider implements UserProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override @@ -398,7 +396,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.FEDERATION_LINK, Operator.EQ, storageProviderId); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override @@ -408,7 +406,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.FEDERATION_LINK, Operator.EQ, storageProviderId); - try (Stream s = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb))) { s.forEach(userEntity -> userEntity.setFederationLink(null)); } } @@ -421,7 +419,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ASSIGNED_ROLE, Operator.EQ, roleId); - try (Stream s = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb))) { s.forEach(userEntity -> userEntity.removeRolesMembership(roleId)); } } @@ -434,7 +432,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ASSIGNED_GROUP, Operator.EQ, groupId); - try (Stream s = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb))) { s.forEach(userEntity -> userEntity.removeGroupsMembership(groupId)); } } @@ -447,7 +445,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.CONSENT_FOR_CLIENT, Operator.EQ, clientId); - try (Stream s = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb))) { s.forEach(userEntity -> userEntity.removeUserConsent(clientId)); } } @@ -467,7 +465,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.CONSENT_WITH_CLIENT_SCOPE, Operator.EQ, clientScopeId); - try (Stream s = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb))) { s.map(MapUserEntity::getUserConsents) .filter(Objects::nonNull) .flatMap(Collection::stream) @@ -486,7 +484,7 @@ public class MapUserProvider implements UserProvider { DefaultModelCriteria mcb = criteria(); mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()); - try (Stream s = txInRealm(realm).read(withCriteria(mcb))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb))) { s.forEach(entity -> entity.addRolesMembership(roleId)); } } @@ -508,7 +506,7 @@ public class MapUserProvider implements UserProvider { SearchableFields.USERNAME_CASE_INSENSITIVE, Operator.EQ, username); // there is orderBy used to always return the same user in case multiple users are returned from the store - try (Stream s = txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING))) { + try (Stream s = storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING))) { List users = s.collect(Collectors.toList()); if (users.isEmpty()) return null; if (users.size() != 1) { @@ -526,7 +524,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.EMAIL, Operator.EQ, email); - List usersWithEmail = txInRealm(realm).read(withCriteria(mcb)).collect(Collectors.toList()); + List usersWithEmail = storeWithRealm(realm).read(withCriteria(mcb)).collect(Collectors.toList()); if (usersWithEmail.isEmpty()) return null; if (usersWithEmail.size() > 1) { @@ -558,7 +556,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.SERVICE_ACCOUNT_CLIENT, Operator.NOT_EXISTS); } - return (int) txInRealm(realm).getCount(withCriteria(mcb)); + return (int) storeWithRealm(realm).getCount(withCriteria(mcb)); } @Override @@ -676,7 +674,7 @@ public class MapUserProvider implements UserProvider { criteria = criteria.compare(SearchableFields.ASSIGNED_GROUP, Operator.IN, authorizedGroups); } - return txInRealm(realm).read(withCriteria(criteria).pagination(firstResult, maxResults, SearchableFields.USERNAME)) + return storeWithRealm(realm).read(withCriteria(criteria).pagination(firstResult, maxResults, SearchableFields.USERNAME)) .map(entityToAdapterFunc(realm)) .filter(Objects::nonNull); } @@ -688,7 +686,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ASSIGNED_GROUP, Operator.EQ, group.getId()); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.USERNAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.USERNAME)) .map(entityToAdapterFunc(realm)); } @@ -699,7 +697,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ATTRIBUTE, Operator.EQ, attrName, attrValue); - return txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING)) + return storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING)) .map(entityToAdapterFunc(realm)); } @@ -716,7 +714,7 @@ public class MapUserProvider implements UserProvider { if (userById.isPresent()) { session.invalidate(USER_BEFORE_REMOVE, realm, user); - txInRealm(realm).delete(userId); + storeWithRealm(realm).delete(userId); session.invalidate(USER_AFTER_REMOVE, realm, user); return true; @@ -732,7 +730,7 @@ public class MapUserProvider implements UserProvider { mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) .compare(SearchableFields.ASSIGNED_ROLE, Operator.EQ, role.getId()); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.USERNAME)) + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.USERNAME)) .map(entityToAdapterFunc(realm)); } @@ -758,8 +756,8 @@ public class MapUserProvider implements UserProvider { .filter(Objects::nonNull) .findFirst().orElse(null); - if (r == null && tx instanceof MapKeycloakTransactionWithAuth) { - MapCredentialValidationOutput result = ((MapKeycloakTransactionWithAuth) tx).authenticate(realm, input); + if (r == null && store instanceof MapStorageWithAuth) { + MapCredentialValidationOutput result = ((MapStorageWithAuth) store).authenticate(realm, input); if (result != null) { UserModel user = null; if (result.getAuthenticatedUser() != null) { diff --git a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java index 1fac8b044c..4fb85140e1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/user/MapUserProviderFactory.java @@ -46,7 +46,7 @@ public class MapUserProviderFactory extends AbstractMapProviderFactory userSessionTx; + protected final MapStorage userSessionTx; /** * Storage for transient user sessions which lifespan is limited to one request. */ private final Map transientUserSessions = new HashMap<>(); - private final boolean txHasRealmId; + private final boolean storeHasRealmId; public MapUserSessionProvider(KeycloakSession session, MapStorage userSessionStore) { this.session = session; - userSessionTx = userSessionStore.createTransaction(session); - - session.getTransactionManager().enlistAfterCompletion(userSessionTx); - this.txHasRealmId = userSessionTx instanceof HasRealmId; + this.userSessionTx = userSessionStore; + this.storeHasRealmId = userSessionTx instanceof HasRealmId; } private Function userEntityToAdapterFunc(RealmModel realm) { @@ -88,7 +84,7 @@ public class MapUserSessionProvider implements UserSessionProvider { if (TRANSIENT == origEntity.getPersistenceState()) { transientUserSessions.remove(origEntity.getId()); } else { - txInRealm(realm).delete(origEntity.getId()); + storeWithRealm(realm).delete(origEntity.getId()); } return null; } else { @@ -97,8 +93,8 @@ public class MapUserSessionProvider implements UserSessionProvider { }; } - private MapKeycloakTransaction txInRealm(RealmModel realm) { - if (txHasRealmId) { + private MapStorage storeWithRealm(RealmModel realm) { + if (storeHasRealmId) { ((HasRealmId) userSessionTx).setRealmId(realm == null ? null : realm.getId()); } return userSessionTx; @@ -161,10 +157,10 @@ public class MapUserSessionProvider implements UserSessionProvider { } transientUserSessions.put(entity.getId(), entity); } else { - if (id != null && txInRealm(realm).exists(id)) { + if (id != null && storeWithRealm(realm).exists(id)) { throw new ModelDuplicateException("User session exists: " + id); } - entity = txInRealm(realm).create(entity); + entity = storeWithRealm(realm).create(entity); } entity.setPersistenceState(persistenceState); @@ -190,7 +186,7 @@ public class MapUserSessionProvider implements UserSessionProvider { DefaultModelCriteria mcb = realmAndOfflineCriteriaBuilder(realm, false) .compare(UserSessionModel.SearchableFields.ID, Operator.EQ, id); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .findFirst() .map(userEntityToAdapterFunc(realm)) .orElse(null); @@ -203,7 +199,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getUserSessionsStream(%s, %s)%s", realm, user, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); } @@ -215,7 +211,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getUserSessionsStream(%s, %s)%s", realm, client, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); } @@ -229,7 +225,7 @@ public class MapUserSessionProvider implements UserSessionProvider { .compare(UserSessionModel.SearchableFields.CLIENT_ID, Operator.EQ, client.getId()); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, UserSessionModel.SearchableFields.LAST_SESSION_REFRESH)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); @@ -242,7 +238,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getUserSessionByBrokerUserIdStream(%s, %s)%s", realm, brokerUserId, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); } @@ -254,7 +250,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getUserSessionByBrokerSessionId(%s, %s)%s", realm, brokerSessionId, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .findFirst() .map(userEntityToAdapterFunc(realm)) .orElse(null); @@ -287,7 +283,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getActiveUserSessions(%s, %s)%s", realm, client, getShortStackTrace()); - return txInRealm(realm).getCount(withCriteria(mcb)); + return storeWithRealm(realm).getCount(withCriteria(mcb)); } @Override @@ -296,7 +292,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getActiveClientSessionStats(%s, %s)%s", realm, offline, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull) .map(UserSessionModel::getAuthenticatedClientSessions) @@ -314,7 +310,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("removeUserSession(%s, %s)%s", realm, session, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override @@ -325,7 +321,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("removeUserSessions(%s, %s)%s", realm, user, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override @@ -344,7 +340,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("removeUserSessions(%s)%s", realm, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } @Override @@ -363,7 +359,7 @@ public class MapUserSessionProvider implements UserSessionProvider { MapUserSessionEntity offlineUserSession = createUserSessionEntityInstance(userSession, true); RealmModel realm = userSession.getRealm(); - offlineUserSession = txInRealm(realm).create(offlineUserSession); + offlineUserSession = storeWithRealm(realm).create(offlineUserSession); // set a reference for the offline user session to the original online user session userSession.setNote(CORRESPONDING_SESSION_ID, offlineUserSession.getId()); @@ -394,12 +390,12 @@ public class MapUserSessionProvider implements UserSessionProvider { DefaultModelCriteria mcb; if (userSession.isOffline()) { - txInRealm(realm).delete(userSession.getId()); + storeWithRealm(realm).delete(userSession.getId()); } else if (userSession.getNote(CORRESPONDING_SESSION_ID) != null) { String uk = userSession.getNote(CORRESPONDING_SESSION_ID); mcb = realmAndOfflineCriteriaBuilder(realm, true) .compare(UserSessionModel.SearchableFields.ID, Operator.EQ, uk); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); userSession.removeNote(CORRESPONDING_SESSION_ID); } } @@ -440,7 +436,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getOfflineUserSessionsStream(%s, %s)%s", realm, user, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); } @@ -452,7 +448,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getOfflineUserSessionByBrokerSessionId(%s, %s)%s", realm, brokerSessionId, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .findFirst() .map(userEntityToAdapterFunc(realm)) .orElse(null); @@ -465,7 +461,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getOfflineUserSessionByBrokerUserIdStream(%s, %s)%s", realm, brokerUserId, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb)) + return storeWithRealm(realm).read(withCriteria(mcb)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); } @@ -477,7 +473,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getOfflineSessionsCount(%s, %s)%s", realm, client, getShortStackTrace()); - return txInRealm(realm).getCount(withCriteria(mcb)); + return storeWithRealm(realm).getCount(withCriteria(mcb)); } @Override @@ -488,7 +484,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("getOfflineUserSessionsStream(%s, %s, %s, %s)%s", realm, client, firstResult, maxResults, getShortStackTrace()); - return txInRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, + return storeWithRealm(realm).read(withCriteria(mcb).pagination(firstResult, maxResults, UserSessionModel.SearchableFields.LAST_SESSION_REFRESH)) .map(userEntityToAdapterFunc(realm)) .filter(Objects::nonNull); @@ -539,7 +535,7 @@ public class MapUserSessionProvider implements UserSessionProvider { LOG.tracef("removeAllUserSessions(%s)%s", realm, getShortStackTrace()); - txInRealm(realm).delete(withCriteria(mcb)); + storeWithRealm(realm).delete(withCriteria(mcb)); } private Stream getOfflineUserSessionEntityStream(RealmModel realm, String userSessionId) { @@ -553,7 +549,7 @@ public class MapUserSessionProvider implements UserSessionProvider { .compare(UserSessionModel.SearchableFields.ID, Operator.EQ, userSessionId); // check if it's an offline user session - MapUserSessionEntity userSessionEntity = txInRealm(realm).read(withCriteria(mcb)).findFirst().orElse(null); + MapUserSessionEntity userSessionEntity = storeWithRealm(realm).read(withCriteria(mcb)).findFirst().orElse(null); if (userSessionEntity != null) { if (Boolean.TRUE.equals(userSessionEntity.isOffline())) { return Stream.of(userSessionEntity); @@ -562,7 +558,7 @@ public class MapUserSessionProvider implements UserSessionProvider { // no session found by the given ID, try to find by corresponding session ID mcb = realmAndOfflineCriteriaBuilder(realm, true) .compare(UserSessionModel.SearchableFields.CORRESPONDING_SESSION_ID, Operator.EQ, userSessionId); - return txInRealm(realm).read(withCriteria(mcb)); + return storeWithRealm(realm).read(withCriteria(mcb)); } // it's online user session so lookup offline user session by corresponding session id reference @@ -570,7 +566,7 @@ public class MapUserSessionProvider implements UserSessionProvider { if (offlineUserSessionId != null) { mcb = realmAndOfflineCriteriaBuilder(realm, true) .compare(UserSessionModel.SearchableFields.ID, Operator.EQ, offlineUserSessionId); - return txInRealm(realm).read(withCriteria(mcb)); + return storeWithRealm(realm).read(withCriteria(mcb)); } return Stream.empty(); @@ -588,7 +584,7 @@ public class MapUserSessionProvider implements UserSessionProvider { MapUserSessionEntity userSessionEntity = transientUserSessions.get(id); if (userSessionEntity == null) { - MapUserSessionEntity userSession = txInRealm(realm).read(id); + MapUserSessionEntity userSession = storeWithRealm(realm).read(id); return userSession; } return userSessionEntity; diff --git a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java index 21789de4f8..20899bcb2c 100644 --- a/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/userSession/MapUserSessionProviderFactory.java @@ -39,7 +39,7 @@ public class MapUserSessionProviderFactory extends AbstractMapProviderFactory { - ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class); - ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class); - ConcurrentHashMapStorage storage2 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class); + ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getMapStorage(ClientModel.class); + ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getMapStorage(ClientModel.class); + ConcurrentHashMapStorage storage2 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getMapStorage(ClientModel.class); // Assert that the map storage can be used both as a standalone store and a component assertThat(storageMain, notNullValue()); assertThat(storage1, notNullValue()); assertThat(storage2, notNullValue()); - final StringKeyConverter kcMain = storageMain.getKeyConverter(); - final StringKeyConverter kc1 = storage1.getKeyConverter(); - final StringKeyConverter kc2 = storage2.getKeyConverter(); + final StringKeyConverter kcMain = (StringKeyConverter) StringKeyConverter.UUIDKey.INSTANCE; + final StringKeyConverter kc1 = (StringKeyConverter) StringKeyConverter.ULongKey.INSTANCE; + final StringKeyConverter kc2 = (StringKeyConverter) StringKeyConverter.StringKey.INSTANCE; String idMain = kcMain.keyToString(kcMain.yieldNewUniqueKey()); String id1 = kc1.keyToString(kc1.yieldNewUniqueKey()); @@ -144,11 +145,11 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest { assertClientsPersisted(component1Id, component2Id, idMain, id1, id2); // Invalidate one component and check that the storage still contains what it should - getFactory().invalidate(null, ObjectType.COMPONENT, component1Id); + getFactory().invalidate(null, InvalidationHandler.ObjectType.COMPONENT, component1Id); assertClientsPersisted(component1Id, component2Id, idMain, id1, id2); // Invalidate whole realm and check that the storage still contains what it should - getFactory().invalidate(null, ObjectType.REALM, realmId); + getFactory().invalidate(null, InvalidationHandler.ObjectType.REALM, realmId); assertClientsPersisted(component1Id, component2Id, idMain, id1, id2); // Refresh factory (akin server restart) and check that the storage still contains what it should @@ -169,15 +170,15 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest { // Check that in the next transaction, the objects are still there withRealm(realmId, (session, realm) -> { @SuppressWarnings("unchecked") - ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class); + ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getMapStorage(ClientModel.class); @SuppressWarnings("unchecked") - ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class); + ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getMapStorage(ClientModel.class); @SuppressWarnings("unchecked") - ConcurrentHashMapStorage storage2 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class); + ConcurrentHashMapStorage storage2 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getMapStorage(ClientModel.class); - final StringKeyConverter kcMain = storageMain.getKeyConverter(); - final StringKeyConverter kc1 = storage1.getKeyConverter(); - final StringKeyConverter kc2 = storage2.getKeyConverter(); + final StringKeyConverter kcMain = (StringKeyConverter) StringKeyConverter.UUIDKey.INSTANCE; + final StringKeyConverter kc1 = (StringKeyConverter) StringKeyConverter.ULongKey.INSTANCE; + final StringKeyConverter kc2 = (StringKeyConverter) StringKeyConverter.StringKey.INSTANCE; // Assert that the stores contain the created clients assertThat(storageMain.read(idMain), notNullValue()); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java index e29e7ccd20..994bb749cc 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/storage/tree/sample/DictStorage.java @@ -16,12 +16,11 @@ */ package org.keycloak.testsuite.model.storage.tree.sample; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.common.DeepCloner; -import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; import org.keycloak.models.map.storage.QueryParameters; + import java.util.List; import java.util.Objects; import java.util.stream.Stream; @@ -45,76 +44,38 @@ public class DictStorage implements MapStorage { - - @Override - public V create(V value) { - V res = cloner.from(value); - store.add(res); - return res; - } - - @Override - public V read(String key) { - return store.stream() - .filter(e -> Objects.equals(e.getId(), key)) - .findFirst() - .orElse(null); - } - - @Override - public Stream read(QueryParameters queryParameters) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public long getCount(QueryParameters queryParameters) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public boolean delete(String key) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public long delete(QueryParameters queryParameters) { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void begin() { - } - - @Override - public void commit() { - } - - @Override - public void rollback() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public void setRollbackOnly() { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } - - @Override - public boolean getRollbackOnly() { - return false; - } - - @Override - public boolean isActive() { - return true; - } - + @Override + public V create(V value) { + V res = cloner.from(value); + store.add(res); + return res; } @Override - public MapKeycloakTransaction createTransaction(KeycloakSession session) { - return new Transaction(); + public V read(String key) { + return store.stream() + .filter(e -> Objects.equals(e.getId(), key)) + .findFirst() + .orElse(null); } + @Override + public Stream read(QueryParameters queryParameters) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public long getCount(QueryParameters queryParameters) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public boolean delete(String key) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public long delete(QueryParameters queryParameters) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } }