Hot Rod map storage: Roles no-downtime store
This commit is contained in:
parent
f54cd969f8
commit
26ac142b99
12 changed files with 190 additions and 3 deletions
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue