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:
parent
1ee98bbbe7
commit
b730d861e7
90 changed files with 2404 additions and 2747 deletions
|
@ -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);
|
||||
}
|
|
@ -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]";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
protected Path getPathForEscapedId(String[] escapedIdPathArray) {
|
||||
Path parentDirectory = getDataDirectory();
|
||||
Path targetPath = parentDirectory;
|
||||
for (String path : escapedIdPathArray) {
|
||||
targetPath = targetPath.resolve(path).normalize();
|
||||
if (! targetPath.getParent().equals(parentDirectory)) {
|
||||
LOG.warnf("Path traversal detected: %s", Arrays.toString(escapedIdPathArray));
|
||||
return null;
|
||||
}
|
||||
parentDirectory = targetPath;
|
||||
}
|
||||
|
||||
return targetPath.resolveSibling(targetPath.getFileName() + FILE_SUFFIX);
|
||||
}
|
||||
|
||||
protected Path getPathForEscapedId(String escapedId) {
|
||||
if (escapedId == null) {
|
||||
throw new IllegalStateException("Invalid ID to escape");
|
||||
}
|
||||
|
||||
String[] escapedIdArray = ID_COMPONENT_SEPARATOR_PATTERN.split(escapedId);
|
||||
return getPathForEscapedId(escapedIdArray);
|
||||
}
|
||||
|
||||
// Percent sign + Unix (/) and https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file reserved characters
|
||||
private static final Pattern RESERVED_CHARACTERS = Pattern.compile("[%<:>\"/\\\\|?*=]");
|
||||
private static final String ID_COMPONENT_SEPARATOR = ":";
|
||||
private static final String ESCAPING_CHARACTER = "=";
|
||||
private static final Pattern ID_COMPONENT_SEPARATOR_PATTERN = Pattern.compile(Pattern.quote(ID_COMPONENT_SEPARATOR) + "+");
|
||||
|
||||
private static String[] escapeId(String[] idArray) {
|
||||
if (idArray == null || idArray.length == 0 || idArray.length == 1 && idArray[0] == null) {
|
||||
return null;
|
||||
}
|
||||
return Stream.of(idArray)
|
||||
.map(Crud::escapeId)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
private static String escapeId(String id) {
|
||||
Objects.requireNonNull(id, "ID must be non-null");
|
||||
|
||||
StringBuilder idEscaped = new StringBuilder();
|
||||
Matcher m = RESERVED_CHARACTERS.matcher(id);
|
||||
while (m.find()) {
|
||||
m.appendReplacement(idEscaped, String.format(ESCAPING_CHARACTER + "%02x", (int) m.group().charAt(0)));
|
||||
}
|
||||
m.appendTail(idEscaped);
|
||||
final Path pId = Path.of(idEscaped.toString());
|
||||
|
||||
return pId.toString();
|
||||
}
|
||||
|
||||
protected V parse(Path fileName) {
|
||||
getLastModifiedTime(fileName);
|
||||
final V parsedObject = YamlParser.parse(fileName, new MapEntityContext<>(entityClass));
|
||||
if (parsedObject == null) {
|
||||
LOG.debugf("Could not parse %s%s", fileName, StackUtil.getShortStackTrace());
|
||||
return null;
|
||||
}
|
||||
|
||||
String escapedId = determineKeyFromValue(parsedObject, false);
|
||||
final String fileNameStr = fileName.getFileName().toString();
|
||||
final String idFromFilename = fileNameStr.substring(0, fileNameStr.length() - FILE_SUFFIX.length());
|
||||
if (escapedId == null) {
|
||||
LOG.debugf("Determined ID from filename: %s%s", idFromFilename);
|
||||
escapedId = idFromFilename;
|
||||
} else if (! escapedId.endsWith(idFromFilename)) {
|
||||
LOG.warnf("Id \"%s\" does not conform with filename \"%s\", expected: %s", escapedId, fileNameStr, escapeId(escapedId));
|
||||
}
|
||||
|
||||
parsedObject.setId(escapedId);
|
||||
parsedObject.clearUpdatedFlag();
|
||||
|
||||
return parsedObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V create(V value) {
|
||||
// TODO: Lock realm directory for changes (e.g. on realm deletion)
|
||||
String escapedId = value.getId();
|
||||
|
||||
writeYamlContents(getPathForEscapedId(escapedId), value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns escaped ID - relative file name in the file system with path separator {@link #ID_COMPONENT_SEPARATOR}.
|
||||
* @param value Object
|
||||
* @param forCreate Whether this is for create operation ({@code true}) or
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String determineKeyFromValue(V value, boolean forCreate) {
|
||||
final boolean randomId;
|
||||
String[] proposedId = suggestedPath.apply(value);
|
||||
|
||||
if (! forCreate) {
|
||||
String[] escapedProposedId = escapeId(proposedId);
|
||||
final String res = proposedId == null ? null : String.join(ID_COMPONENT_SEPARATOR, escapedProposedId);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debugf("determineKeyFromValue: got %s (%s) for %s", res, res == null ? null : String.join(" [/] ", proposedId), value);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
if (proposedId == null || proposedId.length == 0) {
|
||||
randomId = value.getId() == null;
|
||||
proposedId = new String[] { value.getId() == null ? StringKey.INSTANCE.yieldNewUniqueKey() : value.getId() };
|
||||
} else {
|
||||
randomId = false;
|
||||
}
|
||||
|
||||
String[] escapedProposedId = escapeId(proposedId);
|
||||
Path sp = getPathForEscapedId(escapedProposedId); // sp will never be null
|
||||
|
||||
final Path parentDir = sp.getParent();
|
||||
if (! Files.isDirectory(parentDir)) {
|
||||
try {
|
||||
Files.createDirectories(parentDir);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Directory does not exist and cannot be created: " + parentDir, ex);
|
||||
}
|
||||
}
|
||||
|
||||
for (int counter = 0; counter < 100; counter++) {
|
||||
LOG.tracef("Attempting to create file %s", sp, StackUtil.getShortStackTrace());
|
||||
try {
|
||||
touch(sp);
|
||||
final String res = String.join(ID_COMPONENT_SEPARATOR, escapedProposedId);
|
||||
LOG.debugf("determineKeyFromValue: got %s for created %s", res, value);
|
||||
return res;
|
||||
} catch (FileAlreadyExistsException ex) {
|
||||
if (! randomId) {
|
||||
throw new ModelDuplicateException("File " + sp + " already exists!");
|
||||
}
|
||||
final String lastComponent = StringKey.INSTANCE.yieldNewUniqueKey();
|
||||
escapedProposedId[escapedProposedId.length - 1] = lastComponent;
|
||||
sp = getPathForEscapedId(escapedProposedId);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not create file " + sp, ex);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V read(String key) {
|
||||
return Optional.ofNullable(key)
|
||||
.map(this::getPathForEscapedId)
|
||||
.filter(Files::isReadable)
|
||||
.map(this::parse)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public MapModelCriteriaBuilder<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}.
|
||||
*
|
||||
* @param path the {@link Path} to the file whose last modified time it to be obtained.
|
||||
* @return the {@link FileTime} corresponding to the file's last modified time.
|
||||
*/
|
||||
protected abstract FileTime getLastModifiedTime(final Path path);
|
||||
|
||||
/**
|
||||
* Hook to validate that it is safe to modify the file identified by the supplied {@link Path}. The primary
|
||||
* goal is to identify if other transactions have modified the file after it was read by the current transaction,
|
||||
* preventing updates to a stale entity.
|
||||
*
|
||||
* @param path the {@link Path} to the file that is to be modified.
|
||||
*/
|
||||
protected abstract void checkIsSafeToModify(final Path path);
|
||||
private static void silentDelete(Path p) {
|
||||
silenteDelete(p, false);
|
||||
}
|
||||
|
||||
private static void silenteDelete(final Path path, final boolean checkEmpty) {
|
||||
try {
|
||||
if (Files.exists(path)) {
|
||||
if (!checkEmpty || Files.size(path) == 0) {
|
||||
Files.delete(path);
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
// swallow the exception.
|
||||
}
|
||||
}
|
||||
|
||||
public void touch(Path path) throws IOException {
|
||||
Files.createFile(path);
|
||||
createdPaths.add(path);
|
||||
}
|
||||
|
||||
public boolean removeIfExists(Path path) {
|
||||
final boolean res = ! pathsToDelete.contains(path) && Files.exists(path);
|
||||
pathsToDelete.add(path);
|
||||
return res;
|
||||
}
|
||||
|
||||
void registerRenameOnCommit(Path from, Path to) {
|
||||
this.renameOnCommit.put(from, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains and stores the last modified time of the file identified by the supplied {@link Path}. This value is used
|
||||
* to determine if the file was changed by another transaction after it was read by this transaction.
|
||||
*
|
||||
* @param path the {@link Path} to the file.
|
||||
*/
|
||||
FileTime getLastModifiedTime(final Path path) {
|
||||
try {
|
||||
BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
FileTime lastModifiedTime = attr.lastModifiedTime();
|
||||
this.lastModified.put(path, lastModifiedTime);
|
||||
return lastModifiedTime;
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not read file attributes " + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if it is safe to modify the file identified by the supplied {@link Path}. In particular, this method
|
||||
* verifies if the file was changed (removed, updated) after it was read by this transaction. Being it the case, this
|
||||
* transaction should refrain from performing further updates as it must assume its data has become stale.
|
||||
*
|
||||
* @param path the {@link Path} to the file that will be updated.
|
||||
* @throws IllegalStateException if the file was altered by another transaction.
|
||||
*/
|
||||
void checkIsSafeToModify(final Path path) {
|
||||
try {
|
||||
// path wasn't previously loaded - log a message and return.
|
||||
if (this.lastModified.get(path) == null) {
|
||||
LOG.debugf("File %s was not previously loaded, skipping validation prior to writing", path);
|
||||
return;
|
||||
}
|
||||
// check if the original file was deleted by another transaction.
|
||||
if (!Files.exists(path)) {
|
||||
throw new IllegalStateException("File " + path + " was removed by another transaction");
|
||||
}
|
||||
// check if the original file was modified by another transaction.
|
||||
BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
long lastModifiedTime = attr.lastModifiedTime().toMillis();
|
||||
if (this.lastModified.get(path).toMillis() < lastModifiedTime) {
|
||||
throw new IllegalStateException("File " + path + " was changed by another transaction");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public V registerEntityForChanges(V origEntity) {
|
||||
final V watchedValue = super.registerEntityForChanges(origEntity);
|
||||
return DeepCloner.DUMB_CLONER.entityFieldDelegate(watchedValue, new IdProtector(watchedValue));
|
||||
}
|
||||
|
||||
private static class Crud<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]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -18,53 +18,98 @@
|
|||
package org.keycloak.models.map.storage.hotRod;
|
||||
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.SingleUseObjectValueModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.StringKeyConverter;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||
import org.keycloak.models.map.storage.MapStorageProviderFactory;
|
||||
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorage;
|
||||
import org.keycloak.models.map.storage.chm.MapFieldPredicates;
|
||||
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
|
||||
import org.keycloak.models.map.storage.chm.SingleUseObjectMapStorage;
|
||||
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
|
||||
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate;
|
||||
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor;
|
||||
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider;
|
||||
import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodStoresWrapper;
|
||||
import org.keycloak.models.map.storage.hotRod.transaction.HotRodRemoteTransactionWrapper;
|
||||
import org.keycloak.models.map.storage.hotRod.transaction.AllAreasHotRodTransactionsWrapper;
|
||||
import org.keycloak.models.map.storage.hotRod.userSession.HotRodUserSessionMapStorage;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory.CLIENT_SESSION_PREDICATES;
|
||||
import static org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory.CLONER;
|
||||
|
||||
public class HotRodMapStorageProvider implements MapStorageProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final HotRodMapStorageProviderFactory factory;
|
||||
private final String hotRodConfigurationIdentifier;
|
||||
private final boolean jtaEnabled;
|
||||
private final long lockTimeout;
|
||||
private AllAreasHotRodStoresWrapper storesWrapper;
|
||||
|
||||
public HotRodMapStorageProvider(KeycloakSession session, HotRodMapStorageProviderFactory factory, String hotRodConfigurationIdentifier, boolean jtaEnabled) {
|
||||
|
||||
public HotRodMapStorageProvider(KeycloakSession session, HotRodMapStorageProviderFactory factory, boolean jtaEnabled, long lockTimeout) {
|
||||
this.session = session;
|
||||
this.factory = factory;
|
||||
this.hotRodConfigurationIdentifier = hotRodConfigurationIdentifier;
|
||||
this.jtaEnabled = jtaEnabled;
|
||||
this.lockTimeout = lockTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <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);
|
||||
|
||||
if (!jtaEnabled) {
|
||||
// If there is no JTA transaction enabled control HotRod client provided transaction manually using
|
||||
// HotRodRemoteTransactionWrapper
|
||||
HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class);
|
||||
HotRodEntityDescriptor<?, ?> entityDescriptor = factory.getEntityDescriptor(modelType);
|
||||
RemoteCache<Object, Object> remoteCache = connectionProvider.getRemoteCache(entityDescriptor.getCacheName());
|
||||
session.getTransactionManager().enlist(new HotRodRemoteTransactionWrapper(remoteCache.getTransactionManager()));
|
||||
}
|
||||
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
|
||||
HotRodConnectionProvider connectionProvider = session.getProvider(HotRodConnectionProvider.class);
|
||||
HotRodEntityDescriptor<?, ?> entityDescriptor = factory.getEntityDescriptor(modelType);
|
||||
RemoteCache<Object, Object> remoteCache = connectionProvider.getRemoteCache(entityDescriptor.getCacheName());
|
||||
session.getTransactionManager().enlist(new HotRodRemoteTransactionWrapper(remoteCache.getTransactionManager()));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
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);
|
||||
public SingleUseObjectHotRodCrudOperations(KeycloakSession session, RemoteCache<String, HotRodSingleUseObjectEntity> remoteCache, StringKeyConverter<String> keyConverter,
|
||||
HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate> storedEntityDescriptor,
|
||||
DeepCloner cloner, Long lockTimeout) {
|
||||
super(session, remoteCache, keyConverter, storedEntityDescriptor, cloner, lockTimeout);
|
||||
}
|
||||
|
||||
@Override
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
StringKeyConverter<K> keyConverter,
|
||||
DeepCloner cloner,
|
||||
Map<SearchableModelField<? super UserSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapUserSessionEntity, UserSessionModel>> fieldPredicates,
|
||||
MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTransaction
|
||||
public HotRodUserSessionMapStorage(CrudOperations<MapUserSessionEntity, UserSessionModel> map,
|
||||
StringKeyConverter<K> keyConverter,
|
||||
DeepCloner cloner,
|
||||
Map<SearchableModelField<? super UserSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapUserSessionEntity, UserSessionModel>> fieldPredicates,
|
||||
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)))
|
|
@ -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")));
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 {
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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<>();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
@ -189,11 +186,11 @@ public class MapPolicyStore implements PolicyStore {
|
|||
return mcb.compare(name.getSearchableModelField(), Operator.IN, Arrays.asList(value));
|
||||
case PERMISSION: {
|
||||
mcb = mcb.compare(SearchableFields.TYPE, Operator.IN, Arrays.asList("resource", "scope", "uma"));
|
||||
|
||||
|
||||
if (!Boolean.parseBoolean(value[0])) {
|
||||
mcb = DefaultModelCriteria.<Policy>criteria().not(mcb); // TODO: create NOT_IN operator
|
||||
}
|
||||
|
||||
|
||||
return mcb;
|
||||
}
|
||||
case ANY_OWNER:
|
||||
|
@ -202,7 +199,7 @@ public class MapPolicyStore implements PolicyStore {
|
|||
if (value.length != 2) {
|
||||
throw new IllegalArgumentException("Config filter option requires value with two items: [config_name, expected_config_value]");
|
||||
}
|
||||
|
||||
|
||||
value[1] = "%" + value[1] + "%";
|
||||
return mcb.compare(SearchableFields.CONFIG, Operator.LIKE, (Object[]) value);
|
||||
case TYPE:
|
||||
|
@ -218,7 +215,7 @@ public class MapPolicyStore implements PolicyStore {
|
|||
public void findByResource(ResourceServer resourceServer, Resource resource, Consumer<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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* @return See description. Never returns {@code null}
|
||||
*/
|
||||
MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* The ID of the entity passed in the parameter might change to a different value in the returned value
|
||||
* if the underlying storage decided this was necessary.
|
||||
* If the ID of the entity was null before, it will be set on the returned value.
|
||||
*
|
||||
* @param value the value
|
||||
* @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}.
|
||||
*/
|
||||
V create(V value);
|
||||
|
||||
/**
|
||||
* Provides possibility to lookup for values by a {@code key} in the underlying store with respect to changes done
|
||||
* in current transaction. Updates to the returned instance would be visible in the current transaction
|
||||
* and will propagate into the underlying store upon commit.
|
||||
*
|
||||
* If {@code V} implements {@link org.keycloak.models.map.common.ExpirableEntity} this method should not return
|
||||
* entities that are expired. See {@link org.keycloak.models.map.common.ExpirableEntity} JavaDoc for more details.
|
||||
*
|
||||
* @param key identifier of a value
|
||||
* @return a value associated with the given {@code key}
|
||||
*/
|
||||
V read(String key);
|
||||
|
||||
/**
|
||||
* Returns a stream of values from underlying storage that are updated based on the current transaction changes;
|
||||
* i.e. the result contains updates and excludes of records that have been created, updated or deleted in this
|
||||
* transaction by respective methods of this interface.
|
||||
* <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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,12 +28,13 @@ 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 <V> type of the value
|
||||
* @param <M> type of the corresponding model (e.g. {@code UserModel})
|
||||
* @param modelType Model type
|
||||
* @param flags Flags of the returned storage. Best effort, flags may be not honored by underlying implementation
|
||||
* @param flags Flags of the returned storage. Best effort, flags may be not honored by underlying implementation
|
||||
* @return
|
||||
* @throws IllegalArgumentException If some of the types is not supported by the underlying implementation.
|
||||
*/
|
||||
<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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
/**
|
||||
*
|
||||
* It contains basic object CRUD operations as well as bulk {@link #read(org.keycloak.models.map.storage.QueryParameters)}
|
||||
* and bulk {@link #delete(org.keycloak.models.map.storage.QueryParameters)} operations,
|
||||
* and operation for determining the number of the objects satisfying given criteria
|
||||
* ({@link #getCount(org.keycloak.models.map.storage.QueryParameters)}).
|
||||
*
|
||||
* @author hmlnarik
|
||||
*/
|
||||
public class ConcurrentHashMapCrudOperations<K, V extends AbstractEntity & UpdatableEntity, M> implements CrudOperations<V, M> {
|
||||
|
||||
/**
|
||||
* Updates the object with the key of the {@code value}'s ID in the storage if it already exists.
|
||||
*
|
||||
* @param value Updated value
|
||||
* @return the previous value associated with the specified key, or null if there was no mapping for the key.
|
||||
* (A null return can also indicate that the map previously associated null with the key,
|
||||
* if the implementation supports null values.)
|
||||
* @throws NullPointerException if the object or its {@code id} is {@code null}
|
||||
* @see AbstractEntity#getId()
|
||||
*/
|
||||
V update(V value);
|
||||
protected final ConcurrentMap<K, V> store = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Deletes object with the given {@code key} from the storage, if exists, no-op otherwise.
|
||||
* @param key
|
||||
* @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise.
|
||||
*/
|
||||
boolean delete(String key);
|
||||
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
|
||||
protected final StringKeyConverter<K> keyConverter;
|
||||
protected final DeepCloner cloner;
|
||||
private final boolean isExpirableEntity;
|
||||
|
||||
/**
|
||||
* Deletes objects that match the given criteria.
|
||||
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
|
||||
* @return Number of removed objects (might return {@code -1} if not supported)
|
||||
*/
|
||||
long delete(QueryParameters<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.
|
||||
*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
@ -16,172 +16,224 @@
|
|||
*/
|
||||
package org.keycloak.models.map.storage.chm;
|
||||
|
||||
import org.keycloak.models.map.common.ExpirableEntity;
|
||||
import org.keycloak.models.map.common.ExpirationUtils;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.map.common.StringKeyConverter;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.EntityField;
|
||||
import org.keycloak.models.map.common.HasRealmId;
|
||||
import org.keycloak.models.map.common.UpdatableEntity;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.map.storage.CrudOperations;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
import org.keycloak.models.map.storage.ModelEntityUtil;
|
||||
import org.keycloak.models.map.storage.QueryParameters;
|
||||
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc;
|
||||
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Stream;
|
||||
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
public class ConcurrentHashMapStorage<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);
|
||||
|
||||
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;
|
||||
public void commit() {
|
||||
if (rollback) {
|
||||
throw new RuntimeException("Rollback only!");
|
||||
}
|
||||
|
||||
@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()));
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
paginatedStream(storeStream.filter(next -> keyFilter.test(next.getKey()) && entityFilter.test(next.getValue()))
|
||||
, queryParameters.getOffset(), queryParameters.getLimit())
|
||||
.peek(item -> {res.incrementAndGet();})
|
||||
.map(Entry::getKey)
|
||||
.forEach(store::remove);
|
||||
|
||||
return res.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public MapKeycloakTransaction<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 void rollback() {
|
||||
tasks.clear();
|
||||
}
|
||||
|
||||
public MapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() {
|
||||
@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);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
|
@ -21,6 +21,7 @@ import org.keycloak.models.SingleUseObjectValueModel;
|
|||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.common.StringKeyConverter;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
|
||||
import org.keycloak.models.map.storage.CrudOperations;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.Map;
|
||||
|
@ -28,12 +29,12 @@ import java.util.Map;
|
|||
/**
|
||||
* @author <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,
|
||||
StringKeyConverter<K> keyConverter,
|
||||
DeepCloner cloner,
|
||||
Map<SearchableModelField<? super SingleUseObjectValueModel>,
|
||||
public SingleUseObjectMapStorage(CrudOperations<MapSingleUseObjectEntity, SingleUseObjectValueModel> map,
|
||||
StringKeyConverter<K> keyConverter,
|
||||
DeepCloner cloner,
|
||||
Map<SearchableModelField<? super SingleUseObjectValueModel>,
|
||||
MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapSingleUseObjectEntity, SingleUseObjectValueModel>> fieldPredicates) {
|
||||
super(map, keyConverter, cloner, fieldPredicates);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
*/
|
||||
package org.keycloak.testsuite.model.storage.tree.sample;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.map.common.AbstractEntity;
|
||||
import org.keycloak.models.map.common.DeepCloner;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.MapStorage;
|
||||
import org.keycloak.models.map.storage.QueryParameters;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
@ -45,76 +44,38 @@ public class DictStorage<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);
|
||||
store.add(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public V read(String key) {
|
||||
return store.stream()
|
||||
.filter(e -> Objects.equals(e.getId(), key))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<V> read(QueryParameters<M> queryParameters) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCount(QueryParameters<M> queryParameters) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long delete(QueryParameters<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 V create(V value) {
|
||||
V res = cloner.from(value);
|
||||
store.add(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
|
||||
return new Transaction();
|
||||
public V read(String key) {
|
||||
return store.stream()
|
||||
.filter(e -> Objects.equals(e.getId(), key))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<V> read(QueryParameters<M> queryParameters) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCount(QueryParameters<M> queryParameters) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long delete(QueryParameters<M> queryParameters) {
|
||||
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue