Hot Rod map storage: Client scope no-downtime store

This commit is contained in:
Martin Kanis 2022-02-11 13:45:08 +01:00 committed by Hynek Mlnařík
parent 9297a5e1b2
commit 6249e34177
12 changed files with 199 additions and 4 deletions

View file

@ -1312,7 +1312,7 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public void removeClientScopes(RealmModel realm) {
realm.getClientScopesStream().map(ClientScopeModel::getId).forEach(id -> removeClientScope(realm, id));
getClientScopesStream(realm).map(ClientScopeModel::getId).forEach(id -> removeClientScope(realm, id));
}
@Override

View file

@ -22,11 +22,13 @@ import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.clientscope.MapClientScopeEntity;
import org.keycloak.models.map.group.MapGroupEntity;
import org.keycloak.models.map.role.MapRoleEntity;
import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity;
@ -37,6 +39,8 @@ import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityD
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.hotRod.clientscope.HotRodClientScopeEntity;
import org.keycloak.models.map.storage.hotRod.clientscope.HotRodClientScopeEntityDelegate;
import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor;
import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProvider;
import org.keycloak.models.map.storage.MapStorageProvider;
@ -65,6 +69,7 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
private final static DeepCloner CLONER = new DeepCloner.Builder()
.constructor(MapClientEntity.class, HotRodClientEntityDelegate::new)
.constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntityDelegate::new)
.constructor(MapClientScopeEntity.class, HotRodClientScopeEntityDelegate::new)
.constructor(MapGroupEntity.class, HotRodGroupEntityDelegate::new)
.constructor(MapRoleEntity.class, HotRodRoleEntityDelegate::new)
.constructor(MapUserEntity.class, HotRodUserEntityDelegate::new)
@ -81,6 +86,11 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
HotRodClientEntity.class,
HotRodClientEntityDelegate::new));
ENTITY_DESCRIPTOR_MAP.put(ClientScopeModel.class,
new HotRodEntityDescriptor<>(ClientScopeModel.class,
HotRodClientScopeEntity.class,
HotRodClientScopeEntityDelegate::new));
// Groups descriptor
ENTITY_DESCRIPTOR_MAP.put(GroupModel.class,
new HotRodEntityDescriptor<>(GroupModel.class,

View file

@ -0,0 +1,111 @@
/*
* 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.clientscope;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.clientscope.MapClientScopeEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity;
import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity;
import org.keycloak.models.map.storage.hotRod.common.HotRodAttributeEntityNonIndexed;
import org.keycloak.models.map.storage.hotRod.common.UpdatableHotRodEntityDelegateImpl;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.clientscope.MapClientScopeEntity",
inherits = "org.keycloak.models.map.storage.hotRod.clientscope.HotRodClientScopeEntity.AbstractHotRodClientScopeEntityDelegate"
)
@ProtoDoc("@Indexed")
public class HotRodClientScopeEntity extends AbstractHotRodEntity {
@ProtoField(number = 1, required = true)
public int entityVersion = 1;
@ProtoField(number = 2, required = true)
public String id;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 3)
public String realmId;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 4)
public String name;
@ProtoField(number = 5)
public String protocol;
@ProtoField(number = 6)
public String description;
@ProtoField(number = 7)
public Collection<String> scopeMappings;
@ProtoField(number = 8)
public Set<HotRodProtocolMapperEntity> protocolMappers;
@ProtoField(number = 9)
public Set<HotRodAttributeEntityNonIndexed> attributes;
public static abstract class AbstractHotRodClientScopeEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodClientScopeEntity> implements MapClientScopeEntity {
@Override
public String getId() {
return getHotRodEntity().id;
}
@Override
public void setId(String id) {
HotRodClientScopeEntity entity = getHotRodEntity();
if (entity.id != null) throw new IllegalStateException("Id cannot be changed");
entity.id = id;
entity.updated |= id != null;
}
@Override
public Optional<MapProtocolMapperEntity> getProtocolMapper(String id) {
Set<MapProtocolMapperEntity> mappers = getProtocolMappers();
if (mappers == null || mappers.isEmpty()) return Optional.empty();
return mappers.stream().filter(m -> Objects.equals(m.getId(), id)).findFirst();
}
@Override
public void removeProtocolMapper(String id) {
HotRodClientScopeEntity entity = getHotRodEntity();
entity.updated |= entity.protocolMappers != null && entity.protocolMappers.removeIf(m -> Objects.equals(m.id, id));
}
}
@Override
public boolean equals(Object o) {
return HotRodClientScopeEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodClientScopeEntityDelegate.entityHashCode(this);
}
}

View file

@ -21,6 +21,7 @@ import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEntity;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -38,7 +39,7 @@ public class HotRodTypesUtils {
}
public static <MapKey, MapValue, SetValue> Map<MapKey, MapValue> migrateSetToMap(Set<SetValue> set, Function<SetValue, MapKey> keyProducer, Function<SetValue, MapValue> valueProducer) {
return set == null ? null : set.stream().collect(Collectors.toMap(keyProducer, valueProducer));
return set == null ? null : set.stream().collect(HashMap::new, (m, v) -> m.put(keyProducer.apply(v), valueProducer.apply(v)), HashMap::putAll);
}
public static <T, V> HotRodPair<T, V> createHotRodPairFromMapEntry(Map.Entry<T, V> entry) {

View file

@ -21,6 +21,7 @@ import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntity;
import org.keycloak.models.map.storage.hotRod.clientscope.HotRodClientScopeEntity;
import org.keycloak.models.map.storage.hotRod.group.HotRodGroupEntity;
import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity;
import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity;
@ -37,6 +38,9 @@ import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEn
HotRodClientEntity.class,
HotRodProtocolMapperEntity.class,
// Client scopes
HotRodClientScopeEntity.class,
// Groups
HotRodGroupEntity.class,

View file

@ -12,6 +12,14 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="client-scopes" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientScopeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="groups" mode="SYNC">
<indexing>
<indexed-entities>

View file

@ -14,6 +14,14 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="client-scopes" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientScopeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="groups" mode="SYNC">
<indexing>
<indexed-entities>

View file

@ -260,7 +260,7 @@
"username": "${keycloak.connectionsHotRod.username:myuser}",
"password": "${keycloak.connectionsHotRod.password:qwer1234!}",
"enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}",
"reindexCaches": "${keycloak.connectionsHotRod.reindexCaches:clients,groups,users,roles}"
"reindexCaches": "${keycloak.connectionsHotRod.reindexCaches:clients,client-scopes,groups,users,roles}"
}
},

View file

@ -1497,6 +1497,7 @@
<configuration>
<systemPropertyVariables>
<keycloak.client.map.storage.provider>hotrod</keycloak.client.map.storage.provider>
<keycloak.clientScope.map.storage.provider>hotrod</keycloak.clientScope.map.storage.provider>
<keycloak.group.map.storage.provider>hotrod</keycloak.group.map.storage.provider>
<keycloak.role.map.storage.provider>hotrod</keycloak.role.map.storage.provider>
<keycloak.user.map.storage.provider>hotrod</keycloak.user.map.storage.provider>

View file

@ -10,6 +10,14 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="client-scopes" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodClientScopeEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="groups" mode="SYNC">
<indexing>
<indexed-entities>

View file

@ -17,6 +17,8 @@
package org.keycloak.testsuite.model;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
@ -25,6 +27,7 @@ import static org.hamcrest.Matchers.notNullValue;
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -32,8 +35,11 @@ import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel;
import org.keycloak.models.RoleProvider;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
*
@ -213,4 +219,42 @@ public class ClientModelTest extends KeycloakModelTest {
return null;
});
}
@Test
public void testClientScopes() {
List<String> clientScopes = new LinkedList<>();
withRealm(realmId, (session, realm) -> {
ClientModel client = session.clients().addClient(realm, "myClientId");
ClientScopeModel clientScope1 = session.clientScopes().addClientScope(realm, "myClientScope1");
clientScopes.add(clientScope1.getId());
ClientScopeModel clientScope2 = session.clientScopes().addClientScope(realm, "myClientScope2");
clientScopes.add(clientScope2.getId());
client.addClientScope(clientScope1, true);
client.addClientScope(clientScope2, false);
return null;
});
withRealm(realmId, (session, realm) -> {
List<String> actualClientScopes = session.clientScopes().getClientScopesStream(realm).map(ClientScopeModel::getId).collect(Collectors.toList());
assertThat(actualClientScopes, containsInAnyOrder(clientScopes.toArray()));
ClientScopeModel clientScopeById = session.clientScopes().getClientScopeById(realm, clientScopes.get(0));
assertThat(clientScopeById.getId(), is(clientScopes.get(0)));
session.clientScopes().removeClientScopes(realm);
return null;
});
withRealm(realmId, (session, realm) -> {
List<ClientScopeModel> actualClientScopes = session.clientScopes().getClientScopesStream(realm).collect(Collectors.toList());
assertThat(actualClientScopes, empty());
return null;
});
}
}

View file

@ -74,7 +74,7 @@ public class HotRodMapStorage extends KeycloakModelParameters {
public void updateConfig(Config cf) {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.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)
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("role").provider(MapRoleProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)