Optimize user-client session relationship for HotRod storage

Closes #12818
This commit is contained in:
Michal Hajas 2022-07-11 08:54:39 +02:00 committed by Hynek Mlnařík
parent 9a89560771
commit eb1f31e9dd
19 changed files with 799 additions and 40 deletions

View file

@ -89,7 +89,6 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
}
}
private class HotRodGettersAndSettersDelegateGenerator implements Generator {
private static final String ENTITY_VARIABLE = "hotRodEntity";
@ -643,11 +642,13 @@ public class GenerateHotRodEntityImplementationsProcessor extends AbstractGenera
" }");
// cache name
boolean isMethodCall = hotRodAnnotation.cacheName().contains("(");
String quotes = isMethodCall ? "" : "\"";
pw.println(" @Override\n" +
" public String getCacheName() {\n" +
(hotRodAnnotation.cacheName().isEmpty() ?
" return org.keycloak.models.map.storage.ModelEntityUtil.getModelName(" + hotRodAnnotation.modelClass() + ".class);\n"
: " return \"" + hotRodAnnotation.cacheName() + "\";\n") +
: " return " + quotes + hotRodAnnotation.cacheName() + quotes + ";\n") +
" }");
// delegate provider

View file

@ -55,7 +55,7 @@ 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;
public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends AbstractEntity & HotRodEntityDelegate<E>, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class);
@ -223,10 +223,14 @@ public class HotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRo
MapKeycloakTransaction<V, M> sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
if (sessionTransaction == null) {
Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, V, M>> fieldPredicates = MapFieldPredicates.getPredicates((Class<M>) storedEntityDescriptor.getModelTypeClass());
sessionTransaction = new ConcurrentHashMapKeycloakTransaction<>(this, keyConverter, cloner, fieldPredicates);
sessionTransaction = createTransactionInternal(session);
session.setAttribute("map-transaction-" + hashCode(), sessionTransaction);
}
return sessionTransaction;
}
protected MapKeycloakTransaction<V, M> createTransactionInternal(KeycloakSession session) {
Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, V, M>> fieldPredicates = MapFieldPredicates.getPredicates((Class<M>) storedEntityDescriptor.getModelTypeClass());
return new ConcurrentHashMapKeycloakTransaction<>(this, keyConverter, cloner, fieldPredicates);
}
}

View file

@ -54,6 +54,10 @@ 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.chm.MapFieldPredicates;
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
import org.keycloak.models.map.storage.hotRod.authSession.HotRodAuthenticationSessionEntityDelegate;
import org.keycloak.models.map.storage.hotRod.authSession.HotRodRootAuthenticationSessionEntityDelegate;
import org.keycloak.models.map.storage.hotRod.authorization.HotRodPermissionTicketEntityDelegate;
@ -96,8 +100,10 @@ import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntityDelega
import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEntityDelegate;
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;
@ -105,6 +111,7 @@ import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntity;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -114,6 +121,7 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
public static final String PROVIDER_ID = "hotrod";
private final Map<Class<?>, HotRodMapStorage> storages = new ConcurrentHashMap<>();
private static final Map<SearchableModelField<AuthenticatedClientSessionModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<Object, AbstractEntity, AuthenticatedClientSessionModel>> clientSessionPredicates = MapFieldPredicates.basePredicates(HotRodAuthenticatedClientSessionEntity.ID);
private final static DeepCloner CLONER = new DeepCloner.Builder()
.constructor(MapRootAuthenticationSessionEntity.class, HotRodRootAuthenticationSessionEntityDelegate::new)
@ -174,6 +182,9 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
}
public <E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M> HotRodMapStorage<String, E, V, M> getHotRodStorage(KeycloakSession session, Class<M> modelType, 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, flags);
return storages.computeIfAbsent(modelType, c -> createHotRodStorage(session, modelType, flags));
}
@ -183,6 +194,28 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
if (modelType == ActionTokenValueModel.class) {
return new SingleUseObjectHotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER);
} if (modelType == AuthenticatedClientSessionModel.class) {
return new HotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()),
StringKeyConverter.StringKey.INSTANCE,
entityDescriptor,
CLONER) {
@Override
protected MapKeycloakTransaction createTransactionInternal(KeycloakSession session) {
return new ConcurrentHashMapKeycloakTransaction(this, keyConverter, cloner, clientSessionPredicates);
}
};
} if (modelType == UserSessionModel.class) {
HotRodMapStorage clientSessionStore = getHotRodStorage(session, AuthenticatedClientSessionModel.class);
return new HotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()),
StringKeyConverter.StringKey.INSTANCE,
entityDescriptor,
CLONER) {
@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<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER);
}

View file

@ -61,17 +61,10 @@ public class SingleUseObjectHotRodMapStorage<K, E extends AbstractHotRodEntity,
}
@Override
public MapKeycloakTransaction<HotRodSingleUseObjectEntityDelegate, ActionTokenValueModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<HotRodSingleUseObjectEntityDelegate, ActionTokenValueModel> transaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
if (transaction == null) {
Map<SearchableModelField<? super ActionTokenValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, HotRodSingleUseObjectEntityDelegate, ActionTokenValueModel>> fieldPredicates =
MapFieldPredicates.getPredicates((Class<ActionTokenValueModel>) storedEntityDescriptor.getModelTypeClass());
transaction = new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates);
session.setAttribute("map-transaction-" + hashCode(), transaction);
}
return transaction;
protected MapKeycloakTransaction<HotRodSingleUseObjectEntityDelegate, ActionTokenValueModel> createTransactionInternal(KeycloakSession session) {
Map<SearchableModelField<? super ActionTokenValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, HotRodSingleUseObjectEntityDelegate, ActionTokenValueModel>> fieldPredicates =
MapFieldPredicates.getPredicates((Class<ActionTokenValueModel>) storedEntityDescriptor.getModelTypeClass());
return new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates);
}
@Override

View file

@ -18,10 +18,14 @@
package org.keycloak.models.map.storage.hotRod.common;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.hotRod.authSession.HotRodAuthenticationSessionEntity;
import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodLocalizationTexts;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEntity;
import org.keycloak.models.map.storage.hotRod.userSession.AuthenticatedClientSessionReferenceOnlyFieldDelegate;
import org.keycloak.models.map.storage.hotRod.userSession.HotRodAuthenticatedClientSessionEntityReference;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import java.util.HashMap;
import java.util.List;
@ -145,4 +149,12 @@ public class HotRodTypesUtils {
return hotRodLocalizationTexts;
}
public static HotRodAuthenticatedClientSessionEntityReference migrateMapAuthenticatedClientSessionEntityToHotRodAuthenticatedClientSessionEntityReference(MapAuthenticatedClientSessionEntity p0) {
return new HotRodAuthenticatedClientSessionEntityReference(p0.getClientId(), p0.getId());
}
public static MapAuthenticatedClientSessionEntity migrateHotRodAuthenticatedClientSessionEntityReferenceToMapAuthenticatedClientSessionEntity(HotRodAuthenticatedClientSessionEntityReference collectionItem) {
return DeepCloner.DUMB_CLONER.entityFieldDelegate(MapAuthenticatedClientSessionEntity.class, new AuthenticatedClientSessionReferenceOnlyFieldDelegate(collectionItem));
}
}

View file

@ -0,0 +1,47 @@
/*
* 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.userSession;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.EntityFieldDelegate;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntityFields;
public class AuthenticatedClientSessionReferenceOnlyFieldDelegate implements EntityFieldDelegate<MapAuthenticatedClientSessionEntity> {
private final HotRodAuthenticatedClientSessionEntityReference reference;
public AuthenticatedClientSessionReferenceOnlyFieldDelegate(HotRodAuthenticatedClientSessionEntityReference reference) {
this.reference = reference;
}
@Override
public boolean isUpdated() {
return false;
}
@Override
public <EF extends Enum<? extends EntityField<MapAuthenticatedClientSessionEntity>> & EntityField<MapAuthenticatedClientSessionEntity>> Object get(EF field) {
switch ((MapAuthenticatedClientSessionEntityFields)field) {
case ID: return reference.getClientSessionId();
case CLIENT_ID: return reference.getClientId();
}
return null;
}
}

View file

@ -21,59 +21,106 @@ import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator;
import org.keycloak.models.map.storage.hotRod.authorization.HotRodResourceServerEntity;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.models.map.storage.hotRod.common.CommonPrimitivesProtoSchemaInitializer;
import org.keycloak.models.map.storage.hotRod.common.HotRodPair;
import org.keycloak.models.map.storage.hotRod.common.UpdatableHotRodEntityDelegateImpl;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.storage.SearchableModelField;
import java.util.Set;
@GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity"
implementInterface = "org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity",
inherits = "org.keycloak.models.map.storage.hotRod.userSession.HotRodAuthenticatedClientSessionEntity.AbstractHotRodAuthenticatedClientSessionEntityDelegate",
topLevelEntity = true,
modelClass = "org.keycloak.models.AuthenticatedClientSessionModel",
cacheName = "org.keycloak.models.map.storage.ModelEntityUtil.getModelName(org.keycloak.models.UserSessionModel.class)" // Use the same cache name as user-sessions
)
@ProtoDoc("schema-version: " + HotRodResourceServerEntity.VERSION)
@ProtoDoc("@Indexed")
public class HotRodAuthenticatedClientSessionEntity extends AbstractHotRodEntity {
@ProtoField(number = 1)
public String id;
@IgnoreForEntityImplementationGenerator
public static final int VERSION = 1;
@ProtoField(number = 2)
public String realmId;
@IgnoreForEntityImplementationGenerator
public static final SearchableModelField<AuthenticatedClientSessionModel> ID = new SearchableModelField<>("id", String.class);
@AutoProtoSchemaBuilder(
includeClasses = {
HotRodAuthenticatedClientSessionEntity.class
},
schemaFilePath = "proto/",
schemaPackageName = CommonPrimitivesProtoSchemaInitializer.HOT_ROD_ENTITY_PACKAGE,
dependsOn = {CommonPrimitivesProtoSchemaInitializer.class}
)
public interface HotRodAuthenticatedClientSessionEntitySchema extends GeneratedSchema {
HotRodAuthenticatedClientSessionEntitySchema INSTANCE = new HotRodAuthenticatedClientSessionEntitySchemaImpl();
}
@ProtoField(number = 1)
public Integer entityVersion = VERSION;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 2)
public String id;
@ProtoField(number = 3)
public String clientId;
public String realmId;
@ProtoField(number = 4)
public String authMethod;
public String clientId;
@ProtoField(number = 5)
public String redirectUri;
public String authMethod;
@ProtoField(number = 6)
public Long timestamp;
public String redirectUri;
@ProtoField(number = 7)
public Long expiration;
public Long timestamp;
@ProtoField(number = 8)
public String action;
public Long expiration;
@ProtoField(number = 9)
public Set<HotRodPair<String, String>> notes;
public String action;
@ProtoField(number = 10)
public String currentRefreshToken;
public Set<HotRodPair<String, String>> notes;
@ProtoField(number = 11)
public Integer currentRefreshTokenUseCount;
public String currentRefreshToken;
@ProtoField(number = 12)
public Integer currentRefreshTokenUseCount;
@ProtoField(number = 13)
public Boolean offline;
public static abstract class AbstractHotRodAuthenticatedClientSessionEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodAuthenticatedClientSessionEntity> implements MapAuthenticatedClientSessionEntity {
@Override
public void setId(String id) {
HotRodAuthenticatedClientSessionEntity entity = getHotRodEntity();
if (entity.id != null) throw new IllegalStateException("Id cannot be changed");
entity.id = id;
entity.updated |= id != null;
}
@Override
public void setClientId(String clientId) {
HotRodAuthenticatedClientSessionEntity entity = getHotRodEntity();
if (entity.clientId != null) throw new IllegalStateException("ClientId cannot be changed");
entity.clientId = clientId;
entity.updated |= clientId != null;
}
}
@Override
public boolean equals(Object o) {
return HotRodAuthenticatedClientSessionEntityDelegate.entityEquals(this, o);

View file

@ -0,0 +1,56 @@
/*
* 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.userSession;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntityFields;
public abstract class HotRodAuthenticatedClientSessionEntityDelegateProvider implements DelegateProvider<MapAuthenticatedClientSessionEntity> {
private MapAuthenticatedClientSessionEntity fullClientSessionData;
private MapAuthenticatedClientSessionEntity idClientIdReferenceOnly;
public HotRodAuthenticatedClientSessionEntityDelegateProvider(MapAuthenticatedClientSessionEntity idClientIdReferenceOnly) {
this.idClientIdReferenceOnly = idClientIdReferenceOnly;
}
@Override
public MapAuthenticatedClientSessionEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapAuthenticatedClientSessionEntity>> field, Object... parameters) {
if (fullClientSessionData != null) return fullClientSessionData;
if (isRead) {
switch ((MapAuthenticatedClientSessionEntityFields) field) {
case ID:
case CLIENT_ID:
return idClientIdReferenceOnly;
}
}
fullClientSessionData = loadClientSessionFromDatabase();
if (fullClientSessionData == null) {
throw new ModelIllegalStateException("Unable to retrieve client session data with id: " + idClientIdReferenceOnly.getId());
}
return fullClientSessionData;
}
public abstract MapAuthenticatedClientSessionEntity loadClientSessionFromDatabase();
}

View file

@ -0,0 +1,47 @@
/*
* 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.userSession;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
@ProtoDoc("@Indexed")
public class HotRodAuthenticatedClientSessionEntityReference {
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 1)
public String clientId;
@ProtoField(number = 2)
public String clientSessionId;
public HotRodAuthenticatedClientSessionEntityReference() {}
public HotRodAuthenticatedClientSessionEntityReference(String clientId, String clientSessionId) {
this.clientId = clientId;
this.clientSessionId = clientSessionId;
}
public String getClientId() {
return clientId;
}
public String getClientSessionId() {
return clientSessionId;
}
}

View file

@ -24,14 +24,20 @@ import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.annotations.IgnoreForEntityImplementationGenerator;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.storage.hotRod.authorization.HotRodResourceServerEntity;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.models.map.storage.hotRod.common.CommonPrimitivesProtoSchemaInitializer;
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.HotRodStringPair;
import org.keycloak.models.map.storage.hotRod.common.HotRodTypesUtils;
import org.keycloak.models.map.storage.hotRod.common.UpdatableHotRodEntityDelegateImpl;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntityDelegate;
import java.util.Collections;
import java.util.Objects;
@ -54,7 +60,7 @@ public class HotRodUserSessionEntity extends AbstractHotRodEntity {
@AutoProtoSchemaBuilder(
includeClasses = {
HotRodUserSessionEntity.class,
HotRodAuthenticatedClientSessionEntity.class,
HotRodAuthenticatedClientSessionEntityReference.class,
},
schemaFilePath = "proto/",
schemaPackageName = CommonPrimitivesProtoSchemaInitializer.HOT_ROD_ENTITY_PACKAGE,
@ -119,7 +125,7 @@ public class HotRodUserSessionEntity extends AbstractHotRodEntity {
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 16)
public Set<HotRodAuthenticatedClientSessionEntity> authenticatedClientSessions;
public Set<HotRodAuthenticatedClientSessionEntityReference> authenticatedClientSessions;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 17)
@ -166,15 +172,18 @@ public class HotRodUserSessionEntity extends AbstractHotRodEntity {
@Override
public Optional<MapAuthenticatedClientSessionEntity> getAuthenticatedClientSession(String clientUUID) {
Set<HotRodAuthenticatedClientSessionEntity> acss = getHotRodEntity().authenticatedClientSessions;
Set<HotRodAuthenticatedClientSessionEntityReference> acss = getHotRodEntity().authenticatedClientSessions;
if (acss == null || acss.isEmpty()) return Optional.empty();
return acss.stream().filter(acs -> Objects.equals(acs.clientId, clientUUID)).findFirst().map(HotRodAuthenticatedClientSessionEntityDelegate::new);
return acss.stream()
.filter(acs -> Objects.equals(acs.clientId, clientUUID))
.findFirst()
.map(HotRodTypesUtils::migrateHotRodAuthenticatedClientSessionEntityReferenceToMapAuthenticatedClientSessionEntity);
}
@Override
public Boolean removeAuthenticatedClientSession(String clientUUID) {
Set<HotRodAuthenticatedClientSessionEntity> acss = getHotRodEntity().authenticatedClientSessions;
Set<HotRodAuthenticatedClientSessionEntityReference> acss = getHotRodEntity().authenticatedClientSessions;
boolean removed = acss != null && acss.removeIf(uc -> Objects.equals(uc.clientId, clientUUID));
getHotRodEntity().updated |= removed;
return removed;

View file

@ -0,0 +1,165 @@
/*
* 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.userSession;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.UserSessionModel;
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.chm.MapModelCriteriaBuilder;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntityDelegate;
import org.keycloak.models.map.userSession.MapUserSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntityDelegate;
import org.keycloak.storage.SearchableModelField;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator.IN;
public class HotRodUserSessionTransaction<K> extends ConcurrentHashMapKeycloakTransaction<K, MapUserSessionEntity, UserSessionModel> {
private final MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTransaction;
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
) {
super(map, keyConverter, cloner, fieldPredicates);
this.clientSessionTransaction = clientSessionTransaction;
}
@Override
public void commit() {
super.commit();
clientSessionTransaction.commit();
}
private MapAuthenticatedClientSessionEntity wrapClientSessionEntityToClientSessionAwareDelegate(MapAuthenticatedClientSessionEntity d) {
return new MapAuthenticatedClientSessionEntityDelegate(new HotRodAuthenticatedClientSessionEntityDelegateProvider(d) {
@Override
public MapAuthenticatedClientSessionEntity loadClientSessionFromDatabase() {
return clientSessionTransaction.read(d.getId());
}
});
}
private MapUserSessionEntity wrapUserSessionEntityToClientSessionAwareDelegate(MapUserSessionEntity entity) {
if (entity == null) return null;
return new MapUserSessionEntityDelegate(new SimpleDelegateProvider<>(entity)) {
@Override
public Set<MapAuthenticatedClientSessionEntity> getAuthenticatedClientSessions() {
Set<MapAuthenticatedClientSessionEntity> clientSessions = super.getAuthenticatedClientSessions();
return clientSessions == null ? null : clientSessions.stream()
.map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate)
.collect(Collectors.toSet());
}
@Override
public Optional<MapAuthenticatedClientSessionEntity> getAuthenticatedClientSession(String clientUUID) {
return super.getAuthenticatedClientSession(clientUUID)
.map(HotRodUserSessionTransaction.this::wrapClientSessionEntityToClientSessionAwareDelegate);
}
@Override
public void addAuthenticatedClientSession(MapAuthenticatedClientSessionEntity clientSession) {
super.addAuthenticatedClientSession(clientSession);
clientSessionTransaction.create(clientSession);
}
@Override
public Boolean removeAuthenticatedClientSession(String clientUUID) {
Optional<MapAuthenticatedClientSessionEntity> clientSession = getAuthenticatedClientSession(clientUUID);
if (!clientSession.isPresent()) {
return false;
}
return super.removeAuthenticatedClientSession(clientUUID) && clientSessionTransaction.delete(clientSession.get().getId());
}
@Override
public void clearAuthenticatedClientSessions() {
Set<MapAuthenticatedClientSessionEntity> clientSessions = super.getAuthenticatedClientSessions();
if (clientSessions != null) {
clientSessionTransaction.delete(QueryParameters.withCriteria(
DefaultModelCriteria.<AuthenticatedClientSessionModel>criteria()
.compare(HotRodAuthenticatedClientSessionEntity.ID, IN, clientSessions.stream()
.map(MapAuthenticatedClientSessionEntity::getId))
));
}
super.clearAuthenticatedClientSessions();
}
};
}
@Override
public MapUserSessionEntity read(String sKey) {
return wrapUserSessionEntityToClientSessionAwareDelegate(super.read(sKey));
}
@Override
public Stream<MapUserSessionEntity> read(QueryParameters<UserSessionModel> queryParameters) {
return super.read(queryParameters).map(this::wrapUserSessionEntityToClientSessionAwareDelegate);
}
@Override
public MapUserSessionEntity create(MapUserSessionEntity value) {
return wrapUserSessionEntityToClientSessionAwareDelegate(super.create(value));
}
@Override
public boolean delete(String key) {
MapUserSessionEntity uSession = read(key);
Set<MapAuthenticatedClientSessionEntity> clientSessions = uSession.getAuthenticatedClientSessions();
if (clientSessions != null) {
clientSessionTransaction.delete(QueryParameters.withCriteria(
DefaultModelCriteria.<AuthenticatedClientSessionModel>criteria()
.compare(HotRodAuthenticatedClientSessionEntity.ID, IN, clientSessions.stream()
.map(MapAuthenticatedClientSessionEntity::getId))
));
}
return super.delete(key);
}
@Override
public long delete(QueryParameters<UserSessionModel> queryParameters) {
clientSessionTransaction.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)))
));
return super.delete(queryParameters);
}
}

View file

@ -75,8 +75,9 @@
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntityReference</indexed-entity>
<indexed-entity>kc.HotRodStringPair</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>

View file

@ -77,8 +77,9 @@
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntityReference</indexed-entity>
<indexed-entity>kc.HotRodStringPair</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>

View file

@ -23,6 +23,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.ActionTokenValueModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
@ -48,6 +49,7 @@ import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity;
import org.keycloak.models.map.realm.MapRealmEntity;
import org.keycloak.models.map.role.MapRoleEntity;
import org.keycloak.models.map.user.MapUserEntity;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntity;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import java.util.HashMap;
@ -100,6 +102,7 @@ public class ModelEntityUtil {
MODEL_TO_ENTITY_TYPE.put(UserLoginFailureModel.class, MapUserLoginFailureEntity.class);
MODEL_TO_ENTITY_TYPE.put(UserModel.class, MapUserEntity.class);
MODEL_TO_ENTITY_TYPE.put(UserSessionModel.class, MapUserSessionEntity.class);
MODEL_TO_ENTITY_TYPE.put(AuthenticatedClientSessionModel.class, MapAuthenticatedClientSessionEntity.class);
// authz
MODEL_TO_ENTITY_TYPE.put(PermissionTicket.class, MapPermissionTicketEntity.class);

View file

@ -535,7 +535,7 @@ public class MapFieldPredicates {
return mcb.fieldCompare(Boolean.TRUE::equals, getter);
}
protected static <K, V extends AbstractEntity, M> Map<SearchableModelField<M>, UpdatePredicatesFunc<K, V, M>> basePredicates(SearchableModelField<M> idField) {
public static <K, V extends AbstractEntity, M> Map<SearchableModelField<M>, UpdatePredicatesFunc<K, V, M>> basePredicates(SearchableModelField<M> idField) {
Map<SearchableModelField<M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates = new HashMap<>();
fieldPredicates.put(idField, MapModelCriteriaBuilder::idCompare);
return fieldPredicates;

View file

@ -20,6 +20,7 @@ import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -144,7 +145,12 @@ public class MapUserSessionAdapter extends AbstractUserSessionModel {
}
public boolean filterAndRemoveExpiredClientSessions(MapAuthenticatedClientSessionEntity clientSession) {
if (isExpired(clientSession, false)) {
try {
if (isExpired(clientSession, false)) {
entity.removeAuthenticatedClientSession(clientSession.getClientId());
return false;
}
} catch (ModelIllegalStateException ex) {
entity.removeAuthenticatedClientSession(clientSession.getClientId());
return false;
}

View file

@ -73,8 +73,9 @@
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodUserSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntityReference</indexed-entity>
<indexed-entity>kc.HotRodStringPair</indexed-entity>
<indexed-entity>kc.HotRodAuthenticatedClientSessionEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>

View file

@ -0,0 +1,143 @@
/*
* 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.testsuite.model.session;
import org.infinispan.client.hotrod.RemoteCache;
import org.junit.Test;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.map.storage.ModelEntityUtil;
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.userSession.HotRodUserSessionEntity;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.containsInAnyOrder;
@RequireProvider(UserSessionProvider.class)
@RequireProvider(value = HotRodConnectionProvider.class, only = DefaultHotRodConnectionProviderFactory.PROVIDER_ID)
public class HotRodUserSessionClientSessionRelationshipTest extends KeycloakModelTest {
private String realmId;
private String CLIENT0_CLIENT_ID = "client0";
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
realm.setSsoSessionIdleTimeout(1800);
realm.setSsoSessionMaxLifespan(36000);
this.realmId = realm.getId();
s.clients().addClient(realm, CLIENT0_CLIENT_ID);
s.users().addUser(realm, "user1").setEmail("user1@localhost");
}
@Override
public void cleanEnvironment(KeycloakSession s) {
if (realmId != null) {
s.realms().removeRealm(realmId);
}
}
@Test
public void testClientSessionAreRemovedOnUserSessionRemoval() {
AtomicReference<String> uSessionId = new AtomicReference<>();
AtomicReference<String> cSessionId = new AtomicReference<>();
prepareSessions(uSessionId, cSessionId);
withRealm(realmId, (session, realm) -> {
UserSessionModel uSession = session.sessions().getUserSession(realm, uSessionId.get());
session.sessions().removeUserSession(realm, uSession);
return null;
});
assertCacheContains(remoteCache -> assertThat(remoteCache, anEmptyMap()));
}
@Test
public void testSessionsAreRemovedOnUserRemoval() {
AtomicReference<String> uSessionId = new AtomicReference<>();
AtomicReference<String> cSessionId = new AtomicReference<>();
prepareSessions(uSessionId, cSessionId);
withRealm(realmId, (session, realm) -> {
session.users().removeUser(realm, session.users().getUserByUsername(realm, "user1"));
return null;
});
assertCacheContains(remoteCache -> {
assertThat(remoteCache, anEmptyMap());
});
}
@Test
public void testSessionsAreRemovedOnRealmRemoval() {
AtomicReference<String> uSessionId = new AtomicReference<>();
AtomicReference<String> cSessionId = new AtomicReference<>();
prepareSessions(uSessionId, cSessionId);
withRealm(realmId, (session, realm) -> {
session.realms().removeRealm(realm.getId());
return null;
});
assertCacheContains(remoteCache -> {
assertThat(remoteCache, anEmptyMap());
});
}
private void assertCacheContains(Consumer<RemoteCache<String, HotRodUserSessionEntity>> checker) {
withRealm(realmId, (session, realm) -> {
HotRodConnectionProvider provider = session.getProvider(HotRodConnectionProvider.class);
RemoteCache<String, HotRodUserSessionEntity> remoteCache = provider.getRemoteCache(ModelEntityUtil.getModelName(UserSessionModel.class));
checker.accept(remoteCache);
return null;
});
}
private void prepareSessions(AtomicReference<String> uSessionId, AtomicReference<String> cSessionId) {
withRealm(realmId, (session, realm) -> {
UserSessionModel uSession = session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null);
ClientModel client = realm.getClientByClientId(CLIENT0_CLIENT_ID);
AuthenticatedClientSessionModel cSession = session.sessions().createClientSession(realm, client, uSession);
uSessionId.set(uSession.getId());
cSessionId.set(cSession.getId());
return null;
});
assertCacheContains(remoteCache -> {
assertThat(remoteCache, aMapWithSize(2));
assertThat(remoteCache.keySet(), containsInAnyOrder(uSessionId.get(), cSessionId.get()));
});
}
}

View file

@ -0,0 +1,190 @@
/*
* 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.testsuite.model.session;
import org.junit.Test;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.map.storage.ModelEntityUtil;
import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.testsuite.model.KeycloakModelTest;
import org.keycloak.testsuite.model.RequireProvider;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.stream.IntStream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.startsWith;
import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;
@RequireProvider(UserSessionProvider.class)
public class UserSessionConcurrencyTest extends KeycloakModelTest {
private String realmId;
private static final int CLIENTS_COUNT = 10;
private static final Lock SYNC_USESSION = new ReentrantLock();
private static final ThreadLocal<Boolean> wasWriting = ThreadLocal.withInitial(() -> false);
private final boolean isHotRodStore = HotRodMapStorageProviderFactory.PROVIDER_ID.equals(CONFIG.getConfig().get("userSessions.map.storage.provider"));
@Override
public void createEnvironment(KeycloakSession s) {
RealmModel realm = s.realms().createRealm("test");
realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName()));
realm.setSsoSessionIdleTimeout(1800);
realm.setSsoSessionMaxLifespan(36000);
realm.setClientSessionIdleTimeout(500);
this.realmId = realm.getId();
s.users().addUser(realm, "user1").setEmail("user1@localhost");
s.users().addUser(realm, "user2").setEmail("user2@localhost");
for (int i = 0; i < CLIENTS_COUNT; i++) {
s.clients().addClient(realm, "client" + i);
}
}
@Override
protected boolean isUseSameKeycloakSessionFactoryForAllThreads() {
return true;
}
@Test
public void testConcurrentNotesChange() {
// Create user session
String uId = withRealm(this.realmId, (session, realm) -> session.sessions().createUserSession(realm, session.users().getUserByUsername(realm, "user1"), "user1", "127.0.0.1", "form", true, null, null)).getId();
// Create/Update client session's notes concurrently
IntStream.range(0, 200 * CLIENTS_COUNT).parallel()
.forEach(i -> inComittedTransaction(i, (session, n) -> {
RealmModel realm = session.realms().getRealm(realmId);
ClientModel client = realm.getClientByClientId("client" + (n % CLIENTS_COUNT));
// THIS SHOULD BE REMOVED AS PART OF ISSUE https://github.com/keycloak/keycloak/issues/13273
// Without this lock more threads can create client session but only one of them is referenced from
// user session. All others that are not referenced are basically lost and should not be created.
// In other words, this lock is to make sure only one thread creates client session, all other
// should use client session created by the first thread
//
// This is basically the same as JpaMapKeycloakTransaction#read method is doing after calling lockUserSessionsForModification() method
if (isHotRodStore) {
SYNC_USESSION.lock();
releaseLockOnTransactionCommit(session, SYNC_USESSION);
}
UserSessionModel uSession = lockUserSessionsForModification(session, () -> session.sessions().getUserSession(realm, uId));
AuthenticatedClientSessionModel cSession = uSession.getAuthenticatedClientSessionByClient(client.getId());
if (cSession == null) {
wasWriting.set(true);
cSession = session.sessions().createClientSession(realm, client, uSession);
}
cSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state-" + n);
return null;
}));
withRealm(this.realmId, (session, realm) -> {
UserSessionModel uSession = session.sessions().getUserSession(realm, uId);
assertThat(uSession.getAuthenticatedClientSessions(), aMapWithSize(CLIENTS_COUNT));
for (int i = 0; i < CLIENTS_COUNT; i++) {
ClientModel client = realm.getClientByClientId("client" + (i % CLIENTS_COUNT));
AuthenticatedClientSessionModel cSession = uSession.getAuthenticatedClientSessionByClient(client.getId());
assertThat(cSession.getNote(OIDCLoginProtocol.STATE_PARAM), startsWith("state-"));
}
return null;
});
inComittedTransaction((Consumer<KeycloakSession>) session -> session.realms().removeRealm(realmId));
if (isHotRodStore) {
inComittedTransaction(session -> {
HotRodConnectionProvider provider = session.getProvider(HotRodConnectionProvider.class);
Map<?,?> remoteCache = provider.getRemoteCache(ModelEntityUtil.getModelName(UserSessionModel.class));
assertThat(remoteCache, anEmptyMap());
});
}
}
private void releaseLockOnTransactionCommit(KeycloakSession session, Lock l) {
session.getTransactionManager().enlistAfterCompletion(new KeycloakTransaction() {
@Override
public void begin() {
}
@Override
public void commit() {
// THIS IS WORKAROUND FOR MISSING https://github.com/keycloak/keycloak/issues/13280
// It happens that calling remoteCache.put() in one thread and remoteCache.get() in another thread after
// releasing the l lock is so fast that changes are not yet present in the Infinispan server, to avoid
// this we need to leverage HotRod transactions that makes sure the changes are propagated to Infinispan
// server in commit phase
//
// In other words, we need to give Infinispan some time to process put request before we let other
// threads query client session created in this transaction
if (isHotRodStore && wasWriting.get()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
wasWriting.set(false);
}
l.unlock();
}
@Override
public void rollback() {
l.unlock();
}
@Override
public void setRollbackOnly() {
}
@Override
public boolean getRollbackOnly() {
return false;
}
@Override
public boolean isActive() {
return false;
}
});
}
}