8000 Unify expiration handling for SingleUseObjects by mhajas · Pull Request #12230 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@
import org.infinispan.query.dsl.Query;
import org.infinispan.query.dsl.QueryFactory;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
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.ExpirableEntity;
import org.keycloak.models.map.storage.ModelEntityUtil;
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;
Expand All @@ -48,6 +51,7 @@
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;

Expand All @@ -60,13 +64,15 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRo
protected final HotRodEntityDescriptor<E, V> storedEntityDescriptor;
private final Function<E, V> delegateProducer;
protected final DeepCloner cloner;
protected boolean isExpirableEntity;

public HotRodMapStorage(RemoteCache<K, E> remoteCache, StringKeyConverter<K> keyConverter, HotRodEntityDescriptor<E, V> storedEntityDescriptor, DeepCloner cloner) {
this.remoteCache = remoteCache;
this.keyConverter = keyConverter;
this.storedEntityDescriptor = storedEntityDescriptor;
this.cloner = cloner;
this.delegateProducer = storedEntityDescriptor.getHotRodDelegateProvider();
this.isExpirableEntity = ExpirableEntity.class.isAssignableFrom(ModelEntityUtil.getEntityType(storedEntityDescriptor.getModelTypeClass()));
}

@Override
Expand All @@ -86,7 +92,10 @@ public V create(V value) {
public V read(String key) {
Objects.requireNonNull(key, "Key must be non-null");
K k = keyConverter.fromStringSafe(key);
return delegateProducer.apply(remoteCache.get(k));

V v = delegateProducer.apply(remoteCache.get(k));
if (v == null || v.getHotRodEntity() == null) return null;
return isExpirableEntity && isExpired((ExpirableEntity) v, true) ? null : v;
}

@Override
Expand All @@ -109,12 +118,22 @@ private static String toOrderString(QueryParameters.OrderBy<?> orderBy) {
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(", "));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.common.util.Time;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.realm.MapRealmEntity;
Expand Down Expand Up @@ -64,6 +63,8 @@
import java.util.Set;
import java.util.stream.Collectors;

import static org.keycloak.models.map.common.ExpirationUtils.isExpired;

@GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.realm.MapRealmEntity",
inherits = "org.keycloak.models.map.storage.hotRod.realm.HotRodRealmEntity.AbstractHotRodRealmEntityDelegate"
Expand Down Expand Up @@ -424,8 +425,7 @@ public void removeExpiredClientInitialAccesses() {
}

private boolean checkIfExpired(MapClientInitialAccessEntity cia) {
return cia.getRemainingCount() < 1 ||
(cia.getExpiration() != null && cia.getExpiration() < Time.currentTimeMillis());
return cia.getRemainingCount() < 1 || isExpired(cia, true);
}
}
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,6 @@ public JpaRootAuthenticationSessionModelCriteriaBuilder compare(SearchableModelF
throw new CriterionNotSupportedException(modelField, op);
}

case LT:
if (modelField == SearchableFields.EXPIRATION) {
validateValue(value, modelField, op, Number.class);

Number expiration = (Number) value[0];
return new JpaRootAuthenticationSessionModelCriteriaBuilder((cb, root) ->
cb.lt(root.get(modelField.getName()), expiration)
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.utils.SessionExpiration;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.RootAuthenticationSessionModel;
Expand All @@ -41,6 +40,7 @@
import java.util.function.Predicate;

import static org.keycloak.common.util.StackUtil.getShortStackTrace;
import static org.keycloak.models.map.common.ExpirationUtils.isExpired;
import static org.keycloak.models.map.storage.QueryParameters.withCriteria;
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
import static org.keycloak.models.utils.SessionExpiration.getAuthSessionLifespan;
Expand All @@ -65,13 +65,11 @@ public MapRootAuthenticationSessionProvider(KeycloakSession session, MapStorage<

private Function<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> entityToAdapterFunc(RealmModel realm) {
return origEntity -> {
//return new MapRootAuthenticationSessionAdapter(session, realm, origEntity);
Long expiration = origEntity.getExpiration();
if (expiration == null || Time.currentTimeMillis() < origEntity.getExpiration()) {
return new MapRootAuthenticationSessionAdapter(session, realm, origEntity);
} else {
if (isExpired(origEntity, true)) {
tx.delete(origEntity.getId());
return null;
} else {
return new MapRootAuthenticationSessionAdapter(session, realm, origEntity);
}
};
}
Expand Down Expand Up @@ -137,21 +135,14 @@ public void removeRootAuthenticationSession(RealmModel realm, RootAuthentication

@Override
public void removeAllExpired() {
session.realms().getRealmsStream().forEach(this::removeExpired);
LOG.tracef("removeAllExpired()%s", getShortStackTrace());
LOG.warnf("Clearing expired entities should not be triggered manually. It is responsibility of the store to clear these.");
}

@Override
public void removeExpired(RealmModel realm) {
Objects.requireNonNull(realm, "The provided realm can't be null!");
LOG.debugf("Removing expired sessions");

DefaultModelCriteria<RootAuthenticationSessionModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId())
.compare(SearchableFields.EXPIRATION, Operator.LT, Time.currentTimeMillis());

long deletedCount = tx.delete(withCriteria(mcb));

LOG.debugf("Removed %d expired authentication sessions for realm '%s'", deletedCount, realm.getName());
LOG.tracef("removeExpired(%s)%s", realm, getShortStackTrace());
LOG.warnf("Clearing expired entities should not be triggered manually. It is responsibility of the store to clear these.");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public interface ExpirableEntity extends AbstractEntity {
/**
* Returns a point in the time (timestamp in milliseconds since The Epoch) when this entity expires.
*
* @return a timestamp in milliseconds since The Epoch or {@code null} if this entity never expires.
* @return a timestamp in milliseconds since The Epoch or {@code null} if this entity never expires
* or expiration is not known.
*/
Long getExpiration();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.common;

import org.keycloak.common.util.Time;

public class ExpirationUtils {

/**
* Checks whether the {@code entity} is expired
*
* @param entity to check
* @param allowInfiniteValues sets how null values are interpreted, if true entity with expiration equal
* to {@code null} is interpreted as never expiring entity, if false entities
* with {@code null} expiration are interpreted as expired entities
* @return true if the {@code entity} is expired (expiration time is in the past or now), false otherwise
*/
public static boolean isExpired(ExpirableEntity entity, boolean allowInfiniteValues) {
Long expiration = entity.getExpiration();
if (!allowInfiniteValues && expiration == null) return false;
return expiration != null && expiration <= Time.currentTimeMillis();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before, when expiration was not set, some providers used 0 as a default which meant the entity is expired. Now it is the exact opposite. Is this on purpose?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. However, the behaviour you described conflicts with Javadoc here. We should discuss this, I am adding discussion point to the meeting today.

}

public static boolean isNotExpired(Object entity) {
return !isExpired((ExpirableEntity) entity, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,6 @@ public AdminEventQuery maxResults(int max) {

@Override
public Stream<AdminEvent> getResultStream() {
// Add expiration condition to not load expired events
mcb = mcb.and(
criteria.or(
criteria.compare(AdminEvent.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.NOT_EXISTS),
criteria.compare(AdminEvent.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.GT,
Time.currentTimeMillis())
));

return resultProducer.apply(QueryParameters.withCriteria(mcb)
.offset(firstResult)
.limit(maxResults)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,6 @@ public EventQuery maxResults(int max) {

@Override
public Stream<Event> getResultStream() {
// Add expiration condition to not load expired events
mcb = mcb.and(
criteria.or(
criteria.compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.NOT_EXISTS),
criteria.compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.GT,
Time.currentTimeMillis())
));

return resultProducer.apply(QueryParameters.withCriteria(mcb)
.offset(firstResult)
.limit(maxResults)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.stream.Stream;

import static org.keycloak.common.util.StackUtil.getShortStackTrace;
import static org.keycloak.models.map.common.ExpirationUtils.isExpired;
import static org.keycloak.models.map.events.EventUtils.modelToEntity;

public class MapEventStoreProvider implements EventStoreProvider {
Expand Down Expand Up @@ -79,9 +80,8 @@ public void onEvent(Event event) {
}

private boolean filterExpired(ExpirableEntity event) {
Long expiration = event.getExpiration();
// Check if entity is expired
if (expiration != null && expiration <= Time.currentTimeMillis()) {
if (isExpired(event, true)) {
// Remove entity
authEventsTX.delete(event.getId());

Expand Down Expand Up @@ -123,14 +123,7 @@ public void clear(RealmModel realm, long olderThan) {
@Override
public void clearExpiredEvents() {
LOG.tracef("clearExpiredEvents()%s", getShortStackTrace());
Comment thread
hmlnarik marked this conversation as resolved.
Resolved

authEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.<Event>criteria()
.compare(Event.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.LE,
Time.currentTimeMillis())));

adminEventsTX.delete(QueryParameters.withCriteria(DefaultModelCriteria.<AdminEvent>criteria()
.compare(AdminEvent.SearchableFields.EXPIRATION, ModelCriteriaBuilder.Operator.LE,
Time.currentTimeMillis())));
LOG.warnf("Clearing expired entities should not be triggered manually. It is responsibility of the store to clear these.");
}

/** ADMIN EVENTS **/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

package org.keycloak.models.map.realm;

import org.keycloak.common.util.Time;
import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator;
import org.keycloak.models.map.common.AbstractEntity;
Expand All @@ -43,6 +42,8 @@
import java.util.Set;
import java.util.stream.Collectors;

import static org.keycloak.models.map.common.ExpirationUtils.isExpired;

@GenerateEntityImplementations(
inherits = "org.keycloak.models.map.realm.MapRealmEntity.AbstractRealmEntity"
)
Expand Down Expand Up @@ -237,8 +238,7 @@ public boolean hasClientInitialAccess() {
}

private boolean checkIfExpired(MapClientInitialAccessEntity cia) {
return cia.getRemainingCount() < 1 ||
(cia.getExpiration() != null && cia.getExpiration() < Time.currentTimeMillis());
return cia.getRemainingCount() < 1 || isExpired(cia, true);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public String getActionId() {
@Override
public int getExpiration() {
Long expiration = entity.getExpiration();
return expiration != null ? TimeAdapter.fromLongWithTimeInSecondsToIntegerWithTimeInSeconds(expiration) : 0;
return expiration != null ? TimeAdapter.fromLongWithTimeInSecondsToIntegerWithTimeInSeconds(TimeAdapter.fromMilliSecondsToSeconds(expiration)) : 0;
}

@Override
Expand Down
Loading
0