KEYCLOAK-19028 Add HotRod Map storage implementation

This commit is contained in:
Michal Hajas 2021-08-17 13:30:52 +02:00 committed by Hynek Mlnařík
parent 6071e2d518
commit 2f9a5aae0f
46 changed files with 2351 additions and 92 deletions

View file

@ -31,6 +31,7 @@ jobs:
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-quarkus mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-quarkus
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-wildfly mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-wildfly
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-undertow mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/auth-server -Pauth-server-undertow
mvn clean install -nsu -B -e -f testsuite/integration-arquillian/servers/cache-server -Pcache-server-infinispan
- name: Store Keycloak artifacts - name: Store Keycloak artifacts
id: store-keycloak id: store-keycloak
@ -136,13 +137,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
server: ['quarkus', 'undertow-map', 'wildfly'] server: ['quarkus', 'undertow-map', 'wildfly', 'undertow-map-hot-rod']
tests: ['group1','group2','group3'] tests: ['group1','group2','group3']
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Check whether HEAD^ contains HotRod storage relevant changes
run: echo "GIT_HOTROD_RELEVANT_DIFF=$( git diff --name-only HEAD^ | egrep -ic -e '^model/hot-rod|^model/map|^model/build-processor|^testsuite/model' )" >> $GITHUB_ENV
- name: Cache Maven packages - name: Cache Maven packages
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ~/.m2/repository path: ~/.m2/repository
@ -150,6 +157,7 @@ jobs:
restore-keys: cache-1-${{ runner.os }}-m2 restore-keys: cache-1-${{ runner.os }}-m2
- name: Download built keycloak - name: Download built keycloak
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
id: download-keycloak id: download-keycloak
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
with: with:
@ -162,16 +170,20 @@ jobs:
# ls -lR ~/.m2/repository # ls -lR ~/.m2/repository
- uses: actions/setup-java@v1 - uses: actions/setup-java@v1
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
with: with:
java-version: ${{ env.DEFAULT_JDK_VERSION }} java-version: ${{ env.DEFAULT_JDK_VERSION }}
- name: Update maven settings - name: Update maven settings
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/ run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/
- name: Run base tests - name: Run base tests
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
run: | run: |
declare -A PARAMS TESTGROUP declare -A PARAMS TESTGROUP
PARAMS["quarkus"]="-Pauth-server-quarkus" PARAMS["quarkus"]="-Pauth-server-quarkus"
PARAMS["undertow-map"]="-Pauth-server-undertow -Pmap-storage -Dpageload.timeout=90000" PARAMS["undertow-map"]="-Pauth-server-undertow -Pmap-storage -Dpageload.timeout=90000"
PARAMS["undertow-map-hot-rod"]="-Pauth-server-undertow -Pmap-storage,map-storage-hot-rod -Dpageload.timeout=90000"
PARAMS["wildfly"]="-Pauth-server-wildfly" PARAMS["wildfly"]="-Pauth-server-wildfly"
TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r" TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r"
TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b" TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b"

View file

@ -47,6 +47,17 @@
<artifactId>protostream-processor</artifactId> <artifactId>protostream-processor</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -0,0 +1,69 @@
/*
* Copyright 2021 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.client;
import org.infinispan.protostream.annotations.ProtoField;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public class HotRodAttributeEntity {
@ProtoField(number = 1)
public String name;
@ProtoField(number = 2)
public List<String> values = new LinkedList<>();
public HotRodAttributeEntity() {
}
public HotRodAttributeEntity(String name, List<String> values) {
this.name = name;
this.values.addAll(values);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getValues() {
return values;
}
public void setValues(List<String> values) {
this.values = values;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HotRodAttributeEntity that = (HotRodAttributeEntity) o;
return Objects.equals(name, that.name) && Objects.equals(values, that.values);
}
@Override
public int hashCode() {
return Objects.hash(name, values);
}
}

View file

@ -0,0 +1,712 @@
/*
* Copyright 2021 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.client;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.HotRodPair;
import org.keycloak.models.map.common.Versioned;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class HotRodClientEntity implements MapClientEntity, Versioned {
@ProtoField(number = 1, required = true)
public int entityVersion = 1;
@ProtoField(number = 2, required = true)
public String id;
@ProtoField(number = 3)
public String realmId;
@ProtoField(number = 4)
public String clientId;
@ProtoField(number = 5)
public String name;
@ProtoField(number = 6)
public String description;
@ProtoField(number = 7)
public Set<String> redirectUris = new HashSet<>();
@ProtoField(number = 8)
public Boolean enabled;
@ProtoField(number = 9)
public Boolean alwaysDisplayInConsole;
@ProtoField(number = 10)
public String clientAuthenticatorType;
@ProtoField(number = 11)
public String secret;
@ProtoField(number = 12)
public String registrationToken;
@ProtoField(number = 13)
public String protocol;
@ProtoField(number = 14)
public Set<HotRodAttributeEntity> attributes = new HashSet<>();
@ProtoField(number = 15)
public Set<HotRodPair<String, String>> authFlowBindings = new HashSet<>();
@ProtoField(number = 16)
public Boolean publicClient;
@ProtoField(number = 17)
public Boolean fullScopeAllowed;
@ProtoField(number = 18)
public Boolean frontchannelLogout;
@ProtoField(number = 19)
public Integer notBefore;
@ProtoField(number = 20)
public Set<String> scope = new HashSet<>();
@ProtoField(number = 21)
public Set<String> webOrigins = new HashSet<>();
@ProtoField(number = 22)
public Set<HotRodProtocolMapperEntity> protocolMappers = new HashSet<>();
@ProtoField(number = 23)
public Set<HotRodPair<String, Boolean>> clientScopes = new HashSet<>();
@ProtoField(number = 24)
public Set<String> scopeMappings = new HashSet<>();
@ProtoField(number = 25)
public Boolean surrogateAuthRequired;
@ProtoField(number = 26)
public String managementUrl;
@ProtoField(number = 27)
public String baseUrl;
@ProtoField(number = 28)
public Boolean bearerOnly;
@ProtoField(number = 29)
public Boolean consentRequired;
@ProtoField(number = 30)
public String rootUrl;
@ProtoField(number = 31)
public Boolean standardFlowEnabled;
@ProtoField(number = 32)
public Boolean implicitFlowEnabled;
@ProtoField(number = 33)
public Boolean directAccessGrantsEnabled;
@ProtoField(number = 34)
public Boolean serviceAccountsEnabled;
@ProtoField(number = 35)
public Integer nodeReRegistrationTimeout;
private boolean updated = false;
private final DeepCloner cloner;
public HotRodClientEntity() {
this(DeepCloner.DUMB_CLONER);
}
public HotRodClientEntity(DeepCloner cloner) {
this.cloner = cloner;
}
@Override
public int getEntityVersion() {
return entityVersion;
}
@Override
public List<String> getAttribute(String name) {
return attributes.stream()
.filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
.findFirst()
.map(HotRodAttributeEntity::getValues)
.orElse(Collections.emptyList());
}
@Override
public Map<String, List<String>> getAttributes() {
return attributes.stream().collect(Collectors.toMap(HotRodAttributeEntity::getName, HotRodAttributeEntity::getValues));
}
@Override
public void setAttribute(String name, List<String> values) {
boolean valueUndefined = values == null || values.isEmpty();
Optional<HotRodAttributeEntity> first = attributes.stream()
.filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
.findFirst();
if (first.isPresent()) {
HotRodAttributeEntity attributeEntity = first.get();
if (valueUndefined) {
this.updated = true;
removeAttribute(name);
} else {
this.updated |= !Objects.equals(attributeEntity.getValues(), values);
attributeEntity.setValues(values);
}
return;
}
// do not create attributes if empty or null
if (valueUndefined) {
return;
}
HotRodAttributeEntity newAttributeEntity = new HotRodAttributeEntity(name, values);
updated |= attributes.add(newAttributeEntity);
}
@Override
public void removeAttribute(String name) {
attributes.stream()
.filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name))
.findFirst()
.ifPresent(attr -> {
this.updated |= attributes.remove(attr);
});
}
@Override
public String getClientId() {
return clientId;
}
@Override
public void setClientId(String clientId) {
this.updated |= ! Objects.equals(this.clientId, clientId);
this.clientId = clientId;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.updated |= ! Objects.equals(this.name, name);
this.name = name;
}
@Override
public String getDescription() {
return description;
}
@Override
public void setDescription(String description) {
this.updated |= ! Objects.equals(this.description, description);
this.description = description;
}
@Override
public Set<String> getRedirectUris() {
return redirectUris;
}
@Override
public void setRedirectUris(Set<String> redirectUris) {
if (redirectUris == null || redirectUris.isEmpty()) {
this.updated |= !this.redirectUris.isEmpty();
this.redirectUris.clear();
return;
}
this.updated |= ! Objects.equals(this.redirectUris, redirectUris);
this.redirectUris.clear();
this.redirectUris.addAll(redirectUris);
}
@Override
public Boolean isEnabled() {
return enabled;
}
@Override
public void setEnabled(Boolean enabled) {
this.updated |= ! Objects.equals(this.enabled, enabled);
this.enabled = enabled;
}
@Override
public Boolean isAlwaysDisplayInConsole() {
return alwaysDisplayInConsole;
}
@Override
public void setAlwaysDisplayInConsole(Boolean alwaysDisplayInConsole) {
this.updated |= ! Objects.equals(this.alwaysDisplayInConsole, alwaysDisplayInConsole);
this.alwaysDisplayInConsole = alwaysDisplayInConsole;
}
@Override
public String getClientAuthenticatorType() {
return clientAuthenticatorType;
}
@Override
public void setClientAuthenticatorType(String clientAuthenticatorType) {
this.updated |= ! Objects.equals(this.clientAuthenticatorType, clientAuthenticatorType);
this.clientAuthenticatorType = clientAuthenticatorType;
}
@Override
public String getSecret() {
return secret;
}
@Override
public void setSecret(String secret) {
this.updated |= ! Objects.equals(this.secret, secret);
this.secret = secret;
}
@Override
public String getRegistrationToken() {
return registrationToken;
}
@Override
public void setRegistrationToken(String registrationToken) {
this.updated |= ! Objects.equals(this.registrationToken, registrationToken);
this.registrationToken = registrationToken;
}
@Override
public String getProtocol() {
return protocol;
}
@Override
public void setProtocol(String protocol) {
this.updated |= ! Objects.equals(this.protocol, protocol);
this.protocol = protocol;
}
@Override
public Map<String, String> getAuthFlowBindings() {
return authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
}
@Override
public void setAuthFlowBindings(Map<String, String> authFlowBindings) {
if (authFlowBindings == null || authFlowBindings.isEmpty()) {
this.updated |= !this.authFlowBindings.isEmpty();
this.authFlowBindings.clear();
return;
}
this.updated = true;
this.authFlowBindings.clear();
this.authFlowBindings.addAll(authFlowBindings.entrySet().stream().map(e -> new HotRodPair<>(e.getKey(), e.getValue())).collect(Collectors.toSet()));
}
@Override
public Boolean isPublicClient() {
return publicClient;
}
@Override
public void setPublicClient(Boolean publicClient) {
this.updated |= ! Objects.equals(this.publicClient, publicClient);
this.publicClient = publicClient;
}
@Override
public void setRealmId(String realmId) {
this.realmId = realmId;
}
@Override
public Boolean isFullScopeAllowed() {
return fullScopeAllowed;
}
@Override
public void setFullScopeAllowed(Boolean fullScopeAllowed) {
this.updated |= ! Objects.equals(this.fullScopeAllowed, fullScopeAllowed);
this.fullScopeAllowed = fullScopeAllowed;
}
@Override
public Boolean isFrontchannelLogout() {
return frontchannelLogout;
}
@Override
public void setFrontchannelLogout(Boolean frontchannelLogout) {
this.updated |= ! Objects.equals(this.frontchannelLogout, frontchannelLogout);
this.frontchannelLogout = frontchannelLogout;
}
@Override
public Integer getNotBefore() {
return notBefore;
}
@Override
public void setNotBefore(Integer notBefore) {
this.updated |= ! Objects.equals(this.notBefore, notBefore);
this.notBefore = notBefore;
}
@Override
public Set<String> getScope() {
return scope;
}
@Override
public void setScope(Set<String> scope) {
if (scope == null || scope.isEmpty()) {
this.updated |= !this.scope.isEmpty();
this.scope.clear();
return;
}
this.updated |= ! Objects.equals(this.scope, scope);
this.scope.clear();
this.scope.addAll(scope);
}
@Override
public Set<String> getWebOrigins() {
return webOrigins;
}
@Override
public void setWebOrigins(Set<String> webOrigins) {
if (webOrigins == null || webOrigins.isEmpty()) {
this.updated |= !this.webOrigins.isEmpty();
this.webOrigins.clear();
return;
}
this.updated |= ! Objects.equals(this.webOrigins, webOrigins);
this.webOrigins.clear();
this.webOrigins.addAll(webOrigins);
}
@Override
public Map<String, MapProtocolMapperEntity> getProtocolMappers() {
return protocolMappers.stream().collect(Collectors.toMap(HotRodProtocolMapperEntity::getId, Function.identity()));
}
@Override
public MapProtocolMapperEntity getProtocolMapper(String id) {
return protocolMappers.stream().filter(hotRodMapper -> Objects.equals(hotRodMapper.getId(), id)).findFirst().orElse(null);
}
@Override
public void setProtocolMapper(String id, MapProtocolMapperEntity mapping) {
removeProtocolMapper(id);
protocolMappers.add((HotRodProtocolMapperEntity) cloner.from(mapping)); // Workaround, will be replaced by cloners
this.updated = true;
}
@Override
public void removeProtocolMapper(String id) {
protocolMappers.stream().filter(entity -> Objects.equals(id, entity.id))
.findFirst()
.ifPresent(entity -> {
protocolMappers.remove(entity);
updated = true;
});
}
@Override
public Boolean isSurrogateAuthRequired() {
return surrogateAuthRequired;
}
@Override
public void setSurrogateAuthRequired(Boolean surrogateAuthRequired) {
this.updated |= ! Objects.equals(this.surrogateAuthRequired, surrogateAuthRequired);
this.surrogateAuthRequired = surrogateAuthRequired;
}
@Override
public String getManagementUrl() {
return managementUrl;
}
@Override
public void setManagementUrl(String managementUrl) {
this.updated |= ! Objects.equals(this.managementUrl, managementUrl);
this.managementUrl = managementUrl;
}
@Override
public String getRootUrl() {
return rootUrl;
}
@Override
public void setRootUrl(String rootUrl) {
this.updated |= ! Objects.equals(this.rootUrl, rootUrl);
this.rootUrl = rootUrl;
}
@Override
public String getBaseUrl() {
return baseUrl;
}
@Override
public void setBaseUrl(String baseUrl) {
this.updated |= ! Objects.equals(this.baseUrl, baseUrl);
this.baseUrl = baseUrl;
}
@Override
public Boolean isBearerOnly() {
return bearerOnly;
}
@Override
public void setBearerOnly(Boolean bearerOnly) {
this.updated |= ! Objects.equals(this.bearerOnly, bearerOnly);
this.bearerOnly = bearerOnly;
}
@Override
public Boolean isConsentRequired() {
return consentRequired;
}
@Override
public void setConsentRequired(Boolean consentRequired) {
this.updated |= ! Objects.equals(this.consentRequired, consentRequired);
this.consentRequired = consentRequired;
}
@Override
public Boolean isStandardFlowEnabled() {
return standardFlowEnabled;
}
@Override
public void setStandardFlowEnabled(Boolean standardFlowEnabled) {
this.updated |= ! Objects.equals(this.standardFlowEnabled, standardFlowEnabled);
this.standardFlowEnabled = standardFlowEnabled;
}
@Override
public Boolean isImplicitFlowEnabled() {
return implicitFlowEnabled;
}
@Override
public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
this.updated |= ! Objects.equals(this.implicitFlowEnabled, implicitFlowEnabled);
this.implicitFlowEnabled = implicitFlowEnabled;
}
@Override
public Boolean isDirectAccessGrantsEnabled() {
return directAccessGrantsEnabled;
}
@Override
public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) {
this.updated |= ! Objects.equals(this.directAccessGrantsEnabled, directAccessGrantsEnabled);
this.directAccessGrantsEnabled = directAccessGrantsEnabled;
}
@Override
public Boolean isServiceAccountsEnabled() {
return serviceAccountsEnabled;
}
@Override
public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
this.updated |= ! Objects.equals(this.serviceAccountsEnabled, serviceAccountsEnabled);
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
@Override
public Integer getNodeReRegistrationTimeout() {
return nodeReRegistrationTimeout;
}
@Override
public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) {
this.updated |= ! Objects.equals(this.nodeReRegistrationTimeout, nodeReRegistrationTimeout);
this.nodeReRegistrationTimeout = nodeReRegistrationTimeout;
}
@Override
public void addWebOrigin(String webOrigin) {
updated = true;
this.webOrigins.add(webOrigin);
}
@Override
public void removeWebOrigin(String webOrigin) {
updated |= this.webOrigins.remove(webOrigin);
}
@Override
public void addRedirectUri(String redirectUri) {
this.updated |= ! this.redirectUris.contains(redirectUri);
this.redirectUris.add(redirectUri);
}
@Override
public void removeRedirectUri(String redirectUri) {
updated |= this.redirectUris.remove(redirectUri);
}
@Override
public String getAuthenticationFlowBindingOverride(String binding) {
return authFlowBindings.stream().filter(pair -> Objects.equals(pair.getFirst(), binding)).findFirst()
.map(HotRodPair::getSecond)
.orElse(null);
}
@Override
public Map<String, String> getAuthenticationFlowBindingOverrides() {
return this.authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
}
@Override
public void removeAuthenticationFlowBindingOverride(String binding) {
this.authFlowBindings.stream().filter(pair -> Objects.equals(pair.getFirst(), binding)).findFirst()
.ifPresent(pair -> {
updated = true;
authFlowBindings.remove(pair);
});
}
@Override
public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
this.updated = true;
removeAuthenticationFlowBindingOverride(binding);
this.authFlowBindings.add(new HotRodPair<>(binding, flowId));
}
@Override
public Collection<String> getScopeMappings() {
return scopeMappings;
}
@Override
public void addScopeMapping(String id) {
if (id != null) {
updated = true;
scopeMappings.add(id);
}
}
@Override
public void removeScopeMapping(String id) {
updated |= scopeMappings.remove(id);
}
@Override
public Map<String, Boolean> getClientScopes() {
return this.clientScopes.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
}
@Override
public void setClientScope(String id, Boolean defaultScope) {
if (id != null) {
updated = true;
removeClientScope(id);
this.clientScopes.add(new HotRodPair<>(id, defaultScope));
}
}
@Override
public void removeClientScope(String id) {
this.clientScopes.stream().filter(pair -> Objects.equals(pair.getFirst(), id)).findFirst()
.ifPresent(pair -> {
updated = true;
clientScopes.remove(pair);
});
}
@Override
public Stream<String> getClientScopes(boolean defaultScope) {
return this.clientScopes.stream()
.filter(pair -> Objects.equals(pair.getSecond(), defaultScope))
.map(HotRodPair::getFirst);
}
@Override
public String getRealmId() {
return this.realmId;
}
@Override
public String getId() {
return id;
}
@Override
public void setId(String id) {
if (this.id != null) throw new IllegalStateException("Id cannot be changed");
this.id = id;
this.updated |= id != null;
}
@Override
public void clearUpdatedFlag() {
this.updated = false;
}
@Override
public boolean isUpdated() {
return updated;
}
}

View file

@ -0,0 +1,112 @@
/*
* Copyright 2021 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.client;
import org.infinispan.protostream.annotations.ProtoField;
import org.keycloak.models.map.common.HotRodPair;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public class HotRodProtocolMapperEntity implements MapProtocolMapperEntity {
@ProtoField(number = 1)
public String id;
@ProtoField(number = 2)
public String name;
@ProtoField(number = 3)
public String protocol;
@ProtoField(number = 4)
public String protocolMapper;
// @ProtoField(number = 5, defaultValue = "false")
// public boolean consentRequired;
// @ProtoField(number = 5)
// public String consentText;
@ProtoField(number = 5)
public Set<HotRodPair<String, String>> config = new LinkedHashSet<>();
private boolean updated;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HotRodProtocolMapperEntity entity = (HotRodProtocolMapperEntity) o;
return id.equals(entity.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String getId() {
return id;
}
@Override
public void setId(String id) {
updated |= !Objects.equals(this.id, id);
this.id = id;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
updated |= !Objects.equals(this.name, name);
this.name = name;
}
@Override
public String getProtocolMapper() {
return protocolMapper;
}
@Override
public void setProtocolMapper(String protocolMapper) {
updated |= !Objects.equals(this.protocolMapper, protocolMapper);
this.protocolMapper = protocolMapper;
}
@Override
public Map<String, String> getConfig() {
return config.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond));
}
@Override
public void setConfig(Map<String, String> config) {
updated |= !Objects.equals(this.config, config);
this.config.clear();
config.entrySet().stream().map(entry -> new HotRodPair<>(entry.getKey(), entry.getValue())).forEach(this.config::add);
}
@Override
public boolean isUpdated() {
return updated;
}
}

View file

@ -0,0 +1,51 @@
/*
* Copyright 2021 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.common;
import java.util.List;
import java.util.stream.Stream;
public class HotRodEntityDescriptor<EntityType> {
private final Class<?> modelTypeClass;
private final Class<EntityType> entityTypeClass;
private final List<Class<?>> hotRodClasses;
private final String cacheName;
public HotRodEntityDescriptor(Class<?> modelTypeClass, Class<EntityType> entityTypeClass, List<Class<?>> hotRodClasses, String cacheName) {
this.modelTypeClass = modelTypeClass;
this.entityTypeClass = entityTypeClass;
this.hotRodClasses = hotRodClasses;
this.cacheName = cacheName;
}
public Class<?> getModelTypeClass() {
return modelTypeClass;
}
public Class<EntityType> getEntityTypeClass() {
return entityTypeClass;
}
public Stream<Class<?>> getHotRodClasses() {
return hotRodClasses.stream();
}
public String getCacheName() {
return cacheName;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2021 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.common;
import org.infinispan.protostream.WrappedMessage;
import org.infinispan.protostream.annotations.ProtoField;
public class HotRodPair<T, V> {
@ProtoField(number = 1)
public WrappedMessage firstWrapped;
@ProtoField(number = 2)
public WrappedMessage secondWrapped;
public HotRodPair() {}
public HotRodPair(T first, V second) {
this.firstWrapped = new WrappedMessage(first);
this.secondWrapped = new WrappedMessage(second);
}
public T getFirst() {
return firstWrapped == null ? null : (T) firstWrapped.getValue();
}
public V getSecond() {
return secondWrapped == null ? null : (V) secondWrapped.getValue();
}
public void setFirst(T first) {
this.firstWrapped = new WrappedMessage(first);
}
public void setSecond(V second) {
this.secondWrapped = new WrappedMessage(second);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.models.map.common; package org.keycloak.models.map.common;
import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.query.dsl.Query;
import org.infinispan.rest.RestServer; import org.infinispan.rest.RestServer;
import org.infinispan.rest.configuration.RestServerConfigurationBuilder; import org.infinispan.rest.configuration.RestServerConfigurationBuilder;
import org.infinispan.server.configuration.endpoint.SinglePortServerConfigurationBuilder; import org.infinispan.server.configuration.endpoint.SinglePortServerConfigurationBuilder;
@ -84,4 +85,16 @@ public class HotRodUtils {
HotRodUtils.createHotRodMapStoreServer(new HotRodServer(), hotRodCacheManager, embeddedPort); HotRodUtils.createHotRodMapStoreServer(new HotRodServer(), hotRodCacheManager, embeddedPort);
} }
public static <T> Query<T> paginateQuery(Query<T> query, Integer first, Integer max) {
if (first != null && first > 0) {
query = query.startOffset(first);
}
if (max != null && max >= 0) {
query = query.maxResults(max);
}
return query;
}
} }

View file

@ -19,20 +19,19 @@ package org.keycloak.models.map.common;
import org.infinispan.protostream.GeneratedSchema; import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder; import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
//import org.keycloak.models.map.client.HotRodAttributeEntity; import org.keycloak.models.map.client.HotRodAttributeEntity;
//import org.keycloak.models.map.client.HotRodClientEntity; import org.keycloak.models.map.client.HotRodClientEntity;
//import org.keycloak.models.map.client.HotRodPair; import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
//import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
/** /**
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a> * @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/ */
@AutoProtoSchemaBuilder( @AutoProtoSchemaBuilder(
includeClasses = { includeClasses = {
//HotRodAttributeEntity.class, HotRodAttributeEntity.class,
//HotRodClientEntity.class, HotRodClientEntity.class,
//HotRodProtocolMapperEntity.class, HotRodProtocolMapperEntity.class,
//HotRodPair.class HotRodPair.class
}, },
schemaFileName = "KeycloakHotRodMapStorage.proto", schemaFileName = "KeycloakHotRodMapStorage.proto",
schemaFilePath = "proto/", schemaFilePath = "proto/",

View file

@ -0,0 +1,22 @@
/*
* Copyright 2021 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.common;
public interface Versioned {
int getEntityVersion();
}

View file

@ -26,10 +26,10 @@ import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
//import org.keycloak.models.map.common.HotRodEntityDescriptor; import org.keycloak.models.map.common.HotRodEntityDescriptor;
import org.keycloak.models.map.common.HotRodUtils; import org.keycloak.models.map.common.HotRodUtils;
import org.keycloak.models.map.common.ProtoSchemaInitializer; import org.keycloak.models.map.common.ProtoSchemaInitializer;
//import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory; import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -106,9 +106,9 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
if (configureRemoteCaches) { if (configureRemoteCaches) {
// access the caches to force their creation // access the caches to force their creation
/*HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream() HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
.map(HotRodEntityDescriptor::getCacheName) .map(HotRodEntityDescriptor::getCacheName)
.forEach(remoteCacheManager::getCache);*/ .forEach(remoteCacheManager::getCache);
} }
registerSchemata(ProtoSchemaInitializer.INSTANCE); registerSchemata(ProtoSchemaInitializer.INSTANCE);
@ -133,8 +133,8 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
throw new RuntimeException("Cannot read the cache configuration!", e); throw new RuntimeException("Cannot read the cache configuration!", e);
} }
/*HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream() HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
.map(HotRodEntityDescriptor::getCacheName) .map(HotRodEntityDescriptor::getCacheName)
.forEach(name -> builder.remoteCache(name).configurationURI(uri));*/ .forEach(name -> builder.remoteCache(name).configurationURI(uri));
} }
} }

View file

@ -0,0 +1,190 @@
/*
* Copyright 2021 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.infinispan.client.hotrod.Search;
import org.infinispan.commons.util.CloseableIterator;
import org.infinispan.query.dsl.Query;
import org.infinispan.query.dsl.QueryFactory;
import org.jboss.logging.Logger;
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.HotRodEntityDescriptor;
import org.keycloak.models.map.common.StringKeyConvertor;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
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.MapFieldPredicates;
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static org.keycloak.models.map.common.HotRodUtils.paginateQuery;
import static org.keycloak.utils.StreamsUtil.closing;
public class HotRodMapStorage<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class);
private final RemoteCache<K, V> remoteCache;
private final StringKeyConvertor<K> keyConvertor;
private final HotRodEntityDescriptor<V> storedEntityDescriptor;
private final DeepCloner cloner;
public HotRodMapStorage(RemoteCache<K, V> remoteCache, StringKeyConvertor<K> keyConvertor, HotRodEntityDescriptor<V> storedEntityDescriptor, DeepCloner cloner) {
this.remoteCache = remoteCache;
this.keyConvertor = keyConvertor;
this.storedEntityDescriptor = storedEntityDescriptor;
this.cloner = cloner;
}
@Override
public V create(V value) {
K key = keyConvertor.fromStringSafe(value.getId());
if (key == null) {
key = keyConvertor.yieldNewUniqueKey();
value = cloner.from(keyConvertor.keyToString(key), value);
}
remoteCache.putIfAbsent(key, value);
return value;
}
@Override
public V read(String key) {
Objects.requireNonNull(key, "Key must be non-null");
K k = keyConvertor.fromStringSafe(key);
return remoteCache.get(k);
}
@Override
public V update(V value) {
K key = keyConvertor.fromStringSafe(value.getId());
return remoteCache.replace(key, value);
}
@Override
public boolean delete(String key) {
K k = keyConvertor.fromStringSafe(key);
return remoteCache.remove(k) != null;
}
private static String toOrderString(QueryParameters.OrderBy<?> orderBy) {
SearchableModelField<?> field = orderBy.getModelField();
String modelFieldName = IckleQueryMapModelCriteriaBuilder.getFieldName(field);
String orderString = orderBy.getOrder().equals(QueryParameters.Order.ASCENDING) ? "ASC" : "DESC";
return modelFieldName + " " + orderString;
}
@Override
public Stream<V> read(QueryParameters<M> queryParameters) {
IckleQueryMapModelCriteriaBuilder<K, V, M> iqmcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createCriteriaBuilder());
String queryString = iqmcb.getIckleQuery();
if (!queryParameters.getOrderBy().isEmpty()) {
queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodMapStorage::toOrderString)
.collect(Collectors.joining(", "));
}
LOG.tracef("Executing read Ickle query: %s", queryString);
QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
Query<V> query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
queryParameters.getLimit());
query.setParameters(iqmcb.getParameters());
CloseableIterator<V> iterator = query.iterator();
return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false))
.onClose(iterator::close);
}
@Override
public long getCount(QueryParameters<M> queryParameters) {
IckleQueryMapModelCriteriaBuilder<K, V, M> iqmcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createCriteriaBuilder());
String queryString = iqmcb.getIckleQuery();
LOG.tracef("Executing count Ickle query: %s", queryString);
QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
Query<V> query = queryFactory.create(queryString);
query.setParameters(iqmcb.getParameters());
return query.execute().hitCount().orElse(0);
}
@Override
public long delete(QueryParameters<M> queryParameters) {
IckleQueryMapModelCriteriaBuilder<K, V, M> iqmcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(createCriteriaBuilder());
String queryString = "SELECT id " + iqmcb.getIckleQuery();
if (!queryParameters.getOrderBy().isEmpty()) {
queryString += " ORDER BY " + queryParameters.getOrderBy().stream().map(HotRodMapStorage::toOrderString)
.collect(Collectors.joining(", "));
}
LOG.tracef("Executing delete Ickle query: %s", queryString);
QueryFactory queryFactory = Search.getQueryFactory(remoteCache);
Query<V> query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(),
queryParameters.getLimit());
query.setParameters(iqmcb.getParameters());
AtomicLong result = new AtomicLong();
CloseableIterator<V> iterator = query.iterator();
StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)
.peek(e -> result.incrementAndGet())
.map(AbstractEntity::getId)
.forEach(this::delete);
iterator.close();
return result.get();
}
public IckleQueryMapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() {
return new IckleQueryMapModelCriteriaBuilder<>();
}
@Override
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
Map<SearchableModelField<? super M>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, V, M>> fieldPredicates = MapFieldPredicates.getPredicates((Class<M>) storedEntityDescriptor.getModelTypeClass());
return new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner, fieldPredicates);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2021 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.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.HotRodEntityDescriptor;
import org.keycloak.models.map.common.StringKeyConvertor;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.connections.HotRodConnectionProvider;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProviderFactory;
public class HotRodMapStorageProvider implements MapStorageProvider {
private final HotRodMapStorageProviderFactory factory;
private final HotRodConnectionProvider connectionProvider;
private final DeepCloner cloner;
public HotRodMapStorageProvider(HotRodMapStorageProviderFactory factory, HotRodConnectionProvider connectionProvider, DeepCloner cloner) {
this.factory = factory;
this.connectionProvider = connectionProvider;
this.cloner = cloner;
}
@Override
public <V extends AbstractEntity, M> MapStorage<V, M> getStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
HotRodMapStorage storage = getHotRodStorage(modelType, flags);
return storage;
}
@SuppressWarnings("unchecked")
public <V extends AbstractEntity & UpdatableEntity, M> HotRodMapStorage<String, V, M> getHotRodStorage(Class<M> modelType, MapStorageProviderFactory.Flag... flags) {
HotRodEntityDescriptor<V> entityDescriptor = (HotRodEntityDescriptor<V>) factory.getEntityDescriptor(modelType);
return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2021 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.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.map.client.HotRodAttributeEntity;
import org.keycloak.models.map.client.HotRodClientEntity;
import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
import org.keycloak.models.map.common.HotRodPair;
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.common.HotRodEntityDescriptor;
import org.keycloak.models.map.connections.HotRodConnectionProvider;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProviderFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory<MapStorageProvider>, MapStorageProviderFactory, EnvironmentDependentProviderFactory {
public static final String PROVIDER_ID = "hotrod";
private static final Logger LOG = Logger.getLogger(HotRodMapStorageProviderFactory.class);
private final static DeepCloner CLONER = new DeepCloner.Builder()
.constructorDC(MapClientEntity.class, HotRodClientEntity::new)
.constructor(MapProtocolMapperEntity.class, HotRodProtocolMapperEntity::new)
.build();
public static final Map<Class<?>, HotRodEntityDescriptor<?>> ENTITY_DESCRIPTOR_MAP = new HashMap<>();
static {
// Clients descriptor
ENTITY_DESCRIPTOR_MAP.put(ClientModel.class,
new HotRodEntityDescriptor<>(ClientModel.class,
MapClientEntity.class,
Arrays.asList(HotRodClientEntity.class, HotRodAttributeEntity.class, HotRodProtocolMapperEntity.class, HotRodPair.class),
"clients"));
}
@Override
public MapStorageProvider create(KeycloakSession session) {
HotRodConnectionProvider cacheProvider = session.getProvider(HotRodConnectionProvider.class);
if (cacheProvider == null) {
throw new IllegalStateException("Cannot find HotRodConnectionProvider interface implementation");
}
return new HotRodMapStorageProvider(this, cacheProvider, CLONER);
}
public HotRodEntityDescriptor<?> getEntityDescriptor(Class<?> c) {
return ENTITY_DESCRIPTOR_MAP.get(c);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
@Override
public String getHelpText() {
return "HotRod client storage";
}
}

View file

@ -0,0 +1,187 @@
/*
* Copyright 2021 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.keycloak.models.ClientModel;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.storage.SearchableModelField;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.C;
import static org.keycloak.models.map.storage.hotRod.IckleQueryOperators.findAvailableNamedParam;
public class IckleQueryMapModelCriteriaBuilder<K, V extends AbstractEntity, M> implements ModelCriteriaBuilder<M, IckleQueryMapModelCriteriaBuilder<K, V, M>> {
private static final int INITIAL_BUILDER_CAPACITY = 250;
private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
private final Map<String, Object> parameters;
public static final Map<SearchableModelField<?>, String> INFINISPAN_NAME_OVERRIDES = new HashMap<>();
static {
INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.SCOPE_MAPPING_ROLE, "scopeMappings");
INFINISPAN_NAME_OVERRIDES.put(ClientModel.SearchableFields.ATTRIBUTE, "attributes");
}
public IckleQueryMapModelCriteriaBuilder(StringBuilder whereClauseBuilder, Map<String, Object> parameters) {
this.whereClauseBuilder.append(whereClauseBuilder);
this.parameters = parameters;
}
public IckleQueryMapModelCriteriaBuilder() {
this.parameters = new HashMap<>();
}
public static String getFieldName(SearchableModelField<?> modelField) {
return INFINISPAN_NAME_OVERRIDES.getOrDefault(modelField, modelField.getName());
}
private static boolean notEmpty(StringBuilder builder) {
return builder.length() != 0;
}
@Override
public IckleQueryMapModelCriteriaBuilder<K, V, M> compare(SearchableModelField<? super M> modelField, Operator op, Object... value) {
StringBuilder newBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
newBuilder.append("(");
if (notEmpty(whereClauseBuilder)) {
newBuilder.append(whereClauseBuilder).append(" AND (");
}
Map<String, Object> newParameters = new HashMap<>(parameters);
newBuilder.append(IckleQueryWhereClauses.produceWhereClause(modelField, op, value, newParameters));
if (notEmpty(whereClauseBuilder)) {
newBuilder.append(")");
}
return new IckleQueryMapModelCriteriaBuilder<>(newBuilder.append(")"), newParameters);
}
private StringBuilder joinBuilders(IckleQueryMapModelCriteriaBuilder<K, V, M>[] builders, String delimiter) {
return new StringBuilder(INITIAL_BUILDER_CAPACITY).append("(").append(Arrays.stream(builders)
.map(IckleQueryMapModelCriteriaBuilder::getWhereClauseBuilder)
.filter(IckleQueryMapModelCriteriaBuilder::notEmpty)
.collect(Collectors.joining(delimiter))).append(")");
}
private Map<String, Object> joinParameters(IckleQueryMapModelCriteriaBuilder<K, V, M>[] builders) {
return Arrays.stream(builders)
.map(IckleQueryMapModelCriteriaBuilder::getParameters)
.map(Map::entrySet)
.flatMap(Collection::stream)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
@SuppressWarnings("unchecked")
private IckleQueryMapModelCriteriaBuilder<K, V, M>[] resolveNamedQueryConflicts(IckleQueryMapModelCriteriaBuilder<K, V, M>[] builders) {
final Set<String> existingKeys = new HashSet<>();
return Arrays.stream(builders).map(builder -> {
Map<String, Object> oldParameters = builder.getParameters();
if (oldParameters.keySet().stream().noneMatch(existingKeys::contains)) {
existingKeys.addAll(oldParameters.keySet());
return builder;
}
String newWhereClause = builder.getWhereClauseBuilder().toString();
Map<String, Object> newParameters = new HashMap<>();
for (String key : oldParameters.keySet()) {
if (existingKeys.contains(key)) {
// resolve conflict
String newNamedParameter = findAvailableNamedParam(existingKeys, key + "n");
newParameters.put(newNamedParameter, oldParameters.get(key));
newWhereClause = newWhereClause.replace(key, newNamedParameter);
existingKeys.add(newNamedParameter);
} else {
newParameters.put(key, oldParameters.get(key));
existingKeys.add(key);
}
}
return new IckleQueryMapModelCriteriaBuilder<>(new StringBuilder(newWhereClause), newParameters);
}).toArray(IckleQueryMapModelCriteriaBuilder[]::new);
}
@Override
public IckleQueryMapModelCriteriaBuilder<K, V, M> and(IckleQueryMapModelCriteriaBuilder<K, V, M>... builders) {
if (builders.length == 0) {
return new IckleQueryMapModelCriteriaBuilder<>();
}
builders = resolveNamedQueryConflicts(builders);
return new IckleQueryMapModelCriteriaBuilder<>(joinBuilders(builders, " AND "),
joinParameters(builders));
}
@Override
public IckleQueryMapModelCriteriaBuilder<K, V, M> or(IckleQueryMapModelCriteriaBuilder<K, V, M>... builders) {
if (builders.length == 0) {
return new IckleQueryMapModelCriteriaBuilder<>();
}
builders = resolveNamedQueryConflicts(builders);
return new IckleQueryMapModelCriteriaBuilder<>(joinBuilders(builders, " OR "),
joinParameters(builders));
}
@Override
public IckleQueryMapModelCriteriaBuilder<K, V, M> not(IckleQueryMapModelCriteriaBuilder<K, V, M> builder) {
StringBuilder newBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY);
StringBuilder originalBuilder = builder.getWhereClauseBuilder();
if (originalBuilder.length() != 0) {
newBuilder.append("not").append(originalBuilder);
}
return new IckleQueryMapModelCriteriaBuilder<>(newBuilder, builder.getParameters());
}
private StringBuilder getWhereClauseBuilder() {
return whereClauseBuilder;
}
/**
*
* @return Ickle query that represents this QueryBuilder
*/
public String getIckleQuery() {
return "FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity " + C + ((whereClauseBuilder.length() != 0) ? " WHERE " + whereClauseBuilder : "");
}
/**
* Ickle queries are created using named parameters to avoid query injections; this method provides mapping
* between parameter names and corresponding values
*
* @return Mapping from name of the parameter to value
*/
public Map<String, Object> getParameters() {
return parameters;
}
}

View file

@ -0,0 +1,180 @@
/*
* Copyright 2021 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.keycloak.models.map.storage.ModelCriteriaBuilder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* This class provides knowledge on how to build Ickle query where clauses for specified {@link ModelCriteriaBuilder.Operator}.
* <p/>
* For example,
* <p/>
* for operator {@link ModelCriteriaBuilder.Operator.EQ} we concatenate left operand and right operand with equal sign:
* {@code fieldName = :parameterName}
* <p/>
* however, for operator {@link ModelCriteriaBuilder.Operator.EXISTS} we add following:
* <p/>
* {@code fieldName IS NOT NULL AND fieldName IS NOT EMPTY"}.
*
* For right side operands we use named parameters to avoid injection attacks. Mapping between named parameter and
* corresponding value is then saved into {@code Map<String, Object>} that is passed to each {@link ExpressionCombinator}.
*/
public class IckleQueryOperators {
private static final String UNWANTED_CHARACTERS_REGEX = "[^a-zA-Z\\d]";
public static final String C = "c";
private static final Map<ModelCriteriaBuilder.Operator, String> OPERATOR_TO_STRING = new HashMap<>();
private static final Map<ModelCriteriaBuilder.Operator, ExpressionCombinator> OPERATOR_TO_EXPRESSION_COMBINATORS = new HashMap<>();
static {
OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.IN, IckleQueryOperators::in);
OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.EXISTS, IckleQueryOperators::exists);
OPERATOR_TO_EXPRESSION_COMBINATORS.put(ModelCriteriaBuilder.Operator.NOT_EXISTS, IckleQueryOperators::notExists);
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.EQ, "=");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.NE, "!=");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.LT, "<");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.LE, "<=");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.GT, ">");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.GE, ">=");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.LIKE, "LIKE");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.ILIKE, "LIKE");
OPERATOR_TO_STRING.put(ModelCriteriaBuilder.Operator.IN, "IN");
}
@FunctionalInterface
private interface ExpressionCombinator {
/**
* Produces an Ickle query where clause for obtained parameters
*
* @param fieldName left side operand
* @param values right side operands
* @param parameters mapping between named parameters and actual parameter values
* @return resulting string that will be part of resulting
*/
String combine(String fieldName, Object[] values, Map<String, Object> parameters);
}
private static String exists(String modelField, Object[] values, Map<String, Object> parameters) {
String field = C + "." + modelField;
return field + " IS NOT NULL AND " + field + " IS NOT EMPTY";
}
private static String notExists(String modelField, Object[] values, Map<String, Object> parameters) {
String field = C + "." + modelField;
return field + " IS NULL OR " + field + " IS EMPTY";
}
private static String in(String modelField, Object[] values, Map<String, Object> parameters) {
if (values == null || values.length == 0) {
return "false";
}
final Collection<?> operands;
if (values.length == 1) {
final Object value0 = values[0];
if (value0 instanceof Collection) {
operands = (Collection) value0;
} else if (value0 instanceof Stream) {
try (Stream valueS = (Stream) value0) {
operands = (Set) valueS.collect(Collectors.toSet());
}
} else {
operands = Collections.singleton(value0);
}
} else {
operands = new HashSet<>(Arrays.asList(values));
}
return C + "." + modelField + " IN (" + operands.stream()
.map(operand -> {
String namedParam = findAvailableNamedParam(parameters.keySet(), modelField);
parameters.put(namedParam, operand);
return ":" + namedParam;
})
.collect(Collectors.joining(", ")) +
")";
}
private static String removeForbiddenCharactersFromNamedParameter(String name) {
return name.replaceAll(UNWANTED_CHARACTERS_REGEX, "");
}
/**
* Maps {@code namePrefix} to next available parameter name. For example, if {@code namePrefix == "id"}
* and {@code existingNames} set already contains {@code id0} and {@code id1} it returns {@code id2}.
*
* This method is used for computing available names for name query parameters
*
* @param existingNames set of parameter names that are already used in this Ickle query
* @param namePrefix name of the parameter
* @return next available parameter name
*/
public static String findAvailableNamedParam(Set<String> existingNames, String namePrefix) {
String namePrefixCleared = removeForbiddenCharactersFromNamedParameter(namePrefix);
return IntStream.iterate(0, i -> i + 1)
.boxed()
.map(num -> namePrefixCleared + num)
.filter(name -> !existingNames.contains(name))
.findFirst().orElseThrow(() -> new IllegalArgumentException("Cannot create Parameter name for " + namePrefix));
}
private static ExpressionCombinator singleValueOperator(ModelCriteriaBuilder.Operator op) {
return (modelFieldName, values, parameters) -> {
if (values.length != 1) throw new RuntimeException("Invalid arguments, expected (" + modelFieldName + "), got: " + Arrays.toString(values));
String namedParameter = findAvailableNamedParam(parameters.keySet(), modelFieldName);
parameters.put(namedParameter, values[0]);
return C + "." + modelFieldName + " " + IckleQueryOperators.operatorToString(op) + " :" + namedParameter;
};
}
private static String operatorToString(ModelCriteriaBuilder.Operator op) {
return OPERATOR_TO_STRING.get(op);
}
private static ExpressionCombinator operatorToExpressionCombinator(ModelCriteriaBuilder.Operator op) {
return OPERATOR_TO_EXPRESSION_COMBINATORS.getOrDefault(op, singleValueOperator(op));
}
/**
* Provides a string containing where clause for given operator, field name and values
*
* @param op operator
* @param filedName field name
* @param values values
* @param parameters mapping between named parameters and their values
* @return where clause
*/
public static String combineExpressions(ModelCriteriaBuilder.Operator op, String filedName, Object[] values, Map<String, Object> parameters) {
return operatorToExpressionCombinator(op).combine(filedName, values, parameters);
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2021 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.keycloak.models.ClientModel;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.storage.SearchableModelField;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* This class provides knowledge on how to build Ickle query where clauses for specified {@link SearchableModelField}.
*
* For example,
* <p/>
* for {@link ClientModel.SearchableFields.CLIENT_ID} we use {@link IckleQueryOperators.ExpressionCombinator} for
* obtained {@link ModelCriteriaBuilder.Operator} and use it with field name corresponding to {@link ClientModel.SearchableFields.CLIENT_ID}
* <p/>
* however, for {@link ClientModel.SearchableFields.ATTRIBUTE} we need to compare attribute name and attribute value
* so we create where clause similar to the following:
* {@code (attributes.name = :attributeName) AND ( attributes.value = :attributeValue )}
*
*
*/
public class IckleQueryWhereClauses {
private static final Map<SearchableModelField<?>, WhereClauseProducer> WHERE_CLAUSE_PRODUCER_OVERRIDES = new HashMap<>();
static {
WHERE_CLAUSE_PRODUCER_OVERRIDES.put(ClientModel.SearchableFields.ATTRIBUTE, IckleQueryWhereClauses::whereClauseForClientsAttributes);
}
@FunctionalInterface
private interface WhereClauseProducer {
String produceWhereClause(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters);
}
private static String produceWhereClause(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters) {
return IckleQueryOperators.combineExpressions(op, modelFieldName, values, parameters);
}
private static WhereClauseProducer whereClauseProducerForModelField(SearchableModelField<?> modelField) {
return WHERE_CLAUSE_PRODUCER_OVERRIDES.getOrDefault(modelField, IckleQueryWhereClauses::produceWhereClause);
}
/**
* Produces where clause for given {@link SearchableModelField}, {@link ModelCriteriaBuilder.Operator} and values
*
* @param modelField model field
* @param op operator
* @param values searched values
* @param parameters mapping between named parameters and corresponding values
* @return resulting where clause
*/
public static String produceWhereClause(SearchableModelField<?> modelField, ModelCriteriaBuilder.Operator op,
Object[] values, Map<String, Object> parameters) {
return whereClauseProducerForModelField(modelField)
.produceWhereClause(IckleQueryMapModelCriteriaBuilder.getFieldName(modelField), op, values, parameters);
}
private static String whereClauseForClientsAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map<String, Object> parameters) {
if (values == null || values.length != 2) {
throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected attribute_name-value pair, got: " + Arrays.toString(values));
}
final Object attrName = values[0];
if (! (attrName instanceof String)) {
throw new CriterionNotSupportedException(ClientModel.SearchableFields.ATTRIBUTE, op, "Invalid arguments, expected (String attribute_name), got: " + Arrays.toString(values));
}
String attrNameS = (String) attrName;
Object[] realValues = new Object[values.length - 1];
System.arraycopy(values, 1, realValues, 0, values.length - 1);
// Clause for searching attribute name
String nameClause = IckleQueryOperators.combineExpressions(ModelCriteriaBuilder.Operator.EQ, modelFieldName + ".name", new Object[]{attrNameS}, parameters);
// Clause for searching attribute value
String valueClause = IckleQueryOperators.combineExpressions(op, modelFieldName + ".values", realValues, parameters);
return "(" + nameClause + ")" + " AND " + "(" + valueClause + ")";
}
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2020 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.
#
org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory

View file

@ -0,0 +1,71 @@
/*
* Copyright 2021 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.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.map.client.HotRodClientEntity;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.keycloak.models.ClientModel.SearchableFields.CLIENT_ID;
import static org.keycloak.models.ClientModel.SearchableFields.ID;
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
public class IckleQueryMapModelCriteriaBuilderTest {
@Test
public void testSimpleIckleQuery() {
IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntity, ClientModel> v = new IckleQueryMapModelCriteriaBuilder<>();
IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntity, ClientModel> mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3);
assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE (c.clientId = :clientId0)")));
assertThat(mcb.getParameters().entrySet(), hasSize(1));
assertThat(mcb.getParameters(), hasEntry("clientId0", 3));
mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 4)
.compare(ID, ModelCriteriaBuilder.Operator.EQ, 5);
assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE ((c.clientId = :clientId0) AND (c.id = :id0))")));
assertThat(mcb.getParameters().entrySet(), hasSize(2));
assertThat(mcb.getParameters(), allOf(hasEntry("clientId0", 4), hasEntry("id0", 5)));
}
@Test
public void testSimpleIckleQueryFlashedFromDefault() {
DefaultModelCriteria<ClientModel> v = criteria();
IckleQueryMapModelCriteriaBuilder<String, HotRodClientEntity, ClientModel> mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 3).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>());
assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE (c.clientId = :clientId0)")));
assertThat(mcb.getParameters().entrySet(), hasSize(1));
assertThat(mcb.getParameters(), hasEntry("clientId0", 3));
mcb = v.compare(CLIENT_ID, ModelCriteriaBuilder.Operator.EQ, 4)
.compare(ID, ModelCriteriaBuilder.Operator.EQ, 5).flashToModelCriteriaBuilder(new IckleQueryMapModelCriteriaBuilder<>());
assertThat(mcb.getIckleQuery(), is(equalTo("FROM org.keycloak.models.map.storage.hotrod.HotRodClientEntity c WHERE ((c.clientId = :clientId0) AND (c.id = :id0))")));
assertThat(mcb.getParameters().entrySet(), hasSize(2));
assertThat(mcb.getParameters(), allOf(hasEntry("clientId0", 4), hasEntry("id0", 5)));
}
}

View file

@ -0,0 +1,73 @@
package org.keycloak.models.map.storage.chm;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.QueryParameters;
import java.util.stream.Stream;
public interface ConcurrentHashMapCrudOperations<V extends AbstractEntity & UpdatableEntity, M> {
/**
* Creates an object in the store. ID of the {@code value} may be prescribed in id of the {@code value}.
* If the id is {@code null} or its format is not matching the store internal format for ID, then
* the {@code value}'s ID will be generated and returned in the id of the return value.
* @param value Entity to create in the store
* @throws NullPointerException if {@code value} is {@code null}
* @see AbstractEntity#getId()
* @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}
*/
V create(V value);
/**
* Returns object with the given {@code key} from the storage or {@code null} if object does not exist.
* <br>
* TODO: Consider returning {@code Optional<V>} instead.
* @param key Key of the object. Must not be {@code null}.
* @return See description
* @throws NullPointerException if the {@code key} is {@code null}
*/
public V read(String key);
/**
* Updates the object with the key of the {@code value}'s ID in the storage if it already exists.
*
* @param value Updated value
* @throws NullPointerException if the object or its {@code id} is {@code null}
* @see AbstractEntity#getId()
*/
V update(V value);
/**
* Deletes object with the given {@code key} from the storage, if exists, no-op otherwise.
* @param key
* @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise.
*/
boolean delete(String key);
/**
* Deletes objects that match the given criteria.
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
* @return Number of removed objects (might return {@code -1} if not supported)
*/
long delete(QueryParameters<M> queryParameters);
/**
* Returns stream of objects satisfying given {@code criteria} from the storage.
* The criteria are specified in the given criteria builder based on model properties.
*
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
* @return Stream of objects. Never returns {@code null}.
*/
Stream<V> read(QueryParameters<M> queryParameters);
/**
* Returns the number of objects satisfying given {@code criteria} from the storage.
* The criteria are specified in the given criteria builder based on model properties.
*
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
* @return Number of objects. Never returns {@code null}.
*/
long getCount(QueryParameters<M> queryParameters);
}

View file

@ -32,8 +32,9 @@ import java.util.stream.Stream;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.utils.StreamsUtil; import org.keycloak.storage.SearchableModelField;
public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<V, M> { public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity & UpdatableEntity, M> implements MapKeycloakTransaction<V, M> {
@ -42,18 +43,20 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
protected boolean active; protected boolean active;
protected boolean rollback; protected boolean rollback;
protected final Map<String, MapTaskWithValue> tasks = new LinkedHashMap<>(); protected final Map<String, MapTaskWithValue> tasks = new LinkedHashMap<>();
protected final ConcurrentHashMapStorage<K, V, M> map; protected final ConcurrentHashMapCrudOperations<V, M> map;
protected final StringKeyConvertor<K> keyConvertor; protected final StringKeyConvertor<K> keyConvertor;
protected final DeepCloner cloner; protected final DeepCloner cloner;
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
enum MapOperation { enum MapOperation {
CREATE, UPDATE, DELETE, CREATE, UPDATE, DELETE,
} }
public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapStorage<K, V, M> map, StringKeyConvertor<K> keyConvertor, DeepCloner cloner) { public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations<V, M> map, StringKeyConvertor<K> keyConvertor, DeepCloner cloner, Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates) {
this.map = map; this.map = map;
this.keyConvertor = keyConvertor; this.keyConvertor = keyConvertor;
this.cloner = cloner; this.cloner = cloner;
this.fieldPredicates = fieldPredicates;
} }
@Override @Override
@ -95,6 +98,10 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
return active; return active;
} }
private MapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() {
return new MapModelCriteriaBuilder<K, V, M>(keyConvertor, fieldPredicates);
}
/** /**
* Adds a given task if not exists for the given key * Adds a given task if not exists for the given key
*/ */
@ -168,7 +175,7 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
@Override @Override
public Stream<V> read(QueryParameters<M> queryParameters) { public Stream<V> read(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder(); DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder();
MapModelCriteriaBuilder<K,V,M> mapMcb = mcb.flashToModelCriteriaBuilder(map.createCriteriaBuilder()); MapModelCriteriaBuilder<K,V,M> mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super V> filterOutAllBulkDeletedObjects = tasks.values().stream() Predicate<? super V> filterOutAllBulkDeletedObjects = tasks.values().stream()
.filter(BulkDeleteOperation.class::isInstance) .filter(BulkDeleteOperation.class::isInstance)
@ -196,7 +203,7 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
} }
return StreamsUtil.paginatedStream(res, queryParameters.getOffset(), queryParameters.getLimit()); return res;
} }
@Override @Override
@ -246,7 +253,6 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
return true; return true;
} }
@Override @Override
public long delete(QueryParameters<M> queryParameters) { public long delete(QueryParameters<M> queryParameters) {
log.tracef("Adding operation DELETE_BULK"); log.tracef("Adding operation DELETE_BULK");
@ -401,7 +407,7 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
public Predicate<V> getFilterForNonDeletedObjects() { public Predicate<V> getFilterForNonDeletedObjects() {
DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder(); DefaultModelCriteria<M> mcb = queryParameters.getModelCriteriaBuilder();
MapModelCriteriaBuilder<K,V,M> mmcb = mcb.flashToModelCriteriaBuilder(map.createCriteriaBuilder()); MapModelCriteriaBuilder<K,V,M> mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder());
Predicate<? super V> entityFilter = mmcb.getEntityFilter(); Predicate<? super V> entityFilter = mmcb.getEntityFilter();
Predicate<? super K> keyFilter = mmcb.getKeyFilter(); Predicate<? super K> keyFilter = mmcb.getKeyFilter();

View file

@ -49,7 +49,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
* *
* @author hmlnarik * @author hmlnarik
*/ */
public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M> { public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEntity, M> implements MapStorage<V, M>, ConcurrentHashMapCrudOperations<V, M> {
protected final ConcurrentMap<K, V> store = new ConcurrentHashMap<>(); protected final ConcurrentMap<K, V> store = new ConcurrentHashMap<>();
@ -64,15 +64,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
this.cloner = cloner; this.cloner = cloner;
} }
/** @Override
* Creates an object in the store. ID of the {@code value} may be prescribed in id of the {@code value}.
* If the id is {@code null} or its format is not matching the store internal format for ID, then
* the {@code value}'s ID will be generated and returned in the id of the return value.
* @param value Entity to create in the store
* @throws NullPointerException if {@code value} is {@code null}
* @see AbstractEntity#getId()
* @return Entity representing the {@code value} in the store. It may or may not be the same instance as {@code value}
*/
public V create(V value) { public V create(V value) {
K key = keyConvertor.fromStringSafe(value.getId()); K key = keyConvertor.fromStringSafe(value.getId());
if (key == null) { if (key == null) {
@ -83,47 +75,26 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
return value; return value;
} }
/** @Override
* Returns object with the given {@code key} from the storage or {@code null} if object does not exist.
* <br>
* TODO: Consider returning {@code Optional<V>} instead.
* @param key Key of the object. Must not be {@code null}.
* @return See description
* @throws NullPointerException if the {@code key} is {@code null}
*/
public V read(String key) { public V read(String key) {
Objects.requireNonNull(key, "Key must be non-null"); Objects.requireNonNull(key, "Key must be non-null");
K k = keyConvertor.fromStringSafe(key); K k = keyConvertor.fromStringSafe(key);
return store.get(k); return store.get(k);
} }
/** @Override
* Updates the object with the key of the {@code value}'s ID in the storage if it already exists.
*
* @param value Updated value
* @throws NullPointerException if the object or its {@code id} is {@code null}
* @see AbstractEntity#getId()
*/
public V update(V value) { public V update(V value) {
K key = getKeyConvertor().fromStringSafe(value.getId()); K key = getKeyConvertor().fromStringSafe(value.getId());
return store.replace(key, value); return store.replace(key, value);
} }
/** @Override
* Deletes object with the given {@code key} from the storage, if exists, no-op otherwise.
* @param key
* @return Returns {@code true} if the object has been deleted or result cannot be determined, {@code false} otherwise.
*/
public boolean delete(String key) { public boolean delete(String key) {
K k = getKeyConvertor().fromStringSafe(key); K k = getKeyConvertor().fromStringSafe(key);
return store.remove(k) != null; return store.remove(k) != null;
} }
/** @Override
* Deletes objects that match the given criteria.
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
* @return Number of removed objects (might return {@code -1} if not supported)
*/
public long delete(QueryParameters<M> queryParameters) { public long delete(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder(); DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
@ -158,7 +129,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) { public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<V, M> sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); MapKeycloakTransaction<V, M> sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner) : sessionTransaction; return sessionTransaction == null ? new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner, fieldPredicates) : sessionTransaction;
} }
public MapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() { public MapModelCriteriaBuilder<K, V, M> createCriteriaBuilder() {
@ -169,13 +140,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
return keyConvertor; return keyConvertor;
} }
/** @Override
* Returns stream of objects satisfying given {@code criteria} from the storage.
* The criteria are specified in the given criteria builder based on model properties.
*
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
* @return Stream of objects. Never returns {@code null}.
*/
public Stream<V> read(QueryParameters<M> queryParameters) { public Stream<V> read(QueryParameters<M> queryParameters) {
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder(); DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
@ -188,18 +153,17 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
Predicate<? super K> keyFilter = mcb.getKeyFilter(); Predicate<? super K> keyFilter = mcb.getKeyFilter();
Predicate<? super V> entityFilter = mcb.getEntityFilter(); Predicate<? super V> entityFilter = mcb.getEntityFilter();
stream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue())); Stream<V> valueStream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue()))
.map(Map.Entry::getValue);
return stream.map(Map.Entry::getValue); if (!queryParameters.getOrderBy().isEmpty()) {
valueStream = valueStream.sorted(MapFieldPredicates.getComparator(queryParameters.getOrderBy().stream()));
}
return paginatedStream(valueStream, queryParameters.getOffset(), queryParameters.getLimit());
} }
/** @Override
* Returns the number of objects satisfying given {@code criteria} from the storage.
* The criteria are specified in the given criteria builder based on model properties.
*
* @param queryParameters parameters for the query like firstResult, maxResult, requested ordering, etc.
* @return Number of objects. Never returns {@code null}.
*/
public long getCount(QueryParameters<M> queryParameters) { public long getCount(QueryParameters<M> queryParameters) {
return read(queryParameters).count(); return read(queryParameters).count();
} }

View file

@ -25,9 +25,13 @@ import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator; import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder.UpdatePredicatesFunc;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity; import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity;
import org.keycloak.models.map.userSession.MapUserSessionEntity; import org.keycloak.models.map.userSession.MapUserSessionEntity;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -47,8 +51,14 @@ public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapSto
private final MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr; private final MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr;
public Transaction(MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr, StringKeyConvertor<K> keyConvertor, DeepCloner cloner) { public Transaction(MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr,
super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner); StringKeyConvertor<K> keyConvertor,
DeepCloner cloner,
Map<SearchableModelField<? super UserSessionModel>,
UpdatePredicatesFunc<K,
MapUserSessionEntity,
UserSessionModel>> fieldPredicates) {
super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner, fieldPredicates);
this.clientSessionTr = clientSessionTr; this.clientSessionTr = clientSessionTr;
} }
@ -82,6 +92,6 @@ public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapSto
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> createTransaction(KeycloakSession session) { public MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> sessionTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class);
return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor(), cloner) : sessionTransaction; return sessionTransaction == null ? new Transaction(clientSessionStore.createTransaction(session), clientSessionStore.getKeyConvertor(), cloner, fieldPredicates) : sessionTransaction;
} }
} }

View file

@ -35,6 +35,6 @@
<module>infinispan</module> <module>infinispan</module>
<module>map</module> <module>map</module>
<module>build-processor</module> <module>build-processor</module>
<module>hot-rod</module> <module>map-hot-rod</module>
</modules> </modules>
</project> </project>

15
pom.xml
View file

@ -909,6 +909,21 @@
<artifactId>infinispan-jboss-marshalling</artifactId> <artifactId>infinispan-jboss-marshalling</artifactId>
<version>${infinispan.version}</version> <version>${infinispan.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-client-hotrod</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-query-dsl</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>
<artifactId>infinispan-remote-query-client</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.infinispan</groupId> <groupId>org.infinispan</groupId>
<artifactId>infinispan-server-core</artifactId> <artifactId>infinispan-server-core</artifactId>

View file

@ -255,6 +255,8 @@
<argument>myuser</argument> <argument>myuser</argument>
<argument>-p</argument> <argument>-p</argument>
<argument>"qwer1234!"</argument> <argument>"qwer1234!"</argument>
<argument>-g</argument>
<argument>admin</argument>
</arguments> </arguments>
</configuration> </configuration>
</execution> </execution>

View file

@ -141,6 +141,10 @@ public class AuthServerTestEnricher {
public static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc"; public static final String AUTH_SERVER_CROSS_DC_PROPERTY = "auth.server.crossdc";
public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false")); public static final boolean AUTH_SERVER_CROSS_DC = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CROSS_DC_PROPERTY, "false"));
public static final String HOT_ROD_STORE_ENABLED_PROPERTY = "hotrod.store.enabled";
public static final boolean HOT_ROD_STORE_ENABLED = Boolean.parseBoolean(System.getProperty(HOT_ROD_STORE_ENABLED_PROPERTY, "false"));
public static final String AUTH_SERVER_HOME_PROPERTY = "auth.server.home"; public static final String AUTH_SERVER_HOME_PROPERTY = "auth.server.home";
public static final String CACHE_SERVER_LIFECYCLE_SKIP_PROPERTY = "cache.server.lifecycle.skip"; public static final String CACHE_SERVER_LIFECYCLE_SKIP_PROPERTY = "cache.server.lifecycle.skip";
@ -345,6 +349,17 @@ public class AuthServerTestEnricher {
} }
} }
if (HOT_ROD_STORE_ENABLED) {
HotRodStoreTestEnricher.initializeSuiteContext(suiteContext);
for (ContainerInfo container : suiteContext.getContainers()) {
// migrated auth server
if (container.getQualifier().equals("hot-rod-store")) {
suiteContext.setHotRodStoreInfo(container);
}
}
}
suiteContextProducer.set(suiteContext); suiteContextProducer.set(suiteContext);
CrossDCTestEnricher.initializeSuiteContext(suiteContext); CrossDCTestEnricher.initializeSuiteContext(suiteContext);
log.info("\n\n" + suiteContext); log.info("\n\n" + suiteContext);

View file

@ -0,0 +1,53 @@
package org.keycloak.testsuite.arquillian;
import org.jboss.arquillian.container.spi.event.StartContainer;
import org.jboss.arquillian.container.spi.event.StopContainer;
import org.jboss.arquillian.container.test.api.ContainerController;
import org.jboss.arquillian.core.api.Event;
import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.core.api.annotation.Observes;
import org.jboss.arquillian.core.spi.Validate;
import org.jboss.arquillian.test.spi.event.suite.AfterSuite;
import org.jboss.logging.Logger;
import org.jboss.arquillian.container.spi.event.StartSuiteContainers;
public class HotRodStoreTestEnricher {
private static SuiteContext suiteContext;
private static final Logger log = Logger.getLogger(HotRodStoreTestEnricher.class);
@Inject
private Event<StartContainer> startContainerEvent;
@Inject
private Event<StopContainer> stopContainerEvent;
static void initializeSuiteContext(SuiteContext suiteContext) {
Validate.notNull(suiteContext, "Suite context cannot be null.");
HotRodStoreTestEnricher.suiteContext = suiteContext;
}
public void beforeContainerStarted(@Observes(precedence = 1) StartSuiteContainers event) {
if (!AuthServerTestEnricher.HOT_ROD_STORE_ENABLED) return;
ContainerInfo hotRodContainer = suiteContext.getHotRodStoreInfo();
if (hotRodContainer != null && !hotRodContainer.isStarted()) {
log.infof("HotRod store starting: %s", hotRodContainer.getQualifier());
startContainerEvent.fire(new StartContainer(hotRodContainer.getArquillianContainer()));
}
}
public void afterSuite(@Observes(precedence = 4) AfterSuite event) {
if (!AuthServerTestEnricher.HOT_ROD_STORE_ENABLED) return;
ContainerInfo hotRodContainer = suiteContext.getHotRodStoreInfo();
if (hotRodContainer != null && hotRodContainer.isStarted()) {
log.infof("HotRod store stopping: %s", hotRodContainer.getQualifier());
stopContainerEvent.fire(new StopContainer(hotRodContainer.getArquillianContainer()));
}
}
}

View file

@ -67,6 +67,7 @@ public class KeycloakArquillianExtension implements LoadableExtension {
.observer(AuthServerTestEnricher.class) .observer(AuthServerTestEnricher.class)
.observer(AppServerTestEnricher.class) .observer(AppServerTestEnricher.class)
.observer(CrossDCTestEnricher.class) .observer(CrossDCTestEnricher.class)
.observer(HotRodStoreTestEnricher.class)
.observer(H2TestEnricher.class); .observer(H2TestEnricher.class);
builder builder
.service(TestExecutionDecider.class, MigrationTestExecutionDecider.class) .service(TestExecutionDecider.class, MigrationTestExecutionDecider.class)

View file

@ -49,6 +49,8 @@ public final class SuiteContext {
private ContainerInfo migratedAuthServerInfo; private ContainerInfo migratedAuthServerInfo;
private final MigrationContext migrationContext = new MigrationContext(); private final MigrationContext migrationContext = new MigrationContext();
private ContainerInfo hotRodStoreInfo;
private boolean adminPasswordUpdated; private boolean adminPasswordUpdated;
private final Map<String, String> smtpServer = new HashMap<>(); private final Map<String, String> smtpServer = new HashMap<>();
@ -174,6 +176,14 @@ public final class SuiteContext {
this.migratedAuthServerInfo = migratedAuthServerInfo; this.migratedAuthServerInfo = migratedAuthServerInfo;
} }
public ContainerInfo getHotRodStoreInfo() {
return hotRodStoreInfo;
}
public void setHotRodStoreInfo(ContainerInfo hotRodStoreInfo) {
this.hotRodStoreInfo = hotRodStoreInfo;
}
public boolean isAuthServerCluster() { public boolean isAuthServerCluster() {
return ! authServerBackendsInfo.isEmpty(); return ! authServerBackendsInfo.isEmpty();
} }

View file

@ -52,6 +52,8 @@ import org.wildfly.extras.creaper.core.online.OnlineOptions;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.Archive;
import org.keycloak.testsuite.util.ContainerAssume; import org.keycloak.testsuite.util.ContainerAssume;
@ -142,6 +144,10 @@ public class KeycloakContainerEventsController extends ContainerEventController
if (restartContainer.withoutKeycloakAddUserFile()) { if (restartContainer.withoutKeycloakAddUserFile()) {
removeKeycloakAddUserFile(); removeKeycloakAddUserFile();
} }
if (restartContainer.initializeDatabase()) {
clearMapStorageFiles();
}
} }
/** /**
@ -201,6 +207,17 @@ public class KeycloakContainerEventsController extends ContainerEventController
} }
private void clearMapStorageFiles() {
String filePath = System.getProperty("project.build.directory", "target/map");
File f = new File(filePath);
if (!f.exists()) return;
Arrays.stream(f.listFiles())
.filter(file -> file.getName().startsWith("map-") && file.getName().endsWith(".json"))
.forEach(File::delete);
}
/** /**
* Copy keycloak-add-user.json only if it is jboss container (has jbossHome property). * Copy keycloak-add-user.json only if it is jboss container (has jbossHome property).
*/ */

View file

@ -135,7 +135,6 @@
}, },
"mapStorage": { "mapStorage": {
"provider": "${keycloak.mapStorage.provider:}",
"concurrenthashmap": { "concurrenthashmap": {
"dir": "${project.build.directory:target}", "dir": "${project.build.directory:target}",
"keyType.realms": "string", "keyType.realms": "string",
@ -247,9 +246,12 @@
"connectionsHotRod": { "connectionsHotRod": {
"default": { "default": {
"embedded": "${keycloak.connectionsHotRod.embedded:true}", "embedded": "${keycloak.connectionsHotRod.embedded:false}",
"embeddedPort": "${keycloak.connectionsHotRod.embeddedPort:11444}", "port": "${keycloak.connectionsHotRod.port:14232}",
"enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:false}" "configureRemoteCaches": "${keycloak.connectionsHotRod.configureRemoteCaches:true}",
"username": "${keycloak.connectionsHotRod.username:myuser}",
"password": "${keycloak.connectionsHotRod.password:qwer1234!}",
"enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}"
} }
}, },

View file

@ -641,6 +641,19 @@
</container> </container>
</group> </group>
<container qualifier="hot-rod-store" mode="manual" >
<configuration>
<property name="enabled">${hotrod.store.enabled}</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.containers.InfinispanServerDeployableContainer</property>
<property name="infinispanHome">${cache.server.home}-hot-rod-store</property>
<!-- <property name="serverConfig">infinisan-xsite.xml</property>-->
<property name="portOffset">${hotrod.store.port.offset}</property>
<property name="managementPort">${hotrod.store.management.port}</property>
<property name="javaHome">${cache.server.java.home}</property>
</configuration>
</container>
<container qualifier="auth-server-remote" mode="manual" > <container qualifier="auth-server-remote" mode="manual" >
<configuration> <configuration>
<property name="enabled">${auth.server.remote}</property> <property name="enabled">${auth.server.remote}</property>

View file

@ -49,6 +49,7 @@
<auth.server.quarkus.cluster>false</auth.server.quarkus.cluster> <auth.server.quarkus.cluster>false</auth.server.quarkus.cluster>
<auth.server.crossdc>false</auth.server.crossdc> <auth.server.crossdc>false</auth.server.crossdc>
<hotrod.store.enabled>false</hotrod.store.enabled>
<auth.server.undertow.crossdc>false</auth.server.undertow.crossdc> <auth.server.undertow.crossdc>false</auth.server.undertow.crossdc>
<auth.server.jboss.crossdc>false</auth.server.jboss.crossdc> <auth.server.jboss.crossdc>false</auth.server.jboss.crossdc>
<cache.server.lifecycle.skip>false</cache.server.lifecycle.skip> <cache.server.lifecycle.skip>false</cache.server.lifecycle.skip>
@ -140,6 +141,9 @@
<cache.server.console.output>true</cache.server.console.output> <cache.server.console.output>true</cache.server.console.output>
<cache.server.auth>false</cache.server.auth> <cache.server.auth>false</cache.server.auth>
<hotrod.store.port.offset>3010</hotrod.store.port.offset>
<hotrod.store.management.port>13000</hotrod.store.management.port>
<!-- <!--
~ Definition of default JVM parameters for all modular JDKs. See: ~ Definition of default JVM parameters for all modular JDKs. See:
~ ~
@ -666,6 +670,11 @@
<auth.server.jboss.crossdc>${auth.server.jboss.crossdc}</auth.server.jboss.crossdc> <auth.server.jboss.crossdc>${auth.server.jboss.crossdc}</auth.server.jboss.crossdc>
<cache.server.lifecycle.skip>${cache.server.lifecycle.skip}</cache.server.lifecycle.skip> <cache.server.lifecycle.skip>${cache.server.lifecycle.skip}</cache.server.lifecycle.skip>
<!--hot-rod-store properties-->
<hotrod.store.enabled>${hotrod.store.enabled}</hotrod.store.enabled>
<hotrod.store.port.offset>${hotrod.store.port.offset}</hotrod.store.port.offset>
<hotrod.store.management.port>${hotrod.store.management.port}</hotrod.store.management.port>
<cache.server>${cache.server}</cache.server> <cache.server>${cache.server}</cache.server>
<cache.server.legacy>${cache.server.legacy}</cache.server.legacy> <cache.server.legacy>${cache.server.legacy}</cache.server.legacy>
<cache.server.1.port.offset>${cache.server.1.port.offset}</cache.server.1.port.offset> <cache.server.1.port.offset>${cache.server.1.port.offset}</cache.server.1.port.offset>
@ -1422,6 +1431,80 @@
</build> </build>
</profile> </profile>
<profile>
<id>map-storage-hot-rod</id>
<properties>
<hotrod.store.enabled>true</hotrod.store.enabled>
<skip.copy.hotrod.server>false</skip.copy.hotrod.server>
<cache.server>infinispan</cache.server>
</properties>
<build>
<plugins>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack-cache-server-standalone-infinispan</id>
<phase>generate-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.keycloak.testsuite</groupId>
<artifactId>integration-arquillian-servers-cache-server-infinispan-infinispan</artifactId>
<version>${project.version}</version>
<type>zip</type>
<outputDirectory>${containers.home}</outputDirectory>
</artifactItem>
</artifactItems>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>copy-cache-server-to-hot-rod-directory</id>
<phase>process-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<skip>${skip.copy.hotrod.server}</skip>
<target>
<move todir="${cache.server.home}-hot-rod-store">
<fileset dir="${cache.server.home}"/>
</move>
<chmod dir="${cache.server.home}-hot-rod-store/bin" perm="ugo+rx" includes="**/*.sh"/>
</target>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<keycloak.client.map.storage.provider>hotrod</keycloak.client.map.storage.provider>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
<profile> <profile>
<id>auth-server-profile</id> <id>auth-server-profile</id>

View file

@ -17,7 +17,10 @@
package org.keycloak.testsuite.model; package org.keycloak.testsuite.model;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import org.junit.Test; import org.junit.Test;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientProvider; import org.keycloak.models.ClientProvider;
@ -51,6 +54,42 @@ public class ClientModelTest extends KeycloakModelTest {
s.realms().removeRealm(realmId); s.realms().removeRealm(realmId);
} }
@Test
public void testClientsBasics() {
// Create client
ClientModel originalModel = withRealm(realmId, (session, realm) -> session.clients().addClient(realm, "myClientId"));
assertThat(originalModel.getId(), notNullValue());
// Find by id
{
ClientModel model = withRealm(realmId, (session, realm) -> session.clients().getClientById(realm, originalModel.getId()));
assertThat(model, notNullValue());
assertThat(model.getId(), is(equalTo(model.getId())));
assertThat(model.getClientId(), is(equalTo("myClientId")));
}
// Find by clientId
{
ClientModel model = withRealm(realmId, (session, realm) -> session.clients().getClientByClientId(realm, "myClientId"));
assertThat(model, notNullValue());
assertThat(model.getId(), is(equalTo(originalModel.getId())));
assertThat(model.getClientId(), is(equalTo("myClientId")));
}
// Test storing flow binding override
{
// Add some override
withRealm(realmId, (session, realm) -> {
ClientModel clientById = session.clients().getClientById(realm, originalModel.getId());
clientById.setAuthenticationFlowBindingOverride("browser", "customFlowId");
return clientById;
});
String browser = withRealm(realmId, (session, realm) -> session.clients().getClientById(realm, originalModel.getId()).getAuthenticationFlowBindingOverride("browser"));
assertThat(browser, is(equalTo("customFlowId")));
}
}
@Test @Test
public void testScopeMappingRoleRemoval() { public void testScopeMappingRoleRemoval() {
// create two clients, one realm role and one client role and assign both to one of the clients // create two clients, one realm role and one client role and assign both to one of the clients

View file

@ -60,7 +60,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
@Before @Before
public void initMapStorageProviderId() { public void initMapStorageProviderId() {
MapStorageProviderFactory ms = (MapStorageProviderFactory) getFactory().getProviderFactory(MapStorageProvider.class); MapStorageProviderFactory ms = (MapStorageProviderFactory) getFactory().getProviderFactory(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
mapStorageProviderId = ms.getId(); mapStorageProviderId = ms.getId();
assertThat(mapStorageProviderId, Matchers.notNullValue()); assertThat(mapStorageProviderId, Matchers.notNullValue());
} }
@ -84,7 +84,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
String component2Id = createMapStorageComponent("component2", "keyType", "string"); String component2Id = createMapStorageComponent("component2", "keyType", "string");
String[] ids = withRealm(realmId, (session, realm) -> { String[] ids = withRealm(realmId, (session, realm) -> {
ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class).getStorage(ClientModel.class); ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class); ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel> storage2 = (ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class); ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel> storage2 = (ConcurrentHashMapStorage<K2, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component2Id).getStorage(ClientModel.class);
@ -168,7 +168,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
// Check that in the next transaction, the objects are still there // Check that in the next transaction, the objects are still there
withRealm(realmId, (session, realm) -> { withRealm(realmId, (session, realm) -> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class).getStorage(ClientModel.class); ConcurrentHashMapStorage<K, MapClientEntity, ClientModel> storageMain = (ConcurrentHashMapStorage<K, MapClientEntity, ClientModel>) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class); ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View file

@ -38,7 +38,7 @@ import org.keycloak.models.map.realm.MapRealmProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory; import org.keycloak.models.map.role.MapRoleProviderFactory;
import org.keycloak.models.map.storage.MapStorageSpi; import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory; import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
//import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory; import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
import org.keycloak.models.map.user.MapUserProviderFactory; import org.keycloak.models.map.user.MapUserProviderFactory;
import org.keycloak.models.map.userSession.MapUserSessionProviderFactory; import org.keycloak.models.map.userSession.MapUserSessionProviderFactory;
import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.ProviderFactory;
@ -61,9 +61,9 @@ public class HotRodMapStorage extends KeycloakModelParameters {
.build(); .build();
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder() static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
//.add(HotRodMapStorageProviderFactory.class) .add(HotRodMapStorageProviderFactory.class)
.add(HotRodConnectionProviderFactory.class) .add(HotRodConnectionProviderFactory.class)
.add(ConcurrentHashMapStorageProviderFactory.class) .add(ConcurrentHashMapStorageProviderFactory.class) // TODO: this should be removed when we have a HotRod implementation for each area
.build(); .build();
private static final String STORAGE_CONFIG = "storage.provider"; private static final String STORAGE_CONFIG = "storage.provider";
@ -73,8 +73,7 @@ public class HotRodMapStorage extends KeycloakModelParameters {
@Override @Override
public void updateConfig(Config cf) { public void updateConfig(Config cf) {
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) 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("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)

View file

@ -67,8 +67,6 @@ public class Map extends KeycloakModelParameters {
.add(MapUserSessionProviderFactory.class) .add(MapUserSessionProviderFactory.class)
.add(MapUserLoginFailureProviderFactory.class) .add(MapUserLoginFailureProviderFactory.class)
.add(NoLockingDBLockProviderFactory.class) .add(NoLockingDBLockProviderFactory.class)
.add(MapStorageProviderFactory.class)
.build(); .build();
public Map() { public Map() {