Hot Rod map storage: Single-use (action token) no-downtime store

This commit is contained in:
Martin Kanis 2022-05-05 15:17:07 +02:00 committed by Hynek Mlnařík
parent a102e28dbb
commit df72cf72f2
11 changed files with 297 additions and 22 deletions

View file

@ -26,6 +26,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.common.Profile;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.ActionTokenValueModel;
import org.keycloak.events.Event;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.models.AuthenticatedClientSessionModel;
@ -66,6 +67,7 @@ import org.keycloak.models.map.realm.entity.MapRequiredActionProviderEntity;
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.hotRod.authSession.HotRodAuthenticationSessionEntityDelegate;
import org.keycloak.models.map.storage.hotRod.authSession.HotRodRootAuthenticationSessionEntity;
import org.keycloak.models.map.storage.hotRod.authSession.HotRodRootAuthenticationSessionEntityDelegate;
@ -116,6 +118,8 @@ import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodOTPPolicyEntity
import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredActionProviderEntityDelegate;
import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredCredentialEntityDelegate;
import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodWebAuthnPolicyEntityDelegate;
import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity;
import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntityDelegate;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity;
@ -159,6 +163,8 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
.constructor(MapRoleEntity.class, HotRodRoleEntityDelegate::new)
.constructor(MapSingleUseObjectEntity.class, HotRodSingleUseObjectEntityDelegate::new)
.constructor(MapUserEntity.class, HotRodUserEntityDelegate::new)
.constructor(MapUserCredentialEntity.class, HotRodUserCredentialEntityDelegate::new)
.constructor(MapUserFederatedIdentityEntity.class, HotRodUserFederatedIdentityEntityDelegate::new)
@ -242,6 +248,12 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
HotRodRealmEntity.class,
HotRodRealmEntityDelegate::new));
// single-use object storage descriptor
ENTITY_DESCRIPTOR_MAP.put(ActionTokenValueModel.class,
new HotRodEntityDescriptor<>(ActionTokenValueModel.class,
HotRodSingleUseObjectEntity.class,
HotRodSingleUseObjectEntityDelegate::new));
// User sessions descriptor
ENTITY_DESCRIPTOR_MAP.put(UserSessionModel.class,
new HotRodEntityDescriptor<>(UserSessionModel.class,
@ -339,6 +351,8 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
HotRodMapStorage clientSessionStore = getHotRodStorage(session, AuthenticatedClientSessionModel.class);
return new HotRodUserSessionMapStorage(clientSessionStore, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER);
} else if (modelType == ActionTokenValueModel.class) {
return new SingleUseObjectHotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER);
}
return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER);
}

View file

@ -0,0 +1,109 @@
/*
* 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;
import org.infinispan.client.hotrod.RemoteCache;
import org.keycloak.models.ActionTokenValueModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.StringKeyConverter;
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
import org.keycloak.models.map.storage.chm.SingleUseObjectKeycloakTransaction;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.chm.MapFieldPredicates;
import org.keycloak.models.map.storage.chm.SingleUseObjectModelCriteriaBuilder;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor;
import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity;
import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
import java.util.stream.Stream;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectHotRodMapStorage<K, E extends AbstractHotRodEntity, V extends HotRodEntityDelegate<E> & AbstractEntity, M>
extends HotRodMapStorage<String, HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate, ActionTokenValueModel> {
private final StringKeyConverter<String> keyConverter;
private final HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate> storedEntityDescriptor;
private final DeepCloner cloner;
public SingleUseObjectHotRodMapStorage(RemoteCache<String, HotRodSingleUseObjectEntity> remoteCache, StringKeyConverter<String> keyConverter,
HotRodEntityDescriptor<HotRodSingleUseObjectEntity, HotRodSingleUseObjectEntityDelegate> storedEntityDescriptor,
DeepCloner cloner) {
super(remoteCache, keyConverter, storedEntityDescriptor, cloner);
this.keyConverter = keyConverter;
this.storedEntityDescriptor = storedEntityDescriptor;
this.cloner = cloner;
}
@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;
}
@Override
public HotRodSingleUseObjectEntityDelegate create(HotRodSingleUseObjectEntityDelegate value) {
if (value.getId() == null) {
if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) {
value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce());
}
}
return super.create(value);
}
@Override
public Stream<HotRodSingleUseObjectEntityDelegate> read(QueryParameters<ActionTokenValueModel> queryParameters) {
DefaultModelCriteria<ActionTokenValueModel> criteria = queryParameters.getModelCriteriaBuilder();
if (criteria == null) {
return Stream.empty();
}
SingleUseObjectModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createSingleUseObjectCriteriaBuilder());
if (mcb.isValid()) {
HotRodSingleUseObjectEntityDelegate value = read(mcb.getKey());
return value != null && value.getHotRodEntity() != null ? Stream.of(value) : Stream.empty();
}
return super.read(queryParameters);
}
private SingleUseObjectModelCriteriaBuilder createSingleUseObjectCriteriaBuilder() {
return new SingleUseObjectModelCriteriaBuilder();
}
}

View file

@ -54,6 +54,7 @@ import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredCredent
import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequirement;
import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodWebAuthnPolicyEntity;
import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity;
import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity;
@ -110,6 +111,9 @@ import org.keycloak.models.map.storage.hotRod.userSession.HotRodUserSessionEntit
HotRodWebAuthnPolicyEntity.class,
HotRodRealmEntity.class,
// single-use objects
HotRodSingleUseObjectEntity.class,
// User sessions
HotRodUserSessionEntity.class,
HotRodSessionState.class,

View file

@ -0,0 +1,85 @@
/*
* 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.singleUseObject;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.models.map.storage.hotRod.common.HotRodPair;
import org.keycloak.models.map.storage.hotRod.common.UpdatableHotRodEntityDelegateImpl;
import java.util.Set;
@GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity",
inherits = "org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity.AbstractHotRodSingleUseObjectEntityDelegate"
)
@ProtoDoc("@Indexed")
public class HotRodSingleUseObjectEntity extends AbstractHotRodEntity {
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 1)
public Integer entityVersion = 1;
@ProtoField(number = 2)
public String id;
@ProtoField(number = 3)
public String userId;
@ProtoField(number = 4)
public String actionId;
@ProtoField(number = 5)
public String actionVerificationNonce;
@ProtoField(number = 6)
public Long expiration;
@ProtoField(number = 7)
public Set<HotRodPair<String, String>> notes;
public static abstract class AbstractHotRodSingleUseObjectEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodSingleUseObjectEntity> implements MapSingleUseObjectEntity {
@Override
public String getId() {
HotRodSingleUseObjectEntity hotRodEntity = getHotRodEntity();
return hotRodEntity != null ? hotRodEntity.id : null;
}
@Override
public void setId(String id) {
HotRodSingleUseObjectEntity entity = getHotRodEntity();
if (entity.id != null) throw new IllegalStateException("Id cannot be changed");
entity.id = id;
entity.updated |= id != null;
}
}
@Override
public boolean equals(Object o) {
return HotRodSingleUseObjectEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodSingleUseObjectEntityDelegate.entityHashCode(this);
}
}

View file

@ -116,5 +116,13 @@
</indexed-entities>
</indexing>
</distributed-cache>
<distributed-cache name="single-use-objects" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodSingleUseObjectEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -118,5 +118,13 @@
</indexed-entities>
</indexing>
</distributed-cache>
<distributed-cache name="single-use-objects" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodSingleUseObjectEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -26,9 +26,7 @@ import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
import java.util.stream.Stream;
/**
@ -44,7 +42,13 @@ public class SingleUseObjectConcurrentHashMapStorage<K, V extends AbstractEntity
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> actionTokenTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
return actionTokenTransaction == null ? new SingleUseObjectConcurrentHashMapStorage.Transaction(getKeyConverter(), cloner, fieldPredicates) : actionTokenTransaction;
if (actionTokenTransaction == null) {
actionTokenTransaction = new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates);
session.setAttribute("map-transaction-" + hashCode(), actionTokenTransaction);
}
return actionTokenTransaction;
}
@Override
@ -78,21 +82,4 @@ public class SingleUseObjectConcurrentHashMapStorage<K, V extends AbstractEntity
return new SingleUseObjectModelCriteriaBuilder();
}
private class Transaction extends ConcurrentHashMapKeycloakTransaction<K, MapSingleUseObjectEntity, ActionTokenValueModel> {
public Transaction(StringKeyConverter<K> keyConverter, DeepCloner cloner,
Map<SearchableModelField<? super ActionTokenValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapSingleUseObjectEntity, ActionTokenValueModel>> fieldPredicates) {
super(SingleUseObjectConcurrentHashMapStorage.this, keyConverter, cloner, fieldPredicates);
}
@Override
public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) {
if (value.getId() == null) {
if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) {
value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce());
}
}
return super.create(value);
}
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.chm;
import org.keycloak.models.ActionTokenValueModel;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.StringKeyConverter;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
/**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectKeycloakTransaction<K> extends ConcurrentHashMapKeycloakTransaction<K, MapSingleUseObjectEntity, ActionTokenValueModel> {
public SingleUseObjectKeycloakTransaction(ConcurrentHashMapCrudOperations<MapSingleUseObjectEntity, ActionTokenValueModel> map,
StringKeyConverter<K> keyConverter,
DeepCloner cloner,
Map<SearchableModelField<? super ActionTokenValueModel>,
MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapSingleUseObjectEntity, ActionTokenValueModel>> fieldPredicates) {
super(map, keyConverter, cloner, fieldPredicates);
}
@Override
public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) {
if (value.getId() == null) {
if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) {
value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce());
}
}
return super.create(value);
}
}

View file

@ -1507,6 +1507,8 @@
<keycloak.userSession.map.storage.provider>hotrod</keycloak.userSession.map.storage.provider>
<keycloak.authorization.map.storage.provider>hotrod</keycloak.authorization.map.storage.provider>
<keycloak.eventStore.map.storage.provider>hotrod</keycloak.eventStore.map.storage.provider>
<keycloak.actionToken.map.storage.provider>hotrod</keycloak.actionToken.map.storage.provider>
<keycloak.singleUseObject.map.storage.provider>hotrod</keycloak.singleUseObject.map.storage.provider>
</systemPropertyVariables>
</configuration>
</plugin>

View file

@ -114,5 +114,13 @@
</indexed-entities>
</indexing>
</distributed-cache>
<distributed-cache name="single-use-objects" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodSingleUseObjectEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>

View file

@ -76,8 +76,8 @@ public class HotRodMapStorage extends KeycloakModelParameters {
@Override
public void updateConfig(Config cf) {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)