Hot Rod map storage: Roles no-downtime store

This commit is contained in:
Martin Kanis 2022-02-09 14:28:06 +01:00 committed by Hynek Mlnařík
parent f54cd969f8
commit 26ac142b99
12 changed files with 190 additions and 3 deletions

View file

@ -25,8 +25,12 @@ import org.keycloak.models.ClientModel;
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.group.MapGroupEntity;
import org.keycloak.models.map.role.MapRoleEntity;
import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity;
import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntityDelegate;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntity;
import org.keycloak.models.map.storage.hotRod.client.HotRodClientEntityDelegate;
import org.keycloak.models.map.storage.hotRod.client.HotRodProtocolMapperEntityDelegate;
@ -62,6 +66,7 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
.constructor(MapClientEntity.class, HotRodClientEntityDelegate::new)
.constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntityDelegate::new)
.constructor(MapGroupEntity.class, HotRodGroupEntityDelegate::new)
.constructor(MapRoleEntity.class, HotRodRoleEntityDelegate::new)
.constructor(MapUserEntity.class, HotRodUserEntityDelegate::new)
.constructor(MapUserCredentialEntity.class, HotRodUserCredentialEntityDelegate::new)
.constructor(MapUserFederatedIdentityEntity.class, HotRodUserFederatedIdentityEntityDelegate::new)
@ -81,6 +86,13 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory
new HotRodEntityDescriptor<>(GroupModel.class,
HotRodGroupEntity.class,
HotRodGroupEntityDelegate::new));
// Roles descriptor
ENTITY_DESCRIPTOR_MAP.put(RoleModel.class,
new HotRodEntityDescriptor<>(RoleModel.class,
HotRodRoleEntity.class,
HotRodRoleEntityDelegate::new));
// Users descriptor
ENTITY_DESCRIPTOR_MAP.put(UserModel.class,
new HotRodEntityDescriptor<>(UserModel.class,

View file

@ -59,6 +59,8 @@ public class IckleQueryMapModelCriteriaBuilder<E extends AbstractHotRodEntity, M
INFINISPAN_NAME_OVERRIDES.put(GroupModel.SearchableFields.PARENT_ID, "parentId");
INFINISPAN_NAME_OVERRIDES.put(GroupModel.SearchableFields.ASSIGNED_ROLE, "grantedRoles");
INFINISPAN_NAME_OVERRIDES.put(RoleModel.SearchableFields.IS_CLIENT_ROLE, "clientRole");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT, "serviceAccountClientLink");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.CONSENT_FOR_CLIENT, "userConsents.clientId");
INFINISPAN_NAME_OVERRIDES.put(UserModel.SearchableFields.CONSENT_WITH_CLIENT_SCOPE, "userConsents.grantedClientScopesIds");

View file

@ -41,6 +41,9 @@ import java.util.Set;
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class HotRodUtils {
public static final int DEFAULT_MAX_RESULTS = Integer.MAX_VALUE >> 1;
/**
* Not suitable for a production usage. Only for development and test purposes.
* Also do not use in clustered environment.
@ -89,6 +92,11 @@ public class HotRodUtils {
public static <T> Query<T> paginateQuery(Query<T> query, Integer first, Integer max) {
if (first != null && first > 0) {
query = query.startOffset(first);
// workaround because of ISPN-13702 bug, see https://github.com/keycloak/keycloak/issues/10090
if (max == null || max < 0) {
max = DEFAULT_MAX_RESULTS;
}
}
if (max != null && max >= 0) {

View file

@ -22,6 +22,7 @@ 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.group.HotRodGroupEntity;
import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity;
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;
@ -39,6 +40,9 @@ import org.keycloak.models.map.storage.hotRod.user.HotRodUserFederatedIdentityEn
// Groups
HotRodGroupEntity.class,
// Roles
HotRodRoleEntity.class,
// Users
HotRodUserEntity.class,
HotRodUserConsentEntity.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.role;
import org.infinispan.protostream.annotations.ProtoDoc;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation;
import org.keycloak.models.map.role.MapRoleEntity;
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.Objects;
import java.util.Set;
@GenerateHotRodEntityImplementation(
implementInterface = "org.keycloak.models.map.role.MapRoleEntity",
inherits = "org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity.AbstractHotRodRoleEntityDelegate"
)
@ProtoDoc("@Indexed")
public class HotRodRoleEntity extends AbstractHotRodEntity {
public static abstract class AbstractHotRodRoleEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodRoleEntity> implements MapRoleEntity {
@Override
public String getId() {
return getHotRodEntity().id;
}
@Override
public void setId(String id) {
HotRodRoleEntity entity = getHotRodEntity();
if (entity.id != null) throw new IllegalStateException("Id cannot be changed");
entity.id = id;
entity.updated |= id != null;
}
@Override
public void setName(String name) {
HotRodRoleEntity entity = getHotRodEntity();
entity.updated |= ! Objects.equals(entity.name, name);
entity.name = name;
entity.nameLowercase = name == null ? null : name.toLowerCase();
}
}
@ProtoField(number = 1, required = true)
public int entityVersion = 1;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@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;
/**
* Lowercase interpretation of {@link #name} field. Infinispan doesn't support case-insensitive LIKE for non-analyzed fields.
* Search on analyzed fields can be case-insensitive (based on used analyzer) but doesn't support ORDER BY analyzed field.
*/
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 5)
public String nameLowercase;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES, analyze = Analyze.YES, analyzer = @Analyzer(definition = \"filename\"))")
@ProtoField(number = 6)
public String description;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 7)
public Boolean clientRole;
@ProtoDoc("@Field(index = Index.YES, store = Store.YES)")
@ProtoField(number = 8)
public String clientId;
@ProtoField(number = 9)
public Set<String> compositeRoles;
@ProtoField(number = 10)
public Set<HotRodAttributeEntityNonIndexed> attributes;
@Override
public boolean equals(Object o) {
return HotRodRoleEntityDelegate.entityEquals(this, o);
}
@Override
public int hashCode() {
return HotRodRoleEntityDelegate.entityHashCode(this);
}
}

View file

@ -20,6 +20,14 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="roles" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodRoleEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="users" mode="SYNC">
<indexing>
<indexed-entities>
@ -31,4 +39,4 @@
<encoding media-type="application/x-protostream"/>
</distributed-cache>
</cache-container>
</infinispan>
</infinispan>

View file

@ -22,6 +22,14 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="roles" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodRoleEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="users" 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}"
"reindexCaches": "${keycloak.connectionsHotRod.reindexCaches:clients,groups,users,roles}"
}
},

View file

@ -1498,6 +1498,7 @@
<systemPropertyVariables>
<keycloak.client.map.storage.provider>hotrod</keycloak.client.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>
</systemPropertyVariables>
</configuration>

View file

@ -18,6 +18,14 @@
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="roles" mode="SYNC">
<indexing>
<indexed-entities>
<indexed-entity>kc.HotRodRoleEntity</indexed-entity>
</indexed-entities>
</indexing>
<encoding media-type="application/x-protostream"/>
</distributed-cache>
<distributed-cache name="users" mode="SYNC">
<indexing>
<indexed-entities>

View file

@ -77,7 +77,7 @@ public class HotRodMapStorage extends KeycloakModelParameters {
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.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, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("role").provider(MapRoleProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi(DeploymentStateSpi.NAME).provider(MapDeploymentStateProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(StoreFactorySpi.NAME).provider(MapAuthorizationStoreFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("user").provider(MapUserProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)

View file

@ -66,6 +66,7 @@ public class RoleModelTest extends KeycloakModelTest {
rolesSubset = IntStream.range(0, 10)
.boxed()
.map(i -> session.roles().addRealmRole(realm, "main-role-composite-" + i))
.peek(role -> role.setDescription("This is a description for " + role.getName() + " realm role."))
.peek(mainRole::addCompositeRole)
.map(RoleModel::getId)
.collect(Collectors.toList());
@ -74,6 +75,7 @@ public class RoleModelTest extends KeycloakModelTest {
rolesSubset.addAll(IntStream.range(10, 20)
.boxed()
.map(i -> session.roles().addClientRole(clientModel, "main-role-composite-" + i))
.peek(role -> role.setDescription("This is a description for " + role.getName() + " client role."))
.peek(mainRole::addCompositeRole)
.map(RoleModel::getId)
.collect(Collectors.toList()));
@ -202,6 +204,29 @@ public class RoleModelTest extends KeycloakModelTest {
testRolesWithIdsPaginationSearchQueries(this::getModelResult);
}
@Test
public void testSearchRolesByDescription() {
withRealm(realmId, (session, realm) -> {
List<RoleModel> realmRolesByDescription = session.roles().searchForRolesStream(realm, "This is a", null, null).collect(Collectors.toList());
assertThat(realmRolesByDescription, hasSize(10));
realmRolesByDescription = session.roles().searchForRolesStream(realm, "realm role.", 5, null).collect(Collectors.toList());
assertThat(realmRolesByDescription, hasSize(5));
realmRolesByDescription = session.roles().searchForRolesStream(realm, "DESCRIPTION FOR", 3, 9).collect(Collectors.toList());
assertThat(realmRolesByDescription, hasSize(7));
ClientModel client = session.clients().getClientByClientId(realm, "client-with-roles");
List<RoleModel> clientRolesByDescription = session.roles().searchForClientRolesStream(client, "this is a", 0, 10).collect(Collectors.toList());
assertThat(clientRolesByDescription, hasSize(10));
clientRolesByDescription = session.roles().searchForClientRolesStream(client, "role-composite-13 client role", null, null).collect(Collectors.toList());
assertThat(clientRolesByDescription, hasSize(1));
assertThat(clientRolesByDescription.get(0).getDescription(), is("This is a description for main-role-composite-13 client role."));
return null;
});
}
public void testRolesWithIdsPaginationSearchQueries(GetResult resultProvider) {
// test all parameters together
List<RoleModel> result = resultProvider.getResult("1", 4, 3);