Optimize user-client session relationship for HotRod storage
Closes #12818
This commit is contained in:
parent
9a89560771
commit
eb1f31e9dd
19 changed files with 799 additions and 40 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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()));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue