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 <hmlnarik@redhat.com>
This commit is contained in:
Michal Hajas 2023-04-12 11:21:14 +02:00 committed by GitHub
parent 1ee98bbbe7
commit b730d861e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 2404 additions and 2747 deletions

View file

@ -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<V extends AbstractEntity & UpdatableEntity, M> implements CrudOperations<V, M>, HasRealmId {
private static final Logger LOG = Logger.getLogger(FileCrudOperations.class);
private String defaultRealmId;
private final Class<V> entityClass;
private final Function<String, Path> dataDirectoryFunc;
private final Function<V, String[]> suggestedPath;
private final boolean isExpirableEntity;
private final Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, V, M>> fieldPredicates;
private static final Map<Class<?>, Map<SearchableModelField<?>, 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<V> entityClass,
Function<String, Path> dataDirectoryFunc,
Function<V, String[]> 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 <V extends AbstractEntity & UpdatableEntity, M> Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, V, M>> getPredicates(Class<V> entityClass) {
return (Map) ENTITY_FIELD_PREDICATES.computeIfAbsent(entityClass, n -> {
Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, V, M>> 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<String, V, M> createCriteriaBuilder() {
return new MapModelCriteriaBuilder<>(StringKeyConverter.StringKey.INSTANCE, fieldPredicates);
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
final List<Path> 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<Path> 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<V> res = paths.stream()
.filter(FileCrudOperations::canParseFile)
.map(this::parse)
.filter(Objects::nonNull);
MapModelCriteriaBuilder<String, V, M> mcb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super String> keyFilter = mcb.getKeyFilter();
Predicate<? super V> 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<M> queryParameters) {
return read(queryParameters).map(AbstractEntity::getId).map(this::delete).filter(a -> a).count();
}
@Override
public long getCount(QueryParameters<M> 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);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class FileMapKeycloakTransaction<V extends AbstractEntity & UpdatableEntity, M>
extends ConcurrentHashMapKeycloakTransaction<String, V, M> {
private static final Logger LOG = Logger.getLogger(FileMapKeycloakTransaction.class);
private final List<Path> createdPaths = new LinkedList<>();
private final List<Path> pathsToDelete = new LinkedList<>();
private final Map<Path, Path> renameOnCommit = new HashMap<>();
private final Map<Path, FileTime> lastModified = new HashMap<>();
private final String txId = StringKey.INSTANCE.yieldNewUniqueKey();
public static <V extends AbstractEntity & UpdatableEntity, M> FileMapKeycloakTransaction<V, M> newInstance(Class<V> entityClass,
Function<String, Path> dataDirectoryFunc, Function<V, String[]> suggestedPath,
boolean isExpirableEntity, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<String, V, M>> fieldPredicates) {
Crud<V, M> crud = new Crud<>(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity, fieldPredicates);
FileMapKeycloakTransaction<V, M> tx = new FileMapKeycloakTransaction<>(entityClass, crud);
crud.tx = tx;
return tx;
}
private FileMapKeycloakTransaction(Class<V> entityClass, Crud<V, M> 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<Path> 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<V extends AbstractEntity & UpdatableEntity, M> extends FileMapStorage.Crud<V, M> {
private FileMapKeycloakTransaction tx;
public Crud(Class<V> entityClass, Function<String, Path> dataDirectoryFunc, Function<V, String[]> suggestedPath, boolean isExpirableEntity, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<String, V, M>> 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<V> {
public IdProtector(V entity) {
super(entity);
}
@Override
public <T, EF extends java.lang.Enum<? extends org.keycloak.models.map.common.EntityField<V>> & org.keycloak.models.map.common.EntityField<V>> 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]";
}
}
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class FileMapStorage<V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M> {
public class FileMapStorage<V extends AbstractEntity & UpdatableEntity, M>
extends ConcurrentHashMapStorage<String, V, M> {
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<Path> createdPaths = new LinkedList<>();
private final List<Path> pathsToDelete = new LinkedList<>();
private final Map<Path, Path> renameOnCommit = new HashMap<>();
private final Map<Path, FileTime> 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<V> entityClass;
private final Function<String, Path> dataDirectoryFunc;
private final Function<V, String[]> suggestedPath;
private final boolean isExpirableEntity;
private final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<String, V, M>> fieldPredicates;
public static <V extends AbstractEntity & UpdatableEntity, M> FileMapStorage<V, M> newInstance(Class<V> entityClass,
Function<String, Path> dataDirectoryFunc, Function<V, String[]> suggestedPath,
boolean isExpirableEntity) {
Crud<V, M> crud = new Crud<>(entityClass, dataDirectoryFunc, suggestedPath, isExpirableEntity);
FileMapStorage<V, M> 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<V> entityClass, Function<V, String[]> uniqueHumanReadableField, Function<String, Path> 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<V> entityClass, Crud<V, M> crud) {
super(
crud,
StringKeyConverter.StringKey.INSTANCE,
DeepCloner.DUMB_CLONER,
MapFieldPredicates.getPredicates(ModelEntityUtil.getModelType(entityClass)),
ModelEntityUtil.getRealmIdField(entityClass)
);
}
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
@SuppressWarnings("unchecked")
MapKeycloakTransaction<V, M> 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<V, M> 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<Path> 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<V extends AbstractEntity & UpdatableEntity, M> implements ConcurrentHashMapCrudOperations<V, M>, HasRealmId {
private String defaultRealmId;
private final Class<V> entityClass;
private final Function<String, Path> dataDirectoryFunc;
private final Function<V, String[]> suggestedPath;
private final boolean isExpirableEntity;
private final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<String, V, M>> fieldPredicates;
public Crud(Class<V> entityClass, Function<String, Path> dataDirectoryFunc, Function<V, String[]> suggestedPath, boolean isExpirableEntity, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<String, V, M>> 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));
private static void silentDelete(Path p) {
silenteDelete(p, false);
}
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;
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.
}
parentDirectory = targetPath;
}
return targetPath.resolveSibling(targetPath.getFileName() + FILE_SUFFIX);
public void touch(Path path) throws IOException {
Files.createFile(path);
createdPaths.add(path);
}
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);
}
public boolean removeIfExists(Path path) {
final boolean res = ! pathsToDelete.contains(path) && Files.exists(path);
pathsToDelete.add(path);
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;
void registerRenameOnCommit(Path from, Path to) {
this.renameOnCommit.put(from, to);
}
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<String, V, M> createCriteriaBuilder() {
return new MapModelCriteriaBuilder<>(StringKey.INSTANCE, fieldPredicates);
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
final List<Path> 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<Path> 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<V> res = paths.stream()
.filter(FileMapStorage::canParseFile)
.map(this::parse)
.filter(Objects::nonNull);
MapModelCriteriaBuilder<String,V,M> mcb = queryParameters.getModelCriteriaBuilder().flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super String> keyFilter = mcb.getKeyFilter();
Predicate<? super V> 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<M> queryParameters) {
return read(queryParameters).map(AbstractEntity::getId).map(this::delete).filter(a -> a).count();
}
@Override
public long getCount(QueryParameters<M> 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}.
* 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 whose last modified time it to be obtained.
* @return the {@link FileTime} corresponding to the file's last modified time.
* @param path the {@link Path} to the file.
*/
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);
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<V extends AbstractEntity & UpdatableEntity, M> extends FileCrudOperations<V, M> {
private FileMapStorage store;
public Crud(Class<V> entityClass, Function<String, Path> dataDirectoryFunc, Function<V, String[]> 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<V> {
public IdProtector(V entity) {
super(entity);
}
@Override
public <T, EF extends java.lang.Enum<? extends org.keycloak.models.map.common.EntityField<V>> & org.keycloak.models.map.common.EntityField<V>> 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]";
}
}
}

View file

@ -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 <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
FileMapStorage storage = factory.getStorage(modelType, flags);
return (MapStorage<V, M>) storage;
public <V extends AbstractEntity, M> MapStorage<V, M> getMapStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
return (MapStorage<V, M>) SessionAttributesUtils.createMapStorageIfAbsent(session, getClass(), modelType, factoryId, () -> createFileMapStorage(modelType));
}
private <V extends AbstractEntity & UpdatableEntity, M> ConcurrentHashMapStorage<?, V, M> createFileMapStorage(Class<M> modelType) {
String areaName = getModelName(modelType, modelType.getSimpleName());
final Class<V> et = ModelEntityUtil.getEntityType(modelType);
Function<V, String[]> uniqueHumanReadableField = (Function<V, String[]>) 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

View file

@ -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<M
public static final String PROVIDER_ID = "file";
private Path rootRealmsDirectory;
private final Map<String, Function<String, Path>> rootAreaDirectories = new HashMap<>(); // Function: (realmId) -> path
private final Map<Class<?>, FileMapStorage<?, ?>> storages = new HashMap<>();
private final int factoryId = SessionAttributesUtils.grabNewFactoryIdentifier();
private static final Map<Class<?>, Function<?, String[]>> UNIQUE_HUMAN_READABLE_NAME_FIELD = Map.ofEntries(
protected static final Map<Class<?>, Function<?, String[]>> UNIQUE_HUMAN_READABLE_NAME_FIELD = Map.ofEntries(
entry(MapClientEntity.class, ((Function<MapClientEntity, String[]>) v -> new String[] { v.getClientId() })),
entry(MapClientScopeEntity.class, ((Function<MapClientScopeEntity, String[]>) v -> new String[] { v.getName() })),
entry(MapGroupEntity.class, ((Function<MapGroupEntity, String[]>) v -> v.getParentId() == null
@ -89,7 +85,7 @@ public class FileMapStorageProviderFactory implements AmphibianProviderFactory<M
@Override
public MapStorageProvider create(KeycloakSession session) {
return new FileMapStorageProvider(this);
return SessionAttributesUtils.createProviderIfAbsent(session, factoryId, FileMapStorageProvider.class, session1 -> new FileMapStorageProvider(session1, this, factoryId));
}
@Override
@ -151,22 +147,8 @@ public class FileMapStorageProviderFactory implements AmphibianProviderFactory<M
return PROVIDER_ID;
}
public <V extends AbstractEntity & UpdatableEntity, M> FileMapStorage<V, M> initFileStorage(Class<M> modelType) {
String name = getModelName(modelType, modelType.getSimpleName());
final Class<V> et = ModelEntityUtil.getEntityType(modelType);
@SuppressWarnings("unchecked")
FileMapStorage<V, M> res = new FileMapStorage<>(et, (Function<V, String[]>) UNIQUE_HUMAN_READABLE_NAME_FIELD.get(et), rootAreaDirectories.get(name));
return res;
public Function<String, Path> getDataDirectoryFunc(String areaName) {
return rootAreaDirectories.get(areaName);
}
<M> FileMapStorage getStorage(Class<M> 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);
}
}
}

View file

@ -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<K, E extends AbstractHotRodEntity, V extends AbstractEntity & HotRodEntityDelegate<E>, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
public class HotRodCrudOperations<K, E extends AbstractHotRodEntity, V extends AbstractEntity & HotRodEntityDelegate<E>, M> implements CrudOperations<V, M> {
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<K, E> remoteCache;
@ -79,13 +74,12 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
private final Function<E, V> delegateProducer;
protected final DeepCloner cloner;
protected boolean isExpirableEntity;
private final AllAreasHotRodTransactionsWrapper txWrapper;
private final Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, V, M>> fieldPredicates;
private final Long lockTimeout;
private final RemoteCache<String, String> locksCache;
private final Map<K, Long> entityVersionCache = new HashMap<>();
public HotRodMapStorage(KeycloakSession session, RemoteCache<K, E> remoteCache, StringKeyConverter<K> keyConverter, HotRodEntityDescriptor<E, V> storedEntityDescriptor, DeepCloner cloner, AllAreasHotRodTransactionsWrapper txWrapper, Long lockTimeout) {
public HotRodCrudOperations(KeycloakSession session, RemoteCache<K, E> remoteCache, StringKeyConverter<K> keyConverter, HotRodEntityDescriptor<E, V> storedEntityDescriptor, DeepCloner cloner, Long lockTimeout) {
this.session = session;
this.remoteCache = remoteCache;
this.keyConverter = keyConverter;
@ -93,7 +87,6 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
this.cloner = cloner;
this.delegateProducer = storedEntityDescriptor.getHotRodDelegateProvider();
this.isExpirableEntity = ExpirableEntity.class.isAssignableFrom(ModelEntityUtil.getEntityType(storedEntityDescriptor.getModelTypeClass()));
this.txWrapper = txWrapper;
this.fieldPredicates = MapFieldPredicates.getPredicates((Class<M>) storedEntityDescriptor.getModelTypeClass());
this.lockTimeout = lockTimeout;
HotRodConnectionProvider cacheProvider = session.getProvider(HotRodConnectionProvider.class);
@ -155,7 +148,7 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
if (entityWithMetadata == null) return null;
// store entity version
LOG.tracef("Entity %s read in version %s", key, entityWithMetadata.getVersion(), getShortStackTrace());
LOG.tracef("Entity %s read in version %s.%s", key, entityWithMetadata.getVersion(), getShortStackTrace());
entityVersionCache.put(k, entityWithMetadata.getVersion());
// Create delegate that implements Map*Entity
@ -174,10 +167,10 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
throw new OptimisticLockException("Entity " + key + " with version " + entityVersionCache.get(key) + " already changed by a different transaction.");
}
} else {
LOG.warnf("Removing entity %s from storage due to negative/zero lifespan.%s", key, getShortStackTrace());
if (!remoteCache.removeWithVersion(key, entityVersionCache.get(key))) {
throw new OptimisticLockException("Entity " + key + " with version " + entityVersionCache.get(key) + " already changed by a different transaction.");
}
LOG.warnf("Removing entity %s from storage due to negative/zero lifespan.", key);
}
return delegateProducer.apply(value.getHotRodEntity());
@ -273,7 +266,7 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
String queryString = (prefix != null ? prefix : "") + iqmcb.getIckleQuery();
if (!queryParameters.getOrderBy().isEmpty()) {
queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodMapStorage::toOrderString)
queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodCrudOperations::toOrderString)
.collect(Collectors.joining(", "));
}
LOG.tracef("Preparing Ickle query: '%s'%s", queryString, getShortStackTrace());
@ -322,18 +315,6 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
return new IckleQueryMapModelCriteriaBuilder<>(storedEntityDescriptor.getEntityTypeClass());
}
@Override
public MapKeycloakTransaction<V, M> 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<K, V, M>) txWrapper.getOrCreateTxForModel(storedEntityDescriptor.getModelTypeClass(), () -> createTransactionInternal(session)));
}
protected MapKeycloakTransaction<V, M> 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

View file

@ -18,42 +18,66 @@
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 <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> 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 <V extends AbstractEntity, M> MapStorage<V, M> getMapStorage(Class<M> 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);
return (MapStorage<V, M>) 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
@ -64,7 +88,28 @@ public class HotRodMapStorageProvider implements MapStorageProvider {
}
}
return (MapStorage<V, M>) factory.getHotRodStorage(session, modelType, txWrapper, flags);
private <K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> ConcurrentHashMapStorage<K, V, M> createHotRodMapStorage(KeycloakSession session, Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class);
HotRodEntityDescriptor<E, V> entityDescriptor = (HotRodEntityDescriptor<E, V>) factory.getEntityDescriptor(modelType);
Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, V, M>> fieldPredicates = MapFieldPredicates.getPredicates((Class<M>) entityDescriptor.getModelTypeClass());
StringKeyConverter<String> 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

View file

@ -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<MapStorageProvider>, 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<SearchableModelField<AuthenticatedClientSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<Object, AbstractEntity, AuthenticatedClientSessionModel>> clientSessionPredicates = MapFieldPredicates.basePredicates(HotRodAuthenticatedClientSessionEntity.ID);
protected static final Map<SearchableModelField<AuthenticatedClientSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<Object, AbstractEntity, AuthenticatedClientSessionModel>> 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 <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(KeycloakSession session, Class<M> 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 <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> createHotRodStorage(KeycloakSession session, Class<M> modelType, AllAreasHotRodTransactionsWrapper txWrapper, MapStorageProviderFactory.Flag... flags) {
HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class);
HotRodEntityDescriptor<E, V> entityDescriptor = (HotRodEntityDescriptor<E, V>) getEntityDescriptor(modelType);
if (modelType == SingleUseObjectValueModel.class) {
return (HotRodMapStorage) new SingleUseObjectHotRodMapStorage(session, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, (HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate>) 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<SearchableModelField<? super UserSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, MapUserSessionEntity, UserSessionModel>> fieldPredicates = MapFieldPredicates.getPredicates((Class<UserSessionModel>) 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);

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectHotRodMapStorage
extends HotRodMapStorage<String, HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel> {
public class SingleUseObjectHotRodCrudOperations
extends HotRodCrudOperations<String, HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel> {
private final StringKeyConverter<String> keyConverter;
private final HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate> storedEntityDescriptor;
private final DeepCloner cloner;
public SingleUseObjectHotRodMapStorage(KeycloakSession session, RemoteCache<String, HotRodSingleUseObjectEntity> remoteCache, StringKeyConverter<String> keyConverter,
public SingleUseObjectHotRodCrudOperations(KeycloakSession session, RemoteCache<String, HotRodSingleUseObjectEntity> remoteCache, StringKeyConverter<String> keyConverter,
HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate> 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<HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel> createTransactionInternal(KeycloakSession session) {
Map<SearchableModelField<? super SingleUseObjectValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<String, HotRodSingleUseObjectEntityDelegate, SingleUseObjectValueModel>> fieldPredicates =
MapFieldPredicates.getPredicates((Class<SingleUseObjectValueModel>) storedEntityDescriptor.getModelTypeClass());
return new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates);
DeepCloner cloner, Long lockTimeout) {
super(session, remoteCache, keyConverter, storedEntityDescriptor, cloner, lockTimeout);
}
@Override

View file

@ -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<Class<?>, MapKeycloakTransaction<?, ?>> MapKeycloakTransactionsMap = new ConcurrentHashMap<>();
private final Map<Class<?>, ConcurrentHashMapStorage<?, ?, ?>> MapKeycloakStoresMap = new ConcurrentHashMap<>();
public MapKeycloakTransaction<?, ?> getOrCreateTxForModel(Class<?> modelType, Supplier<MapKeycloakTransaction<?,?>> supplier) {
MapKeycloakTransaction<?, ?> tx = MapKeycloakTransactionsMap.computeIfAbsent(modelType, t -> supplier.get());
if (!tx.isActive()) {
tx.begin();
public ConcurrentHashMapStorage<?, ?, ?> getOrCreateStoreForModel(Class<?> modelType, Supplier<ConcurrentHashMapStorage<?, ?, ?>> 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);
}
}

View file

@ -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<K, V extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<V, M> {
private final ConcurrentHashMapKeycloakTransaction<K, V, M> actualTx;
public NoActionHotRodTransactionWrapper(ConcurrentHashMapKeycloakTransaction<K, V, M> 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<V> read(QueryParameters<M> queryParameters) {
return actualTx.read(queryParameters);
}
@Override
public long getCount(QueryParameters<M> queryParameters) {
return actualTx.getCount(queryParameters);
}
@Override
public boolean delete(String key) {
return actualTx.delete(key);
}
@Override
public long delete(QueryParameters<M> queryParameters) {
return actualTx.delete(queryParameters);
}
@Override
public boolean exists(String key) {
return actualTx.exists(key);
}
@Override
public boolean exists(QueryParameters<M> 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();
}
}

View file

@ -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<K> extends ConcurrentHashMapKeycloakTransaction<K, MapUserSessionEntity, UserSessionModel> {
public class HotRodUserSessionMapStorage<K> extends ConcurrentHashMapStorage<K, MapUserSessionEntity, UserSessionModel> {
private final MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTransaction;
private final ConcurrentHashMapStorage<String, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionStore;
public HotRodUserSessionTransaction(ConcurrentHashMapCrudOperations<MapUserSessionEntity, UserSessionModel> map,
public HotRodUserSessionMapStorage(CrudOperations<MapUserSessionEntity, UserSessionModel> map,
StringKeyConverter<K> keyConverter,
DeepCloner cloner,
Map<SearchableModelField<? super UserSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapUserSessionEntity, UserSessionModel>> fieldPredicates,
MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTransaction
ConcurrentHashMapStorage<String, MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> 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<K> 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<K> 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<K> 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<K> 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<MapAuthenticatedClientSessionEntity> clientSessions = super.getAuthenticatedClientSessions();
if (clientSessions != null) {
clientSessionTransaction.delete(QueryParameters.withCriteria(
clientSessionStore.delete(QueryParameters.withCriteria(
DefaultModelCriteria.<AuthenticatedClientSessionModel>criteria()
.compare(HotRodAuthenticatedClientSessionEntity.ID, IN, clientSessions.stream()
.map(MapAuthenticatedClientSessionEntity::getId))
@ -157,7 +150,7 @@ public class HotRodUserSessionTransaction<K> extends ConcurrentHashMapKeycloakTr
MapUserSessionEntity uSession = read(key);
Set<MapAuthenticatedClientSessionEntity> clientSessions = uSession.getAuthenticatedClientSessions();
if (clientSessions != null) {
clientSessionTransaction.delete(QueryParameters.withCriteria(
clientSessionStore.delete(QueryParameters.withCriteria(
DefaultModelCriteria.<AuthenticatedClientSessionModel>criteria()
.compare(HotRodAuthenticatedClientSessionEntity.ID, IN, clientSessions.stream()
.map(MapAuthenticatedClientSessionEntity::getId))
@ -169,7 +162,7 @@ public class HotRodUserSessionTransaction<K> extends ConcurrentHashMapKeycloakTr
@Override
public long delete(QueryParameters<UserSessionModel> queryParameters) {
clientSessionTransaction.delete(QueryParameters.withCriteria(
clientSessionStore.delete(QueryParameters.withCriteria(
DefaultModelCriteria.<AuthenticatedClientSessionModel>criteria()
.compare(HotRodAuthenticatedClientSessionEntity.ID, IN, read(queryParameters)
.flatMap(userSession -> Optional.ofNullable(userSession.getAuthenticatedClientSessions()).orElse(Collections.emptySet()).stream().map(AbstractEntity::getId)))

View file

@ -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<RE extends JpaRootEntity, E extends AbstractEntity, M> implements MapKeycloakTransaction<E, M> {
public abstract class JpaMapStorage<RE extends JpaRootEntity, E extends AbstractEntity, M> implements MapStorage<E, M> {
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<RE> entityType;
private final Class<M> modelType;
private final boolean isExpirableEntity;
protected EntityManager em;
public JpaMapKeycloakTransaction(KeycloakSession session, Class<RE> entityType, Class<M> modelType, EntityManager em) {
public JpaMapStorage(KeycloakSession session, Class<RE> entityType, Class<M> modelType, EntityManager em) {
this.session = session;
this.em = em;
this.entityType = entityType;
@ -304,36 +304,6 @@ public abstract class JpaMapKeycloakTransaction<RE extends JpaRootEntity, E exte
return new MapModelCriteriaBuilder<>(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<RE> root) {
return cb.or(cb.greaterThan(root.get("expiration"), Time.currentTimeMillis()),
cb.isNull(root.get("expiration")));

View file

@ -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 <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, Flag... flags) {
public <V extends AbstractEntity, M> MapStorage<V, M> getMapStorage(Class<M> 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<V, M>() {
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
return factory.createTransaction(session, modelType, em);
}
};
return SessionAttributesUtils.createMapStorageIfAbsent(session, JpaMapStorageProvider.class, modelType, factoryId, () -> factory.createMapStorage(session, modelType, em));
}
}

View file

@ -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<Class<?>> 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<Class<?>, BiFunction<KeycloakSession, EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
private static final Map<Class<?>, BiFunction<KeycloakSession, EntityManager, MapStorage>> 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() {

View file

@ -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<JpaRootAuthenticationSessionEntity, MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> {
public class JpaRootAuthenticationSessionMapStorage extends JpaMapStorage<JpaRootAuthenticationSessionEntity, MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> {
public JpaRootAuthenticationSessionMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaRootAuthenticationSessionMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaRootAuthenticationSessionEntity.class, RootAuthenticationSessionModel.class, em);
}

View file

@ -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<JpaPermissionEntity, MapPermissionTicketEntity, PermissionTicket> {
public class JpaPermissionMapStorage extends JpaMapStorage<JpaPermissionEntity, MapPermissionTicketEntity, PermissionTicket> {
@SuppressWarnings("unchecked")
public JpaPermissionMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaPermissionMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaPermissionEntity.class, PermissionTicket.class, em);
}

View file

@ -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<JpaPolicyEntity, MapPolicyEntity, Policy> {
public class JpaPolicyMapStorage extends JpaMapStorage<JpaPolicyEntity, MapPolicyEntity, Policy> {
@SuppressWarnings("unchecked")
public JpaPolicyMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaPolicyMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaPolicyEntity.class, Policy.class, em);
}

View file

@ -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<JpaResourceEntity, MapResourceEntity, Resource> {
public class JpaResourceMapStorage extends JpaMapStorage<JpaResourceEntity, MapResourceEntity, Resource> {
@SuppressWarnings("unchecked")
public JpaResourceMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaResourceMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaResourceEntity.class, Resource.class, em);
}

View file

@ -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<JpaResourceServerEntity, MapResourceServerEntity, ResourceServer> {
public class JpaResourceServerMapStorage extends JpaMapStorage<JpaResourceServerEntity, MapResourceServerEntity, ResourceServer> {
@SuppressWarnings("unchecked")
public JpaResourceServerMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaResourceServerMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaResourceServerEntity.class, ResourceServer.class, em);
}

View file

@ -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<JpaScopeEntity, MapScopeEntity, Scope> {
public class JpaScopeMapStorage extends JpaMapStorage<JpaScopeEntity, MapScopeEntity, Scope> {
@SuppressWarnings("unchecked")
public JpaScopeMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaScopeMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaScopeEntity.class, Scope.class, em);
}

View file

@ -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<JpaClientEntity, MapClientEntity, ClientModel> {
public class JpaClientMapStorage extends JpaMapStorage<JpaClientEntity, MapClientEntity, ClientModel> {
@SuppressWarnings("unchecked")
public JpaClientMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaClientMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaClientEntity.class, ClientModel.class, em);
}

View file

@ -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<JpaClientScopeEntity, MapClientScopeEntity, ClientScopeModel> {
public class JpaClientScopeMapStorage extends JpaMapStorage<JpaClientScopeEntity, MapClientScopeEntity, ClientScopeModel> {
@SuppressWarnings("unchecked")
public JpaClientScopeMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaClientScopeMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaClientScopeEntity.class, ClientScopeModel.class, em);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAdminEventMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaAdminEventEntity, MapAdminEventEntity, AdminEvent> {
public class JpaAdminEventMapStorage extends JpaMapStorage<JpaAdminEventEntity, MapAdminEventEntity, AdminEvent> {
public JpaAdminEventMapKeycloakTransaction(KeycloakSession session, final EntityManager em) {
public JpaAdminEventMapStorage(KeycloakSession session, final EntityManager em) {
super(session, JpaAdminEventEntity.class, AdminEvent.class, em);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaAuthEventMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaAuthEventEntity, MapAuthEventEntity, Event> {
public class JpaAuthEventMapStorage extends JpaMapStorage<JpaAuthEventEntity, MapAuthEventEntity, Event> {
public JpaAuthEventMapKeycloakTransaction(KeycloakSession session, final EntityManager em) {
public JpaAuthEventMapStorage(KeycloakSession session, final EntityManager em) {
super(session, JpaAuthEventEntity.class, Event.class, em);
}

View file

@ -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<JpaGroupEntity, MapGroupEntity, GroupModel> {
public class JpaGroupMapStorage extends JpaMapStorage<JpaGroupEntity, MapGroupEntity, GroupModel> {
@SuppressWarnings("unchecked")
public JpaGroupMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaGroupMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaGroupEntity.class, GroupModel.class, em);
}

View file

@ -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 <a href="https://github.com/keycloak/keycloak/issues/11666">keycloak/keycloak#11666</a>.
* <p />
* 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;
}

View file

@ -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<JpaLockEntity, MapLockEntity, MapLockEntity> {
public class JpaLockMapStorage extends JpaMapStorage<JpaLockEntity, MapLockEntity, MapLockEntity> {
@SuppressWarnings("unchecked")
public JpaLockMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaLockMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaLockEntity.class, MapLockEntity.class, em);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserLoginFailureMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaUserLoginFailureEntity, MapUserLoginFailureEntity, UserLoginFailureModel> {
public class JpaUserLoginFailureMapStorage extends JpaMapStorage<JpaUserLoginFailureEntity, MapUserLoginFailureEntity, UserLoginFailureModel> {
@SuppressWarnings("unchecked")
public JpaUserLoginFailureMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
public JpaUserLoginFailureMapStorage(KeycloakSession session, EntityManager em) {
super(session, JpaUserLoginFailureEntity.class, UserLoginFailureModel.class, em);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaRealmMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaRealmEntity, MapRealmEntity, RealmModel> {
public class JpaRealmMapStorage extends JpaMapStorage<JpaRealmEntity, MapRealmEntity, RealmModel> {
public JpaRealmMapKeycloakTransaction(KeycloakSession session, final EntityManager em) {
public JpaRealmMapStorage(KeycloakSession session, final EntityManager em) {
super(session, JpaRealmEntity.class, RealmModel.class, em);
}

View file

@ -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<JpaRoleEntity, MapRoleEntity, RoleModel> {
public class JpaRoleMapStorage extends JpaMapStorage<JpaRoleEntity, MapRoleEntity, RoleModel> {
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);
}

View file

@ -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

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaSingleUseObjectMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaSingleUseObjectEntity, MapSingleUseObjectEntity, SingleUseObjectValueModel> {
public class JpaSingleUseObjectMapStorage extends JpaMapStorage<JpaSingleUseObjectEntity, MapSingleUseObjectEntity, SingleUseObjectValueModel> {
public JpaSingleUseObjectMapKeycloakTransaction(KeycloakSession session, final EntityManager em) {
public JpaSingleUseObjectMapStorage(KeycloakSession session, final EntityManager em) {
super(session, JpaSingleUseObjectEntity.class, SingleUseObjectValueModel.class, em);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaUserEntity, MapUserEntity, UserModel> {
public class JpaUserMapStorage extends JpaMapStorage<JpaUserEntity, MapUserEntity, UserModel> {
public JpaUserMapKeycloakTransaction(KeycloakSession session,final EntityManager em) {
public JpaUserMapStorage(KeycloakSession session, final EntityManager em) {
super(session, JpaUserEntity.class, UserModel.class, em);
}

View file

@ -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<JpaUserSessionEntity, MapUserSessionEntity, UserSessionModel> {
public class JpaUserSessionMapStorage extends JpaMapStorage<JpaUserSessionEntity, MapUserSessionEntity, UserSessionModel> {
public JpaUserSessionMapKeycloakTransaction(KeycloakSession session, final EntityManager em) {
public JpaUserSessionMapStorage(KeycloakSession session, final EntityManager em) {
super(session, JpaUserSessionEntity.class, UserSessionModel.class, em);
}

View file

@ -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<RE, E extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<E, M> {
public abstract class LdapMapStorage<RE, E extends AbstractEntity & UpdatableEntity, M> implements MapStorage<E, M>, KeycloakTransaction {
private boolean active;
private boolean rollback;
public LdapMapKeycloakTransaction() {
public LdapMapStorage() {
}
protected abstract static class MapTaskWithValue {

View file

@ -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 <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> 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<V, M>() {
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<V, M> 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 <V extends AbstractEntity, M> MapStorage<V, M> getMapStorage(Class<M> modelType, Flag... flags) {
return SessionAttributesUtils.createMapStorageIfAbsent(session, getClass(), modelType, factoryId, () -> {
LdapMapStorage store = (LdapMapStorage) factory.createMapStorage(session, modelType);
session.getTransactionManager().enlist(store);
return store;
});
}
}

View file

@ -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<Class<?>, LdapRoleMapKeycloakTransaction.LdapRoleMapKeycloakTransactionFunction<KeycloakSession, Config.Scope, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
private static final Map<Class<?>, LdapRoleMapStorage.LdapRoleMapKeycloakTransactionFunction<KeycloakSession, Config.Scope, MapStorage>> 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 <M, V extends AbstractEntity> MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session, Class<M> modelType) {
return MODEL_TO_TX.get(modelType).apply(session, config);
public <M, V extends AbstractEntity> MapStorage<V, M> createMapStorage(KeycloakSession session, Class<M> 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

View file

@ -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<LdapMapRoleEntityFieldDelegate, MapRoleEntity, RoleModel> implements Provider {
public class LdapRoleMapStorage extends LdapMapStorage<LdapMapRoleEntityFieldDelegate, MapRoleEntity, RoleModel> implements Provider {
private final StringKeyConverter<String> keyConverter = new StringKeyConverter.StringKey();
private final Set<String> deletedKeys = new HashSet<>();
@ -63,7 +62,7 @@ public class LdapRoleMapKeycloakTransaction extends LdapMapKeycloakTransaction<L
private final LdapMapConfig ldapMapConfig;
private final LdapMapIdentityStore identityStore;
public LdapRoleMapKeycloakTransaction(KeycloakSession session, Config.Scope config) {
public LdapRoleMapStorage(KeycloakSession session, Config.Scope config) {
this.roleMapperConfig = new LdapMapRoleMapperConfig(config);
this.ldapMapConfig = new LdapMapConfig(config);
this.identityStore = new LdapMapIdentityStore(session, ldapMapConfig);

View file

@ -40,13 +40,13 @@ import org.keycloak.models.map.role.MapRoleEntityFields;
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.role.config.LdapMapRoleMapperConfig;
import org.keycloak.models.map.storage.ldap.role.LdapRoleMapKeycloakTransaction;
import org.keycloak.models.map.storage.ldap.role.LdapRoleMapStorage;
public class LdapRoleEntity extends UpdatableEntity.Impl implements EntityFieldDelegate<MapRoleEntity> {
private final LdapMapObject ldapMapObject;
private final LdapMapRoleMapperConfig roleMapperConfig;
private final LdapRoleMapKeycloakTransaction transaction;
private final LdapRoleMapStorage store;
private final String clientId;
private static final EnumMap<MapRoleEntityFields, BiConsumer<LdapRoleEntity, Object>> 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<String> 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<String> 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<String> members = ldapMapObject.getAttributeAsSet(roleMapperConfig.getMembershipLdapAttribute());
if (members == null) {
members = new HashSet<>();

View file

@ -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<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> tx;
protected final MapStorage<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> store;
private int authSessionsLimit;
private final boolean txHasRealmId;
private final boolean storeHasRealmId;
public MapRootAuthenticationSessionProvider(KeycloakSession session,
MapStorage<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> 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<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> 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<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private Predicate<MapRootAuthenticationSessionEntity> 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<RootAuthenticationSessionModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
txInRealm(realm).delete(withCriteria(mcb));
storeWithRealm(realm).delete(withCriteria(mcb));
}
@Override

View file

@ -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

View file

@ -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<MapPermissionTicketEntity, PermissionTicket> permissionTicketStore,
MapStorage<MapPolicyEntity, Policy> policyStore, MapStorage<MapResourceServerEntity, ResourceServer> resourceServerStore,
MapStorage<MapResourceEntity, Resource> resourceStore, MapStorage<MapScopeEntity, Scope> 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<MapPermissionTicketEntity, PermissionTicket> permissionTicketStore,
MapStorage<MapPolicyEntity, Policy> policyStore,
MapStorage<MapResourceServerEntity, ResourceServer> resourceServerStore,
MapStorage<MapResourceEntity, Resource> resourceStore,
MapStorage<MapScopeEntity, Scope> 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

View file

@ -64,13 +64,13 @@ public class MapAuthorizationStoreFactory implements AmphibianProviderFactory<St
final MapStorageProvider mapStorageProvider = AbstractMapProviderFactory.getProviderFactoryOrComponentFactory(session, storageConfigScope).create(session);
AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);
MapStorage<MapPermissionTicketEntity, PermissionTicket> permissionTicketStore = mapStorageProvider.getStorage(PermissionTicket.class);
MapStorage<MapPolicyEntity, Policy> policyStore = mapStorageProvider.getStorage(Policy.class);
MapStorage<MapResourceServerEntity, ResourceServer> resourceServerStore = mapStorageProvider.getStorage(ResourceServer.class);
MapStorage<MapResourceEntity, Resource> resourceStore = mapStorageProvider.getStorage(Resource.class);
MapStorage<MapScopeEntity, Scope> scopeStore = mapStorageProvider.getStorage(Scope.class);
MapStorage<MapPermissionTicketEntity, PermissionTicket> permissionTicketStore = mapStorageProvider.getMapStorage(PermissionTicket.class);
MapStorage<MapPolicyEntity, Policy> policyStore = mapStorageProvider.getMapStorage(Policy.class);
MapStorage<MapResourceServerEntity, ResourceServer> resourceServerStore = mapStorageProvider.getMapStorage(ResourceServer.class);
MapStorage<MapResourceEntity, Resource> resourceStore = mapStorageProvider.getMapStorage(Resource.class);
MapStorage<MapScopeEntity, Scope> scopeStore = mapStorageProvider.getMapStorage(Scope.class);
authzStore = new MapAuthorizationStore(session,
authzStore = new MapAuthorizationStore(
permissionTicketStore,
policyStore,
resourceServerStore,

View file

@ -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<MapPermissionTicketEntity, PermissionTicket> tx;
private final boolean txHasRealmId;
final MapStorage<MapPermissionTicketEntity, PermissionTicket> store;
private final boolean storeHasRealmId;
public MapPermissionTicketStore(KeycloakSession session, MapStorage<MapPermissionTicketEntity, PermissionTicket> permissionTicketStore, AuthorizationProvider provider) {
public MapPermissionTicketStore(MapStorage<MapPermissionTicketEntity, PermissionTicket> 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<MapPermissionTicketEntity, PermissionTicket> entityToAdapterFunc(RealmModel realm, ResourceServer resourceServer) {
return origEntity -> new MapPermissionTicketAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory());
}
private MapKeycloakTransaction<MapPermissionTicketEntity, PermissionTicket> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapPermissionTicketEntity, PermissionTicket> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private DefaultModelCriteria<PermissionTicket> 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<PermissionTicket> 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)));
}
}

View file

@ -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<MapPolicyEntity, Policy> tx;
private final boolean txHasRealmId;
final MapStorage<MapPolicyEntity, Policy> store;
private final boolean storeHasRealmId;
public MapPolicyStore(KeycloakSession session, MapStorage<MapPolicyEntity, Policy> policyStore, AuthorizationProvider provider) {
public MapPolicyStore(MapStorage<MapPolicyEntity, Policy> 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<MapPolicyEntity, Policy> entityToAdapterFunc(RealmModel realm, ResourceServer resourceServer) {
return origEntity -> new MapPolicyAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory());
}
private MapKeycloakTransaction<MapPolicyEntity, Policy> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapPolicyEntity, Policy> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private DefaultModelCriteria<Policy> forRealmAndResourceServer(RealmModel realm, ResourceServer resourceServer) {
@ -94,7 +91,7 @@ public class MapPolicyStore implements PolicyStore {
DefaultModelCriteria<Policy> 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());
}
@ -218,7 +215,7 @@ public class MapPolicyStore implements PolicyStore {
public void findByResource(ResourceServer resourceServer, Resource resource, Consumer<Policy> 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<Policy> 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<Policy> 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)));
}
}

View file

@ -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<MapResourceServerEntity, ResourceServer> tx;
private final boolean txHasRealmId;
final MapStorage<MapResourceServerEntity, ResourceServer> store;
private final boolean storeHasRealmId;
public MapResourceServerStore(KeycloakSession session, MapStorage<MapResourceServerEntity, ResourceServer> resourceServerStore, AuthorizationProvider provider) {
this.tx = resourceServerStore.createTransaction(session);
public MapResourceServerStore(MapStorage<MapResourceServerEntity, ResourceServer> 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<MapResourceServerEntity, ResourceServer> entityToAdapterFunc(RealmModel realmModel) {
return origEntity -> new MapResourceServerAdapter(realmModel, origEntity, authorizationProvider.getStoreFactory());
}
private MapKeycloakTransaction<MapResourceServerEntity, ResourceServer> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapResourceServerEntity, ResourceServer> 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<ResourceServer> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
txInRealm(realm).delete(withCriteria(mcb));
storeWithRealm(realm).delete(withCriteria(mcb));
}
}

View file

@ -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<MapResourceEntity, Resource> tx;
private final KeycloakSession session;
private final boolean txHasRealmId;
final MapStorage<MapResourceEntity, Resource> store;
private final boolean storeHasRealmId;
public MapResourceStore(KeycloakSession session, MapStorage<MapResourceEntity, Resource> 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<MapResourceEntity, Resource> resourceStore, AuthorizationProvider provider) {
this.authorizationProvider = provider;
this.store = resourceStore;
this.storeHasRealmId = store instanceof HasRealmId;
}
private Function<MapResourceEntity, Resource> entityToAdapterFunc(RealmModel realm, final ResourceServer resourceServer) {
return origEntity -> new MapResourceAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory());
}
private MapKeycloakTransaction<MapResourceEntity, Resource> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapResourceEntity, Resource> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private DefaultModelCriteria<Resource> 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<Resource> 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<Resource> 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<Resource> 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)));
}
}

View file

@ -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<MapScopeEntity, Scope> tx;
private final KeycloakSession session;
private final boolean txHasRealmId;
final MapStorage<MapScopeEntity, Scope> store;
private final boolean storeHasRealmId;
public MapScopeStore(KeycloakSession session, MapStorage<MapScopeEntity, Scope> scopeStore, AuthorizationProvider provider) {
public MapScopeStore(MapStorage<MapScopeEntity, Scope> 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<MapScopeEntity, Scope> entityToAdapterFunc(RealmModel realm, ResourceServer resourceServer) {
return origEntity -> new MapScopeAdapter(realm, resourceServer, origEntity, authorizationProvider.getStoreFactory());
}
private MapKeycloakTransaction<MapScopeEntity, Scope> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapScopeEntity, Scope> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private DefaultModelCriteria<Scope> forRealmAndResourceServer(RealmModel realm, ResourceServer resourceServer) {
@ -91,7 +86,7 @@ public class MapScopeStore implements ScopeStore {
DefaultModelCriteria<Scope> 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<Scope> 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<Scope> 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)));
}
}

View file

@ -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<MapClientEntity, ClientModel> tx;
final MapStorage<MapClientEntity, ClientModel> store;
private final ConcurrentMap<String, ConcurrentMap<String, Long>> clientRegisteredNodesStore;
private final boolean txHasRealmId;
private final boolean storeHasRealmId;
public MapClientProvider(KeycloakSession session, MapStorage<MapClientEntity, ClientModel> clientStore, ConcurrentMap<String, ConcurrentMap<String, Long>> 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<MapClientEntity, ClientModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapClientEntity, ClientModel> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private Predicate<MapClientEntity> entityRealmFilter(RealmModel realm) {
@ -141,7 +139,7 @@ public class MapClientProvider implements ClientProvider {
DefaultModelCriteria<ClientModel> 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<ClientModel> 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<ClientModel> 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<ClientModel> 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<ClientScopeModel> 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<String, ClientScopeModel> 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<MapClientEntity> st = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapClientEntity> 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<MapClientEntity> toRemove = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapClientEntity> toRemove = storeWithRealm(realm).read(withCriteria(mcb))) {
toRemove
.forEach(clientEntity -> clientEntity.removeScopeMapping(role.getId()));
}
@ -373,7 +371,7 @@ public class MapClientProvider implements ClientProvider {
DefaultModelCriteria<ClientModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
txInRealm(realm).delete(withCriteria(mcb));
storeWithRealm(realm).delete(withCriteria(mcb));
}
@Override

View file

@ -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<MapClie
@Override
public MapClientProvider createNew(KeycloakSession session) {
return new MapClientProvider(session, getStorage(session), REGISTERED_NODES_STORE);
return new MapClientProvider(session, getMapStorage(session), REGISTERED_NODES_STORE);
}
@Override

View file

@ -31,7 +31,6 @@ import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
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;
@ -48,14 +47,13 @@ public class MapClientScopeProvider implements ClientScopeProvider {
private static final Logger LOG = Logger.getLogger(MapClientScopeProvider.class);
private final KeycloakSession session;
private final MapKeycloakTransaction<MapClientScopeEntity, ClientScopeModel> tx;
private final boolean txHasRealmId;
private final MapStorage<MapClientScopeEntity, ClientScopeModel> store;
private final boolean storeHasRealmId;
public MapClientScopeProvider(KeycloakSession session, MapStorage<MapClientScopeEntity, ClientScopeModel> 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<MapClientScopeEntity, ClientScopeModel> entityToAdapterFunc(RealmModel realm) {
@ -64,11 +62,11 @@ public class MapClientScopeProvider implements ClientScopeProvider {
return origEntity -> new MapClientScopeAdapter(session, realm, origEntity);
}
private MapKeycloakTransaction<MapClientScopeEntity, ClientScopeModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapClientScopeEntity, ClientScopeModel> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private Predicate<MapClientScopeEntity> entityRealmFilter(RealmModel realm) {
@ -84,7 +82,7 @@ public class MapClientScopeProvider implements ClientScopeProvider {
DefaultModelCriteria<ClientScopeModel> 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<ClientScopeModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
txInRealm(realm).delete(withCriteria(mcb));
storeWithRealm(realm).delete(withCriteria(mcb));
}
@Override

View file

@ -35,7 +35,7 @@ public class MapClientScopeProviderFactory extends AbstractMapProviderFactory<Ma
@Override
public MapClientScopeProvider createNew(KeycloakSession session) {
return new MapClientScopeProvider(session, getStorage(session));
return new MapClientScopeProvider(session, getMapStorage(session));
}
@Override

View file

@ -32,6 +32,8 @@ import org.keycloak.provider.ProviderFactory;
import java.util.Objects;
import org.jboss.logging.Logger;
import static org.keycloak.models.map.common.SessionAttributesUtils.grabNewFactoryIdentifier;
/**
*
* @author hmlnarik
@ -45,7 +47,7 @@ public abstract class AbstractMapProviderFactory<T extends Provider, V extends A
protected final Logger LOG = Logger.getLogger(getClass());
public static final AtomicInteger uniqueCounter = new AtomicInteger();
private final String uniqueKey = getClass().getName() + uniqueCounter.incrementAndGet();
private final int factoryId = grabNewFactoryIdentifier();
protected final Class<M> modelType;
private final Class<T> providerType;
@ -92,13 +94,7 @@ public abstract class AbstractMapProviderFactory<T extends Provider, V extends A
*/
@Override
public T create(KeycloakSession session) {
T provider = session.getAttribute(uniqueKey, providerType);
if (provider != null) {
return provider;
}
provider = createNew(session);
session.setAttribute(uniqueKey, provider);
return provider;
return SessionAttributesUtils.createProviderIfAbsent(session, factoryId, providerType, this::createNew);
}
@Override
@ -106,11 +102,11 @@ public abstract class AbstractMapProviderFactory<T extends Provider, V extends A
return PROVIDER_ID;
}
public MapStorage<V, M> getStorage(KeycloakSession session) {
public MapStorage<V, M> getMapStorage(KeycloakSession session) {
ProviderFactory<MapStorageProvider> storageProviderFactory = getProviderFactoryOrComponentFactory(session, storageConfigScope);
final MapStorageProvider factory = storageProviderFactory.create(session);
session.enlistForClose(factory);
return factory.getStorage(modelType);
return factory.getMapStorage(modelType);
}
public static ProviderFactory<MapStorageProvider> getProviderFactoryOrComponentFactory(KeycloakSession session, Scope storageConfigScope) {

View file

@ -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.
* <p />
* 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 <T> type of the provider
*/
public static <T extends Provider> T createProviderIfAbsent(KeycloakSession session,
int factoryIdentifier,
Class<T> providerClass,
Function<KeycloakSession, T> 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.
* <p />
* 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 <V> entity type
* @param <M> model type
* @param <T> store type
*/
public static <V extends AbstractEntity & UpdatableEntity, M, T extends MapStorage<V, M>> T createMapStorageIfAbsent(
KeycloakSession session,
Class<? extends MapStorageProvider> providerType,
Class<M> modelType,
int factoryId,
Supplier<T> 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;
}
}

View file

@ -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 <P extends Provider, E extends AbstractEntity, M> MapKeycloakTransaction<E, M> getTransaction(KeycloakSession session, Class<P> provider) {
private static <P extends Provider, E extends AbstractEntity, M> MapStorage<E, M> getMapStorage(KeycloakSession session, Class<P> provider) {
ProviderFactory<P> factoryChm = session.getKeycloakSessionFactory().getProviderFactory(provider);
return ((AbstractMapProviderFactory<P, E, M>) factoryChm).getStorage(session).createTransaction(session);
return ((AbstractMapProviderFactory<P, E, M>) factoryChm).getMapStorage(session);
}
private <P extends Provider, M> void copyEntities(String realmId, KeycloakSession sessionChm, Class<P> provider, Class<M> model, SearchableModelField<M> field) {
MapKeycloakTransaction<AbstractEntity, M> txChm = getTransaction(sessionChm, provider);
MapKeycloakTransaction<AbstractEntity, M> txOrig = getTransaction(session, provider);
MapStorage<AbstractEntity, M> storeChm = getMapStorage(sessionChm, provider);
MapStorage<AbstractEntity, M> storeOrig = getMapStorage(session, provider);
DefaultModelCriteria<M> 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) {

View file

@ -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<MapAuthEventEntity, Event> authEventsTX;
private final MapKeycloakTransaction<MapAdminEventEntity, AdminEvent> adminEventsTX;
private final MapStorage<MapAuthEventEntity, Event> authEventsTX;
private final MapStorage<MapAdminEventEntity, AdminEvent> adminEventsTX;
private final boolean adminTxHasRealmId;
private final boolean authTxHasRealmId;
public MapEventStoreProvider(KeycloakSession session, MapStorage<MapAuthEventEntity, Event> loginEventsStore, MapStorage<MapAdminEventEntity, AdminEvent> 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<MapAdminEventEntity, AdminEvent> adminTxInRealm(String realmId) {
private MapStorage<MapAdminEventEntity, AdminEvent> adminTxInRealm(String realmId) {
if (adminTxHasRealmId) {
((HasRealmId) adminEventsTX).setRealmId(realmId);
}
return adminEventsTX;
}
private MapKeycloakTransaction<MapAdminEventEntity, AdminEvent> adminTxInRealm(RealmModel realm) {
private MapStorage<MapAdminEventEntity, AdminEvent> adminTxInRealm(RealmModel realm) {
return adminTxInRealm(realm == null ? null : realm.getId());
}
private MapKeycloakTransaction<MapAuthEventEntity, Event> authTxInRealm(String realmId) {
private MapStorage<MapAuthEventEntity, Event> authTxInRealm(String realmId) {
if (authTxHasRealmId) {
((HasRealmId) authEventsTX).setRealmId(realmId);
}
return authEventsTX;
}
private MapKeycloakTransaction<MapAuthEventEntity, Event> authTxInRealm(RealmModel realm) {
private MapStorage<MapAuthEventEntity, Event> authTxInRealm(RealmModel realm) {
return authTxInRealm(realm == null ? null : realm.getId());
}

View file

@ -60,10 +60,10 @@ public class MapEventStoreProviderFactory implements AmphibianProviderFactory<Ev
if (provider != null) return provider;
final MapStorageProvider factoryAe = AbstractMapProviderFactory.getProviderFactoryOrComponentFactory(session, storageConfigScopeAdminEvents).create(session);
MapStorage<MapAdminEventEntity, AdminEvent> adminEventsStore = factoryAe.getStorage(AdminEvent.class);
MapStorage<MapAdminEventEntity, AdminEvent> adminEventsStore = factoryAe.getMapStorage(AdminEvent.class);
final MapStorageProvider factoryLe = AbstractMapProviderFactory.getProviderFactoryOrComponentFactory(session, storageConfigScopeLoginEvents).create(session);
MapStorage<MapAuthEventEntity, Event> loginEventsStore = factoryLe.getStorage(Event.class);
MapStorage<MapAuthEventEntity, Event> loginEventsStore = factoryLe.getMapStorage(Event.class);
provider = new MapEventStoreProvider(session, loginEventsStore, adminEventsStore);
session.setAttribute(uniqueKey, provider);

View file

@ -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<MapGroupEntity, GroupModel> tx;
private final boolean txHasRealmId;
final MapStorage<MapGroupEntity, GroupModel> store;
private final boolean storeHasRealmId;
public MapGroupProvider(KeycloakSession session, MapStorage<MapGroupEntity, GroupModel> 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<MapGroupEntity, GroupModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapGroupEntity, GroupModel> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private Function<MapGroupEntity, GroupModel> 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<MapGroupEntity> possibleSiblings = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapGroupEntity> 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<MapGroupEntity> possibleSiblings = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapGroupEntity> 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<GroupModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId())
.compare(SearchableFields.ASSIGNED_ROLE, Operator.EQ, role.getId());
try (Stream<MapGroupEntity> toRemove = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapGroupEntity> 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<GroupModel> 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));
}
}

View file

@ -42,7 +42,7 @@ public class MapGroupProviderFactory extends AbstractMapProviderFactory<MapGroup
@Override
public MapGroupProvider createNew(KeycloakSession session) {
return new MapGroupProvider(session, getStorage(session));
return new MapGroupProvider(session, getMapStorage(session));
}
@Override

View file

@ -24,7 +24,6 @@ import org.keycloak.models.KeycloakSessionTaskWithResult;
import org.keycloak.models.locking.GlobalLockProvider;
import org.keycloak.models.locking.LockAcquiringTimeoutException;
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;
import org.keycloak.models.map.storage.QueryParameters;
@ -53,7 +52,7 @@ public class MapGlobalLockProvider implements GlobalLockProvider {
private final KeycloakSession session;
private final long defaultTimeoutMilliseconds;
private MapKeycloakTransaction<MapLockEntity, MapLockEntity> tx;
private MapStorage<MapLockEntity, MapLockEntity> 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<MapLockEntity> mcb = criteria();
mcb = mcb.compare(MapLockEntity.SearchableFields.NAME, ModelCriteriaBuilder.Operator.EQ, lockName);
Optional<MapLockEntity> entry = tx.read(QueryParameters.withCriteria(mcb)).findFirst();
Optional<MapLockEntity> 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<MapLockEntity> mcb = criteria();
tx.delete(QueryParameters.withCriteria(mcb));
store.delete(QueryParameters.withCriteria(mcb));
}
private static String getKeycloakInstanceIdentifier() {

View file

@ -47,7 +47,7 @@ public class MapGlobalLockProviderFactory extends AbstractMapProviderFactory<Glo
@Override
public MapGlobalLockProvider createNew(KeycloakSession session) {
return new MapGlobalLockProvider(session, defaultTimeoutMilliseconds, () -> getStorage(session));
return new MapGlobalLockProvider(session, defaultTimeoutMilliseconds, () -> getMapStorage(session));
}
@Override

View file

@ -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<MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureTx;
protected final MapStorage<MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureTx;
public MapUserLoginFailureProvider(KeycloakSession session, MapStorage<MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureStore) {
this.session = session;
userLoginFailureTx = userLoginFailureStore.createTransaction(session);
session.getTransactionManager().enlistAfterCompletion(userLoginFailureTx);
this.userLoginFailureTx = userLoginFailureStore;
}
private Function<MapUserLoginFailureEntity, UserLoginFailureModel> userLoginFailureEntityToAdapterFunc(RealmModel realm) {

View file

@ -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

View file

@ -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<MapRealmEntity, RealmModel> tx;
final MapStorage<MapRealmEntity, RealmModel> store;
public MapRealmProvider(KeycloakSession session, MapStorage<MapRealmEntity, RealmModel> 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<RealmModel> 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<RealmModel> getRealmsStream(DefaultModelCriteria<RealmModel> 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<RealmModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.CLIENT_INITIAL_ACCESS, Operator.EXISTS);
tx.read(withCriteria(mcb))
store.read(withCriteria(mcb))
.forEach(MapRealmEntity::removeExpiredClientInitialAccesses);
}

View file

@ -32,7 +32,7 @@ public class MapRealmProviderFactory extends AbstractMapProviderFactory<MapRealm
@Override
public MapRealmProvider createNew(KeycloakSession session) {
return new MapRealmProvider(session, getStorage(session));
return new MapRealmProvider(session, getMapStorage(session));
}
@Override

View file

@ -26,7 +26,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.RoleModel.SearchableFields;
import org.keycloak.models.RoleProvider;
@ -46,14 +45,13 @@ public class MapRoleProvider implements RoleProvider {
private static final Logger LOG = Logger.getLogger(MapRoleProvider.class);
private final KeycloakSession session;
final MapKeycloakTransaction<MapRoleEntity, RoleModel> tx;
private final boolean txHasRealmId;
final MapStorage<MapRoleEntity, RoleModel> store;
private final boolean storeHasRealmId;
public MapRoleProvider(KeycloakSession session, MapStorage<MapRoleEntity, RoleModel> 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<MapRoleEntity, RoleModel> entityToAdapterFunc(RealmModel realm) {
@ -61,11 +59,11 @@ public class MapRoleProvider implements RoleProvider {
return origEntity -> new MapRoleAdapter(session, realm, origEntity);
}
private MapKeycloakTransaction<MapRoleEntity, RoleModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapRoleEntity, RoleModel> 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<RoleModel> 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<RoleModel> 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

View file

@ -38,7 +38,7 @@ public class MapRoleProviderFactory extends AbstractMapProviderFactory<MapRolePr
@Override
public MapRoleProvider createNew(KeycloakSession session) {
return new MapRoleProvider(session, getStorage(session));
return new MapRoleProvider(session, getMapStorage(session));
}
@Override

View file

@ -20,12 +20,10 @@ package org.keycloak.models.map.singleUseObject;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.SingleUseObjectValueModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.map.common.DeepCloner;
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;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
@ -44,14 +42,10 @@ import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.crit
public class MapSingleUseObjectProvider implements SingleUseObjectProvider {
private static final Logger LOG = Logger.getLogger(MapSingleUseObjectProvider.class);
private final KeycloakSession session;
protected final MapKeycloakTransaction<MapSingleUseObjectEntity, SingleUseObjectValueModel> singleUseObjectTx;
protected final MapStorage<MapSingleUseObjectEntity, SingleUseObjectValueModel> singleUseObjectTx;
public MapSingleUseObjectProvider(KeycloakSession session, MapStorage<MapSingleUseObjectEntity, SingleUseObjectValueModel> storage) {
this.session = session;
singleUseObjectTx = storage.createTransaction(session);
session.getTransactionManager().enlistAfterCompletion(singleUseObjectTx);
public MapSingleUseObjectProvider(MapStorage<MapSingleUseObjectEntity, SingleUseObjectValueModel> storage) {
this.singleUseObjectTx = storage;
}
@Override

View file

@ -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

View file

@ -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 <V> Type of the value stored in the storage
* @param <M> Type of the model object
*/
public interface CrudOperations<V extends AbstractEntity & UpdatableEntity, M> {
/**
* Creates an object in the storage.
* <br />
* 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.
* <br />
* 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<V>} 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<M> 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.
* <br />
* 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<V> read(QueryParameters<M> 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<M> 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<M> 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();
}
}

View file

@ -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<V extends AbstractEntity, M> extends KeycloakTransaction {
/**
* Instructs this transaction to add a new value into the underlying store on commit.
* <p>
* 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.
* <p>
* 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<V> read(QueryParameters<M> 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<M> 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<M> 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<M> queryParameters) {
return getCount(queryParameters) > 0;
}
}

View file

@ -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<V extends AbstractEntity, M> extends MapKeycloakTransaction<V, M> {
/**
* 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<V> authenticate(RealmModel realm, CredentialInput input);
}

View file

@ -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 <V> 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 <M> 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<V extends AbstractEntity, M> {
/**
* 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.
* Instructs this storage to add a new value into the underlying store on commit in the context of the current transaction.
* <p>
* Updates to the returned instances of {@code V} would be visible in the current transaction
* and will propagate into the underlying store upon commit.
*
* @return See description. Never returns {@code null}
* 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}.
*/
MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session);
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.
* <p>
* 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<V> read(QueryParameters<M> 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<M> 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<M> 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<M> queryParameters) {
return getCount(queryParameters) > 0;
}
}

View file

@ -28,6 +28,7 @@ public interface MapStorageProvider extends Provider {
/**
* Returns a key-value storage implementation for the given types.
*
* @param <V> type of the value
* @param <M> type of the corresponding model (e.g. {@code UserModel})
* @param modelType Model type
@ -35,5 +36,5 @@ public interface MapStorageProvider extends Provider {
* @return
* @throws IllegalArgumentException If some of the types is not supported by the underlying implementation.
*/
<V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, Flag... flags);
<V extends AbstractEntity, M> MapStorage<V, M> getMapStorage(Class<M> modelType, Flag... flags);
}

View file

@ -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<V extends AbstractEntity & UpdatableEntity, M> extends MapStorage<V, M> {
public interface MapStorageWithAuth<V extends AbstractEntity, M> extends MapStorage<V, M> {
/**
* 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 <code>true</code> 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<V, M> createTransaction(KeycloakSession session);
MapCredentialValidationOutput<V> authenticate(RealmModel realm, CredentialInput input);
}

View file

@ -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<V extends AbstractEntity & UpdatableEntity, M> {
/**
* 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.
* <br>
* 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<V>} 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);
/**
* Updates the object with the key of the {@code value}'s ID in the storage if it already exists.
* 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)}).
*
* @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()
* @author hmlnarik
*/
V update(V value);
public class ConcurrentHashMapCrudOperations<K, V extends AbstractEntity & UpdatableEntity, M> implements CrudOperations<V, M> {
/**
* 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 ConcurrentMap<K, V> store = new ConcurrentHashMap<>();
/**
* 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<M> queryParameters);
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
protected final StringKeyConverter<K> keyConverter;
protected final DeepCloner cloner;
private final boolean isExpirableEntity;
/**
* 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<V> read(QueryParameters<M> 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<M> 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<M> modelClass, StringKeyConverter<K> 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<M> 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<M> queryParameters) {
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
if (criteria == null) {
long res = store.size();
store.clear();
return res;
}
@SuppressWarnings("unchecked")
MapModelCriteriaBuilder<K,V,M> mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super K> keyFilter = mcb.getKeyFilter();
Predicate<? super V> entityFilter = mcb.getEntityFilter();
Stream<Entry<K, V>> storeStream = store.entrySet().stream();
final AtomicLong res = new AtomicLong(0);
if (!queryParameters.getOrderBy().isEmpty()) {
Comparator<V> 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<K, V, M> createCriteriaBuilder() {
return new MapModelCriteriaBuilder<>(keyConverter, fieldPredicates);
}
public StringKeyConverter<K> getKeyConverter() {
return keyConverter;
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
if (criteria == null) {
return Stream.empty();
}
MapModelCriteriaBuilder<K,V,M> mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder());
Stream<Entry<K, V>> stream = store.entrySet().stream();
Predicate<? super K> keyFilter = mcb.getKeyFilter();
Predicate<? super V> entityFilter;
if (isExpirableEntity) {
entityFilter = mcb.getEntityFilter().and(ExpirationUtils::isNotExpired);
} else {
entityFilter = mcb.getEntityFilter();
}
Stream<V> 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<M> queryParameters) {
return read(queryParameters).count();
}
}

View file

@ -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<K, V extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<V, M>, HasRealmId {
private final static Logger log = Logger.getLogger(ConcurrentHashMapKeycloakTransaction.class);
protected boolean active;
protected boolean rollback;
protected final Map<String, MapTaskWithValue> tasks = new LinkedHashMap<>();
protected final ConcurrentHashMapCrudOperations<V, M> map;
protected final StringKeyConverter<K> keyConverter;
protected final DeepCloner cloner;
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
protected final EntityField<V> realmIdEntityField;
private String realmId;
private final boolean mapHasRealmId;
enum MapOperation {
CREATE, UPDATE, DELETE,
}
public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations<V, M> map, StringKeyConverter<K> keyConverter, DeepCloner cloner, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates) {
this(map, keyConverter, cloner, fieldPredicates, null);
}
public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations<V, M> map, StringKeyConverter<K> keyConverter, DeepCloner cloner, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, EntityField<V> 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<String> 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<K, V, M> 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.
* <p>
* 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<String, V> 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<V> read(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder();
MapModelCriteriaBuilder<K,V,M> mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super V> filterOutAllBulkDeletedObjects = tasks.values().stream()
.filter(BulkDeleteOperation.class::isInstance)
.map(BulkDeleteOperation.class::cast)
.map(BulkDeleteOperation::getFilterForNonDeletedObjects)
.reduce(Predicate::and)
.orElse(v -> true);
Stream<V> 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<V> 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<M> 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<V> 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<M> 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<V> filterForNonDeletedObjects = bdo.getFilterForNonDeletedObjects();
long res = 0;
for (Iterator<Entry<String, MapTaskWithValue>> it = tasks.entrySet().iterator(); it.hasNext();) {
Entry<String, MapTaskWithValue> 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<V> createdValuesStream(Predicate<? super K> keyFilter, Predicate<? super V> 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<M> queryParameters;
public BulkDeleteOperation(QueryParameters<M> queryParameters) {
super(null);
this.queryParameters = queryParameters;
}
@Override
@SuppressWarnings("unchecked")
public void execute() {
map.delete(queryParameters);
}
public Predicate<V> getFilterForNonDeletedObjects() {
DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder();
MapModelCriteriaBuilder<K,V,M> mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super V> entityFilter = mmcb.getEntityFilter();
Predicate<? super K> 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<V> postProcess(Stream<V> stream) {
if (this.realmId == null) {
return stream;
}
String localRealmId = this.realmId;
return stream.map((V value) -> ModelEntityUtil.supplyReadOnlyFieldValueIfUnset(value, realmIdEntityField, localRealmId));
}
}

View file

@ -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<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M>, 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<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
protected final ConcurrentMap<K, V> store = new ConcurrentHashMap<>();
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
protected boolean active;
protected boolean rollback;
protected final Map<String, MapTaskWithValue> tasks = new LinkedHashMap<>();
protected final CrudOperations<V, M> map;
protected final StringKeyConverter<K> keyConverter;
protected final DeepCloner cloner;
private final boolean isExpirableEntity;
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
protected final EntityField<V> realmIdEntityField;
private String realmId;
private final boolean mapHasRealmId;
@SuppressWarnings("unchecked")
public ConcurrentHashMapStorage(Class<M> modelClass, StringKeyConverter<K> keyConverter, DeepCloner cloner) {
this.fieldPredicates = MapFieldPredicates.getPredicates(modelClass);
enum MapOperation {
CREATE, UPDATE, DELETE,
}
public ConcurrentHashMapStorage(CrudOperations<V, M> map, StringKeyConverter<K> keyConverter, DeepCloner cloner, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates) {
this(map, keyConverter, cloner, fieldPredicates, null);
}
public ConcurrentHashMapStorage(CrudOperations<V, M> map, StringKeyConverter<K> keyConverter, DeepCloner cloner, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates, EntityField<V> 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);
public void commit() {
if (rollback) {
throw new RuntimeException("Rollback only!");
}
V v = store.get(k);
if (v == null) return null;
return isExpirableEntity && isExpired((ExpirableEntity) v, true) ? null : v;
final Consumer<String> 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 V update(V value) {
K key = getKeyConverter().fromStringSafe(value.getId());
return store.replace(key, value);
public void rollback() {
tasks.clear();
}
@Override
public boolean delete(String key) {
K k = getKeyConverter().fromStringSafe(key);
return store.remove(k) != null;
public void setRollbackOnly() {
rollback = true;
}
@Override
public long delete(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
if (criteria == null) {
long res = store.size();
store.clear();
return res;
}
@SuppressWarnings("unchecked")
MapModelCriteriaBuilder<K,V,M> mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super K> keyFilter = mcb.getKeyFilter();
Predicate<? super V> entityFilter = mcb.getEntityFilter();
Stream<Entry<K, V>> storeStream = store.entrySet().stream();
final AtomicLong res = new AtomicLong(0);
if (!queryParameters.getOrderBy().isEmpty()) {
Comparator<V> 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 boolean getRollbackOnly() {
return rollback;
}
@Override
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<V, M> 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 boolean isActive() {
return active;
}
public MapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() {
private MapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() {
return new MapModelCriteriaBuilder<>(keyConverter, fieldPredicates);
}
public StringKeyConverter<K> 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.
* <p>
* 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<String, V> 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<V> read(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder();
MapModelCriteriaBuilder<K,V,M> mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
if (criteria == null) {
return Stream.empty();
Predicate<? super V> filterOutAllBulkDeletedObjects = tasks.values().stream()
.filter(BulkDeleteOperation.class::isInstance)
.map(BulkDeleteOperation.class::cast)
.map(BulkDeleteOperation::getFilterForNonDeletedObjects)
.reduce(Predicate::and)
.orElse(v -> true);
Stream<V> 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<K,V,M> mcb = criteria.flashToModelCriteriaBuilder(createCriteriaBuilder());
Stream<Entry<K, V>> stream = store.entrySet().stream();
Predicate<? super K> keyFilter = mcb.getKeyFilter();
Predicate<? super V> entityFilter;
if (isExpirableEntity) {
entityFilter = mcb.getEntityFilter().and(ExpirationUtils::isNotExpired);
} else {
entityFilter = mcb.getEntityFilter();
}
Stream<V> 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<V> 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<K, V extends AbstractEntity & UpdatableEnt
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<V> 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<M> 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<V> filterForNonDeletedObjects = bdo.getFilterForNonDeletedObjects();
long res = 0;
for (Iterator<Entry<String, MapTaskWithValue>> it = tasks.entrySet().iterator(); it.hasNext();) {
Entry<String, MapTaskWithValue> 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<V> createdValuesStream(Predicate<? super K> keyFilter, Predicate<? super V> 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<M> queryParameters;
public BulkDeleteOperation(QueryParameters<M> queryParameters) {
super(null);
this.queryParameters = queryParameters;
}
@Override
@SuppressWarnings("unchecked")
public void execute() {
map.delete(queryParameters);
}
public Predicate<V> getFilterForNonDeletedObjects() {
DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder();
MapModelCriteriaBuilder<K,V,M> mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super V> entityFilter = mmcb.getEntityFilter();
Predicate<? super K> 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<V> postProcess(Stream<V> stream) {
if (this.realmId == null) {
return stream;
}
String localRealmId = this.realmId;
return stream.map((V value) -> ModelEntityUtil.supplyReadOnlyFieldValueIfUnset(value, realmIdEntityField, localRealmId));
}
}

View file

@ -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 <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, Flag... flags) {
ConcurrentHashMapStorage storage = factory.getStorage(modelType, flags);
return (MapStorage<V, M>) storage;
public <V extends AbstractEntity, M> MapStorage<V, M> getMapStorage(Class<M> 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 <V extends AbstractEntity & UpdatableEntity, M> ConcurrentHashMapStorage getMapStorage(Class<?> modelType, CrudOperations<V, M> 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));
}
}

View file

@ -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<String, ConcurrentHashMapStorage<?,?,?>> storages = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, CrudOperations<?,?>> storages = new ConcurrentHashMap<>();
private final Map<String, StringKeyConverter> 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 <K, V extends AbstractEntity & UpdatableEntity, M> ConcurrentHashMapStorage<K, V, M> loadMap(String mapName,
Class<M> modelType, EnumSet<Flag> flags) {
private <V extends AbstractEntity & UpdatableEntity, M> CrudOperations<V, M> loadMap(String mapName,
Class<M> modelType,
EnumSet<Flag> flags) {
final StringKeyConverter kc = keyConverters.getOrDefault(mapName, defaultKeyConverter);
Class<?> valueType = ModelEntityUtil.getEntityType(modelType);
LOG.debugf("Initializing new map storage: %s", mapName);
ConcurrentHashMapStorage<K, V, M> store;
CrudOperations<V, M> 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 <K, V extends AbstractEntity & UpdatableEntity, M> ConcurrentHashMapStorage<K, V, M> getStorage(
public <K, V extends AbstractEntity & UpdatableEntity, M> CrudOperations<V, M> getStorage(
Class<M> modelType, Flag... flags) {
EnumSet<Flag> 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<K, V, M>) storages.computeIfAbsent(name, n -> loadMap(name, modelType, f));
return (CrudOperations<V, M>) 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) {

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectConcurrentHashMapStorage<K, V extends AbstractEntity, M> extends ConcurrentHashMapStorage<K, MapSingleUseObjectEntity, SingleUseObjectValueModel> {
public class SingleUseObjectConcurrentHashMapCrudOperations<K, V extends AbstractEntity, M> extends ConcurrentHashMapCrudOperations<K, MapSingleUseObjectEntity, SingleUseObjectValueModel> {
public SingleUseObjectConcurrentHashMapStorage(StringKeyConverter<K> keyConverter, DeepCloner cloner) {
public SingleUseObjectConcurrentHashMapCrudOperations(StringKeyConverter<K> keyConverter, DeepCloner cloner) {
super(SingleUseObjectValueModel.class, keyConverter, cloner);
}
@Override
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<MapSingleUseObjectEntity, SingleUseObjectValueModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<MapSingleUseObjectEntity, SingleUseObjectValueModel> 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) {

View file

@ -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,9 +29,9 @@ import java.util.Map;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectKeycloakTransaction<K> extends ConcurrentHashMapKeycloakTransaction<K, MapSingleUseObjectEntity, SingleUseObjectValueModel> {
public class SingleUseObjectMapStorage<K> extends ConcurrentHashMapStorage<K, MapSingleUseObjectEntity, SingleUseObjectValueModel> {
public SingleUseObjectKeycloakTransaction(ConcurrentHashMapCrudOperations<MapSingleUseObjectEntity, SingleUseObjectValueModel> map,
public SingleUseObjectMapStorage(CrudOperations<MapSingleUseObjectEntity, SingleUseObjectValueModel> map,
StringKeyConverter<K> keyConverter,
DeepCloner cloner,
Map<SearchableModelField<? super SingleUseObjectValueModel>,

View file

@ -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<V extends AbstractEntity, M> implements MapStorage<V, M> {
public class EmptyMapStorage {
private static final EmptyMapStorage<?, ?> INSTANCE = new EmptyMapStorage<>();
@SuppressWarnings("unchecked")
public static <V extends AbstractEntity, M> EmptyMapStorage<V, M> getInstance() {
return (EmptyMapStorage<V, M>) INSTANCE;
}
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
return new MapKeycloakTransaction<V, M>() {
public static <V extends AbstractEntity, M> MapStorage<V, M> getInstance() {
return new MapStorage<>() {
@Override
public V create(V value) {
return null;
@ -68,33 +58,6 @@ public class EmptyMapStorage<V extends AbstractEntity, M> implements MapStorage<
public long delete(QueryParameters<M> 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;
}
};
}
}

View file

@ -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<MapUserEntity, UserModel> tx;
private final boolean txHasRealmId;
final MapStorage<MapUserEntity, UserModel> store;
private final boolean storeHasRealmId;
public MapUserProvider(KeycloakSession session, MapStorage<MapUserEntity, UserModel> 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<MapUserEntity, UserModel> entityToAdapterFunc(RealmModel realm) {
@ -118,11 +116,11 @@ public class MapUserProvider implements UserProvider {
};
}
private MapKeycloakTransaction<MapUserEntity, UserModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
((HasRealmId) tx).setRealmId(realm == null ? null : realm.getId());
private MapStorage<MapUserEntity, UserModel> storeWithRealm(RealmModel realm) {
if (storeHasRealmId) {
((HasRealmId) store).setRealmId(realm == null ? null : realm.getId());
}
return tx;
return store;
}
private Predicate<MapUserEntity> entityRealmFilter(RealmModel realm) {
@ -139,7 +137,7 @@ public class MapUserProvider implements UserProvider {
private Optional<MapUserEntity> 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<UserModel> 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<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapUserEntity> 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<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapUserEntity> 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<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapUserEntity> 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<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapUserEntity> 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<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapUserEntity> 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<UserModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId());
try (Stream<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb))) {
try (Stream<MapUserEntity> 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<MapUserEntity> s = txInRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING))) {
try (Stream<MapUserEntity> s = storeWithRealm(realm).read(withCriteria(mcb).orderBy(SearchableFields.USERNAME, ASCENDING))) {
List<MapUserEntity> 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<MapUserEntity> usersWithEmail = txInRealm(realm).read(withCriteria(mcb)).collect(Collectors.toList());
List<MapUserEntity> 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<MapUserEntity> result = ((MapKeycloakTransactionWithAuth<MapUserEntity, UserModel>) tx).authenticate(realm, input);
if (r == null && store instanceof MapStorageWithAuth) {
MapCredentialValidationOutput<MapUserEntity> result = ((MapStorageWithAuth<MapUserEntity, UserModel>) store).authenticate(realm, input);
if (result != null) {
UserModel user = null;
if (result.getAuthenticatedUser() != null) {

View file

@ -46,7 +46,7 @@ public class MapUserProviderFactory extends AbstractMapProviderFactory<MapUserPr
@Override
public MapUserProvider createNew(KeycloakSession session) {
return new MapUserProvider(session, getStorage(session));
return new MapUserProvider(session, getMapStorage(session));
}
@Override

View file

@ -18,7 +18,6 @@ package org.keycloak.models.map.userSession;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.device.DeviceActivityManager;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
@ -30,7 +29,6 @@ import org.keycloak.models.UserSessionProvider;
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;
@ -64,20 +62,18 @@ public class MapUserSessionProvider implements UserSessionProvider {
private static final Logger LOG = Logger.getLogger(MapUserSessionProvider.class);
private final KeycloakSession session;
protected final MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> userSessionTx;
protected final MapStorage<MapUserSessionEntity, UserSessionModel> userSessionTx;
/**
* Storage for transient user sessions which lifespan is limited to one request.
*/
private final Map<String, MapUserSessionEntity> transientUserSessions = new HashMap<>();
private final boolean txHasRealmId;
private final boolean storeHasRealmId;
public MapUserSessionProvider(KeycloakSession session, MapStorage<MapUserSessionEntity, UserSessionModel> 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<MapUserSessionEntity, UserSessionModel> 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<MapUserSessionEntity, UserSessionModel> txInRealm(RealmModel realm) {
if (txHasRealmId) {
private MapStorage<MapUserSessionEntity, UserSessionModel> 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<UserSessionModel> 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<UserSessionModel> 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<MapUserSessionEntity> 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;

View file

@ -39,7 +39,7 @@ public class MapUserSessionProviderFactory extends AbstractMapProviderFactory<Ma
@Override
public MapUserSessionProvider createNew(KeycloakSession session) {
return new MapUserSessionProvider(session, getStorage(session));
return new MapUserSessionProvider(session, getMapStorage(session));
}
@Override

View file

@ -16,6 +16,10 @@
*/
package org.keycloak.testsuite.model;
import org.hamcrest.Matchers;
import org.jboss.logging.Logger;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
@ -27,20 +31,17 @@ import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityImpl;
import org.keycloak.models.map.client.MapClientProviderFactory;
import org.keycloak.models.map.common.DeepCloner;
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.common.StringKeyConverter;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.InvalidationHandler.ObjectType;
import org.hamcrest.Matchers;
import org.jboss.logging.Logger;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.provider.InvalidationHandler;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
@ -85,18 +86,18 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
String component2Id = createMapStorageComponent("component2", "keyType", "string");
String[] ids = withRealm(realmId, (session, realm) -> {
ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel> storage2 = (ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getMapStorage(ClientModel.class);
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getMapStorage(ClientModel.class);
ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel> storage2 = (ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel>) (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<K> kcMain = storageMain.getKeyConverter();
final StringKeyConverter<K1> kc1 = storage1.getKeyConverter();
final StringKeyConverter<K2> kc2 = storage2.getKeyConverter();
final StringKeyConverter<K> kcMain = (StringKeyConverter<K>) StringKeyConverter.UUIDKey.INSTANCE;
final StringKeyConverter<K1> kc1 = (StringKeyConverter<K1>) StringKeyConverter.ULongKey.INSTANCE;
final StringKeyConverter<K2> kc2 = (StringKeyConverter<K2>) 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<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getMapStorage(ClientModel.class);
@SuppressWarnings("unchecked")
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getMapStorage(ClientModel.class);
@SuppressWarnings("unchecked")
ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel> storage2 = (ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel> storage2 = (ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getMapStorage(ClientModel.class);
final StringKeyConverter<K> kcMain = storageMain.getKeyConverter();
final StringKeyConverter<K1> kc1 = storage1.getKeyConverter();
final StringKeyConverter<K2> kc2 = storage2.getKeyConverter();
final StringKeyConverter<K> kcMain = (StringKeyConverter<K>) StringKeyConverter.UUIDKey.INSTANCE;
final StringKeyConverter<K1> kc1 = (StringKeyConverter<K1>) StringKeyConverter.ULongKey.INSTANCE;
final StringKeyConverter<K2> kc2 = (StringKeyConverter<K2>) StringKeyConverter.StringKey.INSTANCE;
// Assert that the stores contain the created clients
assertThat(storageMain.read(idMain), notNullValue());

View file

@ -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,8 +44,6 @@ public class DictStorage<V extends AbstractEntity, M> implements MapStorage<V, M
return store;
}
private final class Transaction implements MapKeycloakTransaction<V, M> {
@Override
public V create(V value) {
V res = cloner.from(value);
@ -81,40 +78,4 @@ public class DictStorage<V extends AbstractEntity, M> implements MapStorage<V, M
public long delete(QueryParameters<M> 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 MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
return new Transaction();
}
}