Leverage Infinispan lifespan for ExpirableEntities in HotRod storage

This commit is contained in:
Martin Kanis 2022-08-29 16:12:28 +02:00 committed by Michal Hajas
parent fc075a3d35
commit 5ba004b447
27 changed files with 435 additions and 62 deletions

View file

@ -45,12 +45,12 @@ import org.keycloak.storage.SearchableModelField;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterators;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static org.keycloak.models.map.common.ExpirationUtils.isExpired;
import static org.keycloak.models.map.storage.hotRod.common.HotRodUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing;
@ -82,6 +82,17 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
value = cloner.from(keyConverter.keyToString(key), value);
}
if (isExpirableEntity) {
Long lifespan = getLifespan(value);
if (lifespan != null) {
if (lifespan > 0) {
remoteCache.putIfAbsent(key, value.getHotRodEntity(), lifespan, TimeUnit.MILLISECONDS);
} else {
LOG.warnf("Skipped creation of entity %s in storage due to negative/zero lifespan.", key);
}
return value;
}
}
remoteCache.putIfAbsent(key, value.getHotRodEntity());
return value;
@ -97,20 +108,28 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
if (hotRodEntity == null) return null;
// Create delegate that implements Map*Entity
V delegateEntity = delegateProducer.apply(hotRodEntity);
// Check expiration if necessary and return value
return isExpirableEntity && isExpired((ExpirableEntity) delegateEntity, true) ? null : delegateEntity;
return delegateProducer.apply(hotRodEntity);
}
@Override
public V update(V value) {
K key = keyConverter.fromStringSafe(value.getId());
if (isExpirableEntity) {
Long lifespan = getLifespan(value);
if (lifespan != null) {
E previousValue;
if (lifespan > 0) {
previousValue = remoteCache.replace(key, value.getHotRodEntity(), lifespan, TimeUnit.MILLISECONDS);
} else {
LOG.warnf("Removing entity %s from storage due to negative/zero lifespan.", key);
previousValue = remoteCache.remove(key);
}
return previousValue == null ? null : delegateProducer.apply(previousValue);
}
}
E previousValue = remoteCache.replace(key, value.getHotRodEntity());
if (previousValue == null) return null;
return delegateProducer.apply(previousValue);
return previousValue == null ? null : delegateProducer.apply(previousValue);
}
@Override
@ -127,22 +146,12 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
return modelFieldName + " " + orderString;
}
private static String isNotExpiredIckleWhereClause() {
return "(" + IckleQueryOperators.C + ".expiration > " + Time.currentTimeMillis() + " OR "
+ IckleQueryOperators.C + ".expiration is null)";
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
IckleQueryMapModelCriteriaBuilder<E, M> iqmcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createCriteriaBuilder());
String queryString = iqmcb.getIckleQuery();
// Temporary solution until https://github.com/keycloak/keycloak/issues/12068 is fixed
if (isExpirableEntity) {
queryString += (queryString.contains("WHERE") ? " AND " : " WHERE ") + isNotExpiredIckleWhereClause();
}
if (!queryParameters.getOrderBy().isEmpty()) {
queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodMapStorage::toOrderString)
.collect(Collectors.joining(", "));
@ -232,4 +241,12 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends Abstr
Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, V, M>> fieldPredicates = MapFieldPredicates.getPredicates((Class<M>) storedEntityDescriptor.getModelTypeClass());
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
private Long getLifespan(V value) {
Long expiration = ((ExpirableEntity) value).getExpiration();
return expiration != null ? expiration - Time.currentTimeMillis() : null;
}
}

View file

@ -78,7 +78,6 @@ public class HotRodRootAuthenticationSessionEntity extends AbstractHotRodEntity
@ProtoField(number = 4)
public Long timestamp;
@Basic(sortable = true)
@ProtoField(number = 5)
public Long expiration;

View file

@ -26,12 +26,14 @@ import org.infinispan.commons.marshall.ProtoStreamMarshaller;
import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.map.storage.hotRod.locking.HotRodLocksUtils;
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor;
import org.keycloak.models.map.storage.hotRod.common.CommonPrimitivesProtoSchemaInitializer;
import org.keycloak.models.map.storage.hotRod.common.HotRodVersionUtils;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import java.net.URI;
import java.net.URISyntaxException;
@ -49,9 +51,10 @@ import static org.keycloak.models.map.storage.hotRod.common.HotRodVersionUtils.i
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionProviderFactory {
public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionProviderFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "default";
public static final String SCRIPT_CACHE = "___script_cache";
public static final String HOT_ROD_LOCKS_CACHE_NAME = "locks";
private static final String HOT_ROD_INIT_LOCK_NAME = "HOT_ROD_INIT_LOCK";
private static final Logger LOG = Logger.getLogger(DefaultHotRodConnectionProviderFactory.class);
@ -267,4 +270,9 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
.nearCacheUseBloomFilter(config.scope(cacheName).getBoolean("nearCacheBloomFilter", config.getBoolean("nearCacheBloomFilter", false)));
};
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
}

View file

@ -59,7 +59,6 @@ public class HotRodAdminEventEntity extends AbstractHotRodEntity {
@ProtoField(number = 2)
public String id;
@Basic(sortable = true)
@ProtoField(number = 3)
public Long expiration;

View file

@ -68,7 +68,6 @@ public class HotRodAuthEventEntity extends AbstractHotRodEntity {
@ProtoField(number = 3)
public Integer type;
@Basic(sortable = true)
@ProtoField(number = 4)
public Long expiration;

View file

@ -109,7 +109,6 @@ public class HotRodUserSessionEntity extends AbstractHotRodEntity {
@ProtoField(number = 12)
public Long lastSessionRefresh;
@Basic(sortable = true)
@ProtoField(number = 13)
public Long expiration;

View file

@ -37,6 +37,7 @@ import org.keycloak.storage.SearchableModelField;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -65,6 +66,7 @@ public class HotRodUserSessionTransaction<K> extends ConcurrentHashMapKeycloakTr
}
private MapAuthenticatedClientSessionEntity wrapClientSessionEntityToClientSessionAwareDelegate(MapAuthenticatedClientSessionEntity d) {
if (!clientSessionTransaction.exists(d.getId())) return null;
return new MapAuthenticatedClientSessionEntityDelegate(new HotRodAuthenticatedClientSessionEntityDelegateProvider(d) {
@Override
public MapAuthenticatedClientSessionEntity loadClientSessionFromDatabase() {
@ -82,13 +84,15 @@ public class HotRodUserSessionTransaction<K> extends ConcurrentHashMapKeycloakTr
Set<MapAuthenticatedClientSessionEntity> clientSessions = super.getAuthenticatedClientSessions();
return clientSessions == null ? null : clientSessions.stream()
.map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}
@Override
public Optional<MapAuthenticatedClientSessionEntity> getAuthenticatedClientSession(String clientUUID) {
return super.getAuthenticatedClientSession(clientUUID)
.map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate);
.map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate)
.filter(Objects::nonNull);
}
@Override

View file

@ -67,6 +67,11 @@
<version>${project.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-tasks-api</artifactId>
<version>${infinispan.version}</version>
</dependency>
</dependencies>
<build>

View file

@ -0,0 +1,112 @@
/*
* 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.testsuite.model.infinispan;
import org.infinispan.commons.logging.Log;
import org.infinispan.commons.logging.LogFactory;
import org.infinispan.commons.time.TimeService;
import org.infinispan.expiration.ExpirationManager;
import org.infinispan.factories.GlobalComponentRegistry;
import org.infinispan.factories.impl.BasicComponentRegistry;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.tasks.ServerTask;
import org.infinispan.tasks.TaskContext;
import org.infinispan.tasks.TaskExecutionMode;
import org.infinispan.util.EmbeddedTimeService;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class InfinispanTimeServiceTask implements ServerTask<String> {
private static final Log log = LogFactory.getLog(InfinispanTimeServiceTask.class);
private TaskContext context = null;
private static int offset;
public InfinispanTimeServiceTask() {
log.info("InfinispanTimeServiceTask construction");
}
@Override
public String call() {
EmbeddedCacheManager cacheManager = context.getCacheManager();
Map<String, Object> params = new HashMap();
if (this.context.getParameters().isPresent())
params = this.context.getParameters().get();
if (params.containsKey("timeService")) {
offset = (int) params.get("timeService");
// rewire the Time service
GlobalComponentRegistry cr = cacheManager.getGlobalComponentRegistry();
BasicComponentRegistry bcr = cr.getComponent(BasicComponentRegistry.class);
bcr.replaceComponent(TimeService.class.getName(), KEYCLOAK_TIME_SERVICE, true);
cr.rewire();
cr.rewireNamedRegistries();
// process expiration in all caches
cacheManager.getCacheNames().stream()
.map(cacheManager::getCache)
.filter(Objects::nonNull)
.map(cache -> cache.getAdvancedCache().getExpirationManager())
.forEach(ExpirationManager::processExpiration);
}
return "InfinispanTimeServiceTask: Infinispan server time moved by " + offset + " seconds.";
}
@Override
public String getName() {
log.info("getName() called");
return "InfinispanTimeServiceTask";
}
@Override
public void setTaskContext(TaskContext context) {
this.context = context;
}
@Override
public TaskExecutionMode getExecutionMode() {
return TaskExecutionMode.ALL_NODES;
}
public static final TimeService KEYCLOAK_TIME_SERVICE = new EmbeddedTimeService() {
private long getCurrentTimeMillis() {
return System.currentTimeMillis() + (TimeUnit.SECONDS.toMillis(offset));
}
@Override
public long wallClockTime() {
return getCurrentTimeMillis();
}
@Override
public long time() {
return TimeUnit.MILLISECONDS.toNanos(getCurrentTimeMillis());
}
@Override
public Instant instant() {
return Instant.ofEpochMilli(getCurrentTimeMillis());
}
};
}

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.rest;
import org.infinispan.client.hotrod.RemoteCache;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.http.HttpRequest;
import org.keycloak.Config;
@ -45,6 +46,13 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.map.common.AbstractMapProviderFactory;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory;
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider;
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStoreFactory;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.ResetTimeOffsetEvent;
@ -238,6 +246,18 @@ public class TestingResourceProvider implements RealmResourceProvider {
public Map<String, String> setTimeOffset(Map<String, String> time) {
int offset = Integer.parseInt(time.get("offset"));
// move time on Hot Rod server if present
// determine usage of Infinispan based on user sessions config
String userSessionProvider = Config.scope(UserSessionSpi.NAME, MapUserSessionProviderFactory.PROVIDER_ID, AbstractMapProviderFactory.CONFIG_STORAGE).get("provider");
if (Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE) && "hotrod".equals(userSessionProvider)) {
RemoteCache<Object, Object> scriptCache = session.getProvider(HotRodConnectionProvider.class).getRemoteCache(DefaultHotRodConnectionProviderFactory.SCRIPT_CACHE);
if (scriptCache != null) {
Map<String, Object> param = new HashMap<>();
param.put("timeService", offset);
scriptCache.execute("InfinispanTimeServiceTask", param);
}
}
Time.setOffset(offset);
// Time offset was restarted

View file

@ -0,0 +1 @@
org.keycloak.testsuite.model.infinispan.InfinispanTimeServiceTask

View file

@ -327,6 +327,18 @@
</artifactItems>
</configuration>
</execution>
<execution>
<id>copy-testsuite-providers-to-base-testsuite</id>
<phase>generate-test-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>org.keycloak.testsuite</includeGroupIds>
<includeArtifactIds>integration-arquillian-testsuite-providers</includeArtifactIds>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
@ -957,6 +969,7 @@
<keycloak.authEventsStore.map.storage.provider>hotrod</keycloak.authEventsStore.map.storage.provider>
<keycloak.singleUseObject.map.storage.provider>hotrod</keycloak.singleUseObject.map.storage.provider>
<infinispan.version>${infinispan.version}</infinispan.version>
<project.version>${project.version}</project.version>
<keycloak.testsuite.start-hotrod-container>${keycloak.testsuite.start-hotrod-container}</keycloak.testsuite.start-hotrod-container>
<auth.server.quarkus.mapStorage.profile.config>hotrod</auth.server.quarkus.mapStorage.profile.config>
<keycloak.globalLock.provider>hotrod</keycloak.globalLock.provider>

View file

@ -12,10 +12,11 @@ public class HotRodStoreTestEnricher {
public static final boolean HOT_ROD_START_CONTAINER = Boolean.parseBoolean(System.getProperty("keycloak.testsuite.start-hotrod-container", "false"));
private final InfinispanContainer hotRodContainer = new InfinispanContainer();
private InfinispanContainer hotRodContainer;
public void beforeContainerStarted(@Observes(precedence = 1) StartSuiteContainers event) {
if (!HOT_ROD_START_CONTAINER) return;
hotRodContainer = new InfinispanContainer();
hotRodContainer.start();
// Add env variable, so it can be picked up by Keycloak
@ -24,6 +25,6 @@ public class HotRodStoreTestEnricher {
public void afterSuite(@Observes(precedence = 4) AfterSuite event) {
if (!HOT_ROD_START_CONTAINER) return;
hotRodContainer.stop();
if (hotRodContainer != null) hotRodContainer.stop();
}
}

View file

@ -21,7 +21,11 @@ import org.jboss.logging.Logger;
import org.keycloak.testsuite.arquillian.HotRodStoreTestEnricher;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.MountableFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -51,6 +55,18 @@ public class InfinispanContainer extends GenericContainer<InfinispanContainer> {
withEnv("PASS", PASSWORD);
withNetworkMode("host");
Path dir = Path.of(Path.of("").toAbsolutePath() + "/target/lib");
String projectVersion = System.getProperty("project.version");
Path timeTaskPath;
try {
timeTaskPath = Files.find(dir, 1, (path, attr) -> path.toString()
.endsWith("integration-arquillian-testsuite-providers-" + projectVersion + ".jar")).findFirst().orElse(null);
} catch (IOException e) {
throw new RuntimeException(e);
}
MountableFile mountableFile = MountableFile.forHostPath(timeTaskPath, 0666);
withCopyFileToContainer(mountableFile, "/opt/infinispan/server/lib/integration-arquillian-testsuite-providers.jar");
withStartupTimeout(Duration.ofMinutes(5));
waitingFor(Wait.forLogMessage(".*Infinispan Server.*started in.*", 1));
}

View file

@ -644,6 +644,7 @@ public abstract class AbstractKeycloakTest {
/**
* Sets time offset in seconds that will be added to Time.currentTime() and Time.currentTimeMillis() both for client and server.
* Moves time on the remote Infinispan server as well if the HotRod storage is used.
*
* @param offset
*/

View file

@ -0,0 +1,66 @@
/*
* 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.testsuite.admin;
import org.keycloak.Config;
import org.keycloak.common.util.Time;
import org.keycloak.events.Event;
import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventStoreSpi;
import org.keycloak.events.EventType;
import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
import org.keycloak.models.RealmModel;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class TimeOffsetTest extends AbstractAdminTest {
@Test
public void testOffset() {
String realmId = adminClient.realm(REALM_NAME).toRepresentation().getId();
testingClient.server().run(session -> {
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
realm.setEventsExpiration(5);
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
Event e = new Event();
e.setType(EventType.LOGIN);
e.setTime(Time.currentTimeMillis());
e.setRealmId(realmId);
provider.onEvent(e);
});
testingClient.server().run(session -> {
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
assertEquals(1, provider.createQuery().realm(realmId).getResultStream().count());
});
setTimeOffset(5);
// legacy store requires manual trigger of expired events removal
String eventStoreProvider = testingClient.server().fetch(session -> Config.getProvider(EventStoreSpi.NAME), String.class);
if (eventStoreProvider.equals(JpaEventStoreProviderFactory.ID)) {
testingClient.testing().clearExpiredEvents();
}
testingClient.server().run(session -> {
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
assertEquals(0, provider.createQuery().realm(realmId).getResultStream().count());
});
}
}

View file

@ -179,6 +179,7 @@
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<org.jboss.logging.provider>log4j</org.jboss.logging.provider>
<infinispan.version>${infinispan.version}</infinispan.version>
<project.version>${project.version}</project.version>
</systemPropertyVariables>
<properties>
<property>
@ -208,6 +209,23 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-testsuite-providers-to-model-testsuite</id>
<phase>generate-test-resources</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>org.keycloak.testsuite</includeGroupIds>
<includeArtifactIds>integration-arquillian-testsuite-providers</includeArtifactIds>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.model;
import org.infinispan.client.hotrod.RemoteCache;
import org.junit.Assert;
import org.keycloak.Config.Scope;
import org.keycloak.authorization.AuthorizationSpi;
@ -45,6 +46,8 @@ import org.keycloak.models.DeploymentStateSpi;
import org.keycloak.models.UserLoginFailureSpi;
import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.UserSpi;
import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory;
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider;
import org.keycloak.models.locking.GlobalLockProviderSpi;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.PostMigrationEvent;
@ -63,7 +66,7 @@ import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@ -81,7 +84,6 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@ -527,17 +529,17 @@ public abstract class KeycloakModelTest {
@Before
public final void createEnvironment() {
Time.setOffset(0);
setTimeOffset(0);
USE_DEFAULT_FACTORY = isUseSameKeycloakSessionFactoryForAllThreads();
KeycloakModelUtils.runJobInTransaction(getFactory(), this::createEnvironment);
}
@After
public final void cleanEnvironment() {
Time.setOffset(0);
if (getFactory() == null) {
reinitializeKeycloakSessionFactory();
}
setTimeOffset(0);
KeycloakModelUtils.runJobInTransaction(getFactory(), this::cleanEnvironment);
}
@ -637,4 +639,24 @@ public abstract class KeycloakModelTest {
return realm;
}
/**
* Moves time on the Keycloak server as well as on the remote Infinispan server if the Infinispan is used.
* @param seconds time offset in seconds by which Keycloak (and Infinispan) server time is moved
*/
protected void setTimeOffset(int seconds) {
inComittedTransaction(session -> {
// move time on Hot Rod server if present
HotRodConnectionProvider hotRodConnectionProvider = session.getProvider(HotRodConnectionProvider.class);
if (hotRodConnectionProvider != null) {
RemoteCache<Object, Object> scriptCache = hotRodConnectionProvider.getRemoteCache(DefaultHotRodConnectionProviderFactory.SCRIPT_CACHE);
if (scriptCache != null) {
Map<String, Object> param = new HashMap<>();
param.put("timeService", seconds);
Object returnFromTask = scriptCache.execute("InfinispanTimeServiceTask", param);
LOG.info(returnFromTask);
}
}
Time.setOffset(seconds);
});
}
}

View file

@ -22,7 +22,6 @@ import org.jboss.logging.Logger;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.Version;
import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.migration.MigrationModel;
import org.keycloak.migration.ModelVersion;
@ -72,12 +71,12 @@ public class MigrationModelTest extends KeycloakModelTest {
Assert.assertEquals(currentVersion, m.getStoredVersion());
Assert.assertEquals(m.getResourcesTag(), l.get(0).getId());
Time.setOffset(-60000);
setTimeOffset(-60000);
session.getProvider(DeploymentStateProvider.class).getMigrationModel().setStoredVersion("6.0.0");
em.flush();
Time.setOffset(0);
setTimeOffset(0);
l = em.createQuery("select m from MigrationModelEntity m ORDER BY m.updatedTime DESC", MigrationModelEntity.class).getResultList();
Assert.assertEquals(2, l.size());

View file

@ -0,0 +1,82 @@
/*
* 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.testsuite.model;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.events.Event;
import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventType;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ProviderFactory;
import static org.junit.Assert.assertEquals;
@RequireProvider(EventStoreProvider.class)
public class TimeOffsetTest extends KeycloakModelTest {
private String realmId;
@Override
protected void createEnvironment(KeycloakSession s) {
RealmModel r = s.realms().createRealm("realm");
r.setDefaultRole(s.roles().addRealmRole(r, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + r.getName()));
r.setEventsExpiration(5);
realmId = r.getId();
}
@Override
protected void cleanEnvironment(KeycloakSession s) {
s.realms().removeRealm(realmId);
}
@Test
public void testOffset() {
withRealm(realmId, (session, realmModel) -> {
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
Event e = new Event();
e.setType(EventType.LOGIN);
e.setRealmId(realmId);
e.setTime(Time.currentTimeMillis());
provider.onEvent(e);
return null;
});
withRealm(realmId, (session, realmModel) -> {
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
assertEquals(1, provider.createQuery().realm(realmId).getResultStream().count());
setTimeOffset(5);
// legacy store requires explicit expiration of expired events
ProviderFactory<EventStoreProvider> providerFactory = session.getKeycloakSessionFactory().getProviderFactory(EventStoreProvider.class);
if ("jpa".equals(providerFactory.getId())) {
provider.clearExpiredEvents();
}
return null;
});
withRealm(realmId, (session, realmModel) -> {
EventStoreProvider provider = session.getProvider(EventStoreProvider.class);
assertEquals(0, provider.createQuery().realm(realmId).getResultStream().count());
return null;
});
}
}

View file

@ -17,7 +17,6 @@
package org.keycloak.testsuite.model.events;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.Time;
import org.keycloak.events.Event;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider;
@ -159,7 +158,7 @@ public class EventQueryTest extends KeycloakModelTest {
return null;
});
Time.setOffset(10);
setTimeOffset(10);
try {
withRealm(realmId, (session, realm) -> {
@ -173,7 +172,7 @@ public class EventQueryTest extends KeycloakModelTest {
return null;
});
} finally {
Time.setOffset(0);
setTimeOffset(0);
}

View file

@ -20,7 +20,6 @@ package org.keycloak.testsuite.model.session;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
@ -72,7 +71,7 @@ public class AuthenticationSessionTest extends KeycloakModelTest {
ClientModel client = realm.getClientByClientId("test-app");
return IntStream.range(0, 300)
.mapToObj(i -> {
Time.setOffset(i);
setTimeOffset(i);
return ras.createAuthenticationSession(client);
})
.map(AuthenticationSessionModel::getTabId)
@ -184,7 +183,7 @@ public class AuthenticationSessionTest extends KeycloakModelTest {
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSessionId.get());
Assert.assertNotNull(rootAuthSession);
Time.setOffset(1900);
setTimeOffset(1900);
return null;
});

View file

@ -18,7 +18,6 @@
package org.keycloak.testsuite.model.session;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -67,9 +66,8 @@ public class UserSessionExpirationTest extends KeycloakModelTest {
assertThat(withRealm(realmId, (session, realm) -> session.sessions().getUserSession(realm, uSId)), notNullValue());
Time.setOffset(5);
setTimeOffset(5);
assertThat(withRealm(realmId, (session, realm) -> session.sessions().getUserSession(realm, uSId)), nullValue());
}
}

View file

@ -432,7 +432,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
for (int i = 0; i < USER_SESSION_COUNT; i++) {
// Having different offsets for each session (to ensure that lastSessionRefresh is also different)
Time.setOffset(i);
setTimeOffset(i);
UserSessionModel userSession = session.sessions().createUserSession(realm, user, "user1", "127.0.0.1", "form", true, null, null);
createClientSession(session, realmId, realm.getClientByClientId("test-app"), userSession, "http://redirect", "state");
@ -464,6 +464,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
}
return null;
});
}
@Test
@ -495,7 +496,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
persister.updateLastSessionRefreshes(realm, lastSessionRefresh, Collections.singleton(userSession1[0].getId()), true);
// Increase time offset - 40 days
Time.setOffset(3456000);
setTimeOffset(3456000);
try {
// Run expiration thread
persister.removeExpired(realm);
@ -507,7 +508,7 @@ public class UserSessionPersisterProviderTest extends KeycloakModelTest {
} finally {
// Cleanup
Time.setOffset(0);
setTimeOffset(0);
session.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
}
});

View file

@ -20,7 +20,6 @@ import org.hamcrest.Matchers;
import org.infinispan.client.hotrod.RemoteCache;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@ -193,7 +192,7 @@ public class UserSessionProviderModelTest extends KeycloakModelTest {
clientSession.setTimestamp(1);
});
} else {
Time.setOffset(1000);
setTimeOffset(1000);
}
});
@ -211,7 +210,7 @@ public class UserSessionProviderModelTest extends KeycloakModelTest {
});
});
} finally {
Time.setOffset(0);
setTimeOffset(0);
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
if (timer != null && timerTaskCtx != null) {
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);

View file

@ -17,7 +17,6 @@
package org.keycloak.testsuite.model.session;
import org.hamcrest.Matchers;
import org.infinispan.Cache;
import org.junit.Assert;
import org.junit.Assume;
@ -51,9 +50,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -163,7 +160,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
// sessions are in persister too
Assert.assertEquals(3, persister.getUserSessionsCount(true));
Time.setOffset(300);
setTimeOffset(300);
log.infof("Set time offset to 300. Time is: %d", Time.currentTime());
// Set lastSessionRefresh to currentSession[0] to 0
@ -178,7 +175,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
int timeOffset = 1728000 + (i * 86400);
RealmModel realm = session.realms().getRealm(realmId);
Time.setOffset(timeOffset);
setTimeOffset(timeOffset);
log.infof("Set time offset to %d. Time is: %d", timeOffset, Time.currentTime());
UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId());
@ -192,7 +189,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
persister = session.getProvider(UserSessionPersisterProvider.class);
// Increase timeOffset - 40 days
Time.setOffset(3456000);
setTimeOffset(3456000);
log.infof("Set time offset to 3456000. Time is: %d", Time.currentTime());
// Expire and ensure that all sessions despite session0 were removed
@ -211,7 +208,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
Assert.assertEquals(1, persister.getUserSessionsCount(true));
// Expire everything and assert nothing found
Time.setOffset(7000000);
setTimeOffset(7000000);
persister.removeExpired(realm);
});
@ -228,7 +225,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
});
} finally {
Time.setOffset(0);
setTimeOffset(0);
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
if (timer != null) {
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);
@ -278,7 +275,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
persister = session.getProvider(UserSessionPersisterProvider.class);
// Expire everything except offline client sessions
Time.setOffset(7000000);
setTimeOffset(7000000);
persister.removeExpired(realm);
});
@ -300,7 +297,7 @@ public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
});
} finally {
Time.setOffset(0);
setTimeOffset(0);
kcSession.getKeycloakSessionFactory().publish(new ResetTimeOffsetEvent());
if (timer != null) {
timer.schedule(timerTaskCtx.getRunnable(), timerTaskCtx.getIntervalMillis(), PersisterLastSessionRefreshStoreFactory.DB_LSR_PERIODIC_TASK_NAME);

View file

@ -60,7 +60,6 @@ public class SingleUseObjectModelTest extends KeycloakModelTest {
@Override
public void cleanEnvironment(KeycloakSession s) {
Time.setOffset(0);
s.realms().removeRealm(realmId);
}
@ -103,7 +102,7 @@ public class SingleUseObjectModelTest extends KeycloakModelTest {
Assert.assertNotNull(notes);
Assert.assertEquals("bar", notes.get("foo"));
Time.setOffset(70);
setTimeOffset(70);
notes = singleUseObjectProvider.get(key.serializeKey());
Assert.assertNull(notes);
@ -154,7 +153,7 @@ public class SingleUseObjectModelTest extends KeycloakModelTest {
Map<String, String> actualNotes = singleUseStore.get(key);
assertThat(actualNotes, Matchers.anEmptyMap());
Time.setOffset(70);
setTimeOffset(70);
Assert.assertNull(singleUseStore.get(key));
});