KEYCLOAK-19028 Add HotRod Map storage implementation
This commit is contained in:
parent
6071e2d518
commit
2f9a5aae0f
46 changed files with 2351 additions and 92 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -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-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/cache-server -Pcache-server-infinispan
|
||||
|
||||
- name: Store Keycloak artifacts
|
||||
id: store-keycloak
|
||||
|
@ -136,13 +137,19 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
server: ['quarkus', 'undertow-map', 'wildfly']
|
||||
server: ['quarkus', 'undertow-map', 'wildfly', 'undertow-map-hot-rod']
|
||||
tests: ['group1','group2','group3']
|
||||
fail-fast: false
|
||||
steps:
|
||||
- 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
|
||||
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
|
@ -150,6 +157,7 @@ jobs:
|
|||
restore-keys: cache-1-${{ runner.os }}-m2
|
||||
|
||||
- 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
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
|
@ -162,16 +170,20 @@ jobs:
|
|||
# ls -lR ~/.m2/repository
|
||||
|
||||
- uses: actions/setup-java@v1
|
||||
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
|
||||
with:
|
||||
java-version: ${{ env.DEFAULT_JDK_VERSION }}
|
||||
- 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/
|
||||
|
||||
- name: Run base tests
|
||||
if: ${{ github.event_name != 'pull_request' || matrix.server != 'undertow-map-hot-rod' || env.GIT_HOTROD_RELEVANT_DIFF != 0 }}
|
||||
run: |
|
||||
declare -A PARAMS TESTGROUP
|
||||
PARAMS["quarkus"]="-Pauth-server-quarkus"
|
||||
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"
|
||||
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"
|
||||
|
|
|
@ -47,6 +47,17 @@
|
|||
<artifactId>protostream-processor</artifactId>
|
||||
<scope>provided</scope>
|
||||
</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>
|
||||
|
||||
</project>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package org.keycloak.models.map.common;
|
||||
|
||||
import org.infinispan.manager.DefaultCacheManager;
|
||||
import org.infinispan.query.dsl.Query;
|
||||
import org.infinispan.rest.RestServer;
|
||||
import org.infinispan.rest.configuration.RestServerConfigurationBuilder;
|
||||
import org.infinispan.server.configuration.endpoint.SinglePortServerConfigurationBuilder;
|
||||
|
@ -84,4 +85,16 @@ public class HotRodUtils {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -19,20 +19,19 @@ package org.keycloak.models.map.common;
|
|||
|
||||
import org.infinispan.protostream.GeneratedSchema;
|
||||
import org.infinispan.protostream.annotations.AutoProtoSchemaBuilder;
|
||||
//import org.keycloak.models.map.client.HotRodAttributeEntity;
|
||||
//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.HotRodAttributeEntity;
|
||||
import org.keycloak.models.map.client.HotRodClientEntity;
|
||||
import org.keycloak.models.map.client.HotRodProtocolMapperEntity;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
|
||||
*/
|
||||
@AutoProtoSchemaBuilder(
|
||||
includeClasses = {
|
||||
//HotRodAttributeEntity.class,
|
||||
//HotRodClientEntity.class,
|
||||
//HotRodProtocolMapperEntity.class,
|
||||
//HotRodPair.class
|
||||
HotRodAttributeEntity.class,
|
||||
HotRodClientEntity.class,
|
||||
HotRodProtocolMapperEntity.class,
|
||||
HotRodPair.class
|
||||
},
|
||||
schemaFileName = "KeycloakHotRodMapStorage.proto",
|
||||
schemaFilePath = "proto/",
|
|
@ -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();
|
||||
}
|
|
@ -26,10 +26,10 @@ import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
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.ProtoSchemaInitializer;
|
||||
//import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
|
||||
import org.keycloak.models.map.storage.hotRod.HotRodMapStorageProviderFactory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
@ -106,9 +106,9 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
|
|||
|
||||
if (configureRemoteCaches) {
|
||||
// access the caches to force their creation
|
||||
/*HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
|
||||
HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
|
||||
.map(HotRodEntityDescriptor::getCacheName)
|
||||
.forEach(remoteCacheManager::getCache);*/
|
||||
.forEach(remoteCacheManager::getCache);
|
||||
}
|
||||
|
||||
registerSchemata(ProtoSchemaInitializer.INSTANCE);
|
||||
|
@ -133,8 +133,8 @@ public class DefaultHotRodConnectionProviderFactory implements HotRodConnectionP
|
|||
throw new RuntimeException("Cannot read the cache configuration!", e);
|
||||
}
|
||||
|
||||
/*HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
|
||||
HotRodMapStorageProviderFactory.ENTITY_DESCRIPTOR_MAP.values().stream()
|
||||
.map(HotRodEntityDescriptor::getCacheName)
|
||||
.forEach(name -> builder.remoteCache(name).configurationURI(uri));*/
|
||||
.forEach(name -> builder.remoteCache(name).configurationURI(uri));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 + ")";
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -32,8 +32,9 @@ import java.util.stream.Stream;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
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.utils.StreamsUtil;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
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 rollback;
|
||||
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 DeepCloner cloner;
|
||||
protected final Map<SearchableModelField<? super M>, UpdatePredicatesFunc<K, V, M>> fieldPredicates;
|
||||
|
||||
enum MapOperation {
|
||||
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.keyConvertor = keyConvertor;
|
||||
this.cloner = cloner;
|
||||
this.fieldPredicates = fieldPredicates;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -95,6 +98,10 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
|
|||
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
|
||||
*/
|
||||
|
@ -168,7 +175,7 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
|
|||
@Override
|
||||
public Stream<V> read(QueryParameters<M> queryParameters) {
|
||||
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()
|
||||
.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
|
||||
|
@ -246,7 +253,6 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long delete(QueryParameters<M> queryParameters) {
|
||||
log.tracef("Adding operation DELETE_BULK");
|
||||
|
@ -401,7 +407,7 @@ public class ConcurrentHashMapKeycloakTransaction<K, V extends AbstractEntity &
|
|||
|
||||
public Predicate<V> getFilterForNonDeletedObjects() {
|
||||
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 K> keyFilter = mmcb.getKeyFilter();
|
||||
|
|
|
@ -49,7 +49,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream;
|
|||
*
|
||||
* @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<>();
|
||||
|
||||
|
@ -64,15 +64,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
|
|||
this.cloner = cloner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
@Override
|
||||
public V create(V value) {
|
||||
K key = keyConvertor.fromStringSafe(value.getId());
|
||||
if (key == null) {
|
||||
|
@ -83,47 +75,26 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
|
|||
return 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}
|
||||
*/
|
||||
@Override
|
||||
public V read(String key) {
|
||||
Objects.requireNonNull(key, "Key must be non-null");
|
||||
K k = keyConvertor.fromStringSafe(key);
|
||||
return store.get(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
*/
|
||||
@Override
|
||||
public V update(V value) {
|
||||
K key = getKeyConvertor().fromStringSafe(value.getId());
|
||||
return store.replace(key, 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.
|
||||
*/
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
K k = getKeyConvertor().fromStringSafe(key);
|
||||
return store.remove(k) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
@Override
|
||||
public long delete(QueryParameters<M> queryParameters) {
|
||||
DefaultModelCriteria<M> criteria = queryParameters.getModelCriteriaBuilder();
|
||||
|
||||
|
@ -158,7 +129,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
|
|||
@SuppressWarnings("unchecked")
|
||||
public MapKeycloakTransaction<V, M> createTransaction(KeycloakSession session) {
|
||||
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() {
|
||||
|
@ -169,13 +140,7 @@ public class ConcurrentHashMapStorage<K, V extends AbstractEntity & UpdatableEnt
|
|||
return keyConvertor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
@Override
|
||||
public Stream<V> read(QueryParameters<M> queryParameters) {
|
||||
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 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
@Override
|
||||
public long getCount(QueryParameters<M> queryParameters) {
|
||||
return read(queryParameters).count();
|
||||
}
|
||||
|
|
|
@ -25,9 +25,13 @@ import org.keycloak.models.map.common.DeepCloner;
|
|||
import org.keycloak.models.map.storage.MapKeycloakTransaction;
|
||||
import org.keycloak.models.map.storage.ModelCriteriaBuilder.Operator;
|
||||
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.userSession.MapAuthenticatedClientSessionEntity;
|
||||
import org.keycloak.models.map.userSession.MapUserSessionEntity;
|
||||
import org.keycloak.storage.SearchableModelField;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -47,8 +51,14 @@ public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapSto
|
|||
|
||||
private final MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr;
|
||||
|
||||
public Transaction(MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr, StringKeyConvertor<K> keyConvertor, DeepCloner cloner) {
|
||||
super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner);
|
||||
public Transaction(MapKeycloakTransaction<MapAuthenticatedClientSessionEntity, AuthenticatedClientSessionModel> clientSessionTr,
|
||||
StringKeyConvertor<K> keyConvertor,
|
||||
DeepCloner cloner,
|
||||
Map<SearchableModelField<? super UserSessionModel>,
|
||||
UpdatePredicatesFunc<K,
|
||||
MapUserSessionEntity,
|
||||
UserSessionModel>> fieldPredicates) {
|
||||
super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner, fieldPredicates);
|
||||
this.clientSessionTr = clientSessionTr;
|
||||
}
|
||||
|
||||
|
@ -82,6 +92,6 @@ public class UserSessionConcurrentHashMapStorage<K> extends ConcurrentHashMapSto
|
|||
@SuppressWarnings("unchecked")
|
||||
public MapKeycloakTransaction<MapUserSessionEntity, UserSessionModel> createTransaction(KeycloakSession session) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,6 @@
|
|||
<module>infinispan</module>
|
||||
<module>map</module>
|
||||
<module>build-processor</module>
|
||||
<module>hot-rod</module>
|
||||
<module>map-hot-rod</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
15
pom.xml
15
pom.xml
|
@ -909,6 +909,21 @@
|
|||
<artifactId>infinispan-jboss-marshalling</artifactId>
|
||||
<version>${infinispan.version}</version>
|
||||
</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>
|
||||
<groupId>org.infinispan</groupId>
|
||||
<artifactId>infinispan-server-core</artifactId>
|
||||
|
|
|
@ -255,6 +255,8 @@
|
|||
<argument>myuser</argument>
|
||||
<argument>-p</argument>
|
||||
<argument>"qwer1234!"</argument>
|
||||
<argument>-g</argument>
|
||||
<argument>admin</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
|
|
@ -141,6 +141,10 @@ public class AuthServerTestEnricher {
|
|||
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 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 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);
|
||||
CrossDCTestEnricher.initializeSuiteContext(suiteContext);
|
||||
log.info("\n\n" + suiteContext);
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@ public class KeycloakArquillianExtension implements LoadableExtension {
|
|||
.observer(AuthServerTestEnricher.class)
|
||||
.observer(AppServerTestEnricher.class)
|
||||
.observer(CrossDCTestEnricher.class)
|
||||
.observer(HotRodStoreTestEnricher.class)
|
||||
.observer(H2TestEnricher.class);
|
||||
builder
|
||||
.service(TestExecutionDecider.class, MigrationTestExecutionDecider.class)
|
||||
|
|
|
@ -49,6 +49,8 @@ public final class SuiteContext {
|
|||
private ContainerInfo migratedAuthServerInfo;
|
||||
private final MigrationContext migrationContext = new MigrationContext();
|
||||
|
||||
private ContainerInfo hotRodStoreInfo;
|
||||
|
||||
private boolean adminPasswordUpdated;
|
||||
private final Map<String, String> smtpServer = new HashMap<>();
|
||||
|
||||
|
@ -174,6 +176,14 @@ public final class SuiteContext {
|
|||
this.migratedAuthServerInfo = migratedAuthServerInfo;
|
||||
}
|
||||
|
||||
public ContainerInfo getHotRodStoreInfo() {
|
||||
return hotRodStoreInfo;
|
||||
}
|
||||
|
||||
public void setHotRodStoreInfo(ContainerInfo hotRodStoreInfo) {
|
||||
this.hotRodStoreInfo = hotRodStoreInfo;
|
||||
}
|
||||
|
||||
public boolean isAuthServerCluster() {
|
||||
return ! authServerBackendsInfo.isEmpty();
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ import org.wildfly.extras.creaper.core.online.OnlineOptions;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.jboss.shrinkwrap.api.Archive;
|
||||
import org.keycloak.testsuite.util.ContainerAssume;
|
||||
|
||||
|
@ -142,6 +144,10 @@ public class KeycloakContainerEventsController extends ContainerEventController
|
|||
if (restartContainer.withoutKeycloakAddUserFile()) {
|
||||
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).
|
||||
*/
|
||||
|
|
|
@ -135,7 +135,6 @@
|
|||
},
|
||||
|
||||
"mapStorage": {
|
||||
"provider": "${keycloak.mapStorage.provider:}",
|
||||
"concurrenthashmap": {
|
||||
"dir": "${project.build.directory:target}",
|
||||
"keyType.realms": "string",
|
||||
|
@ -247,9 +246,12 @@
|
|||
|
||||
"connectionsHotRod": {
|
||||
"default": {
|
||||
"embedded": "${keycloak.connectionsHotRod.embedded:true}",
|
||||
"embeddedPort": "${keycloak.connectionsHotRod.embeddedPort:11444}",
|
||||
"enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:false}"
|
||||
"embedded": "${keycloak.connectionsHotRod.embedded:false}",
|
||||
"port": "${keycloak.connectionsHotRod.port:14232}",
|
||||
"configureRemoteCaches": "${keycloak.connectionsHotRod.configureRemoteCaches:true}",
|
||||
"username": "${keycloak.connectionsHotRod.username:myuser}",
|
||||
"password": "${keycloak.connectionsHotRod.password:qwer1234!}",
|
||||
"enableSecurity": "${keycloak.connectionsHotRod.enableSecurity:true}"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -641,6 +641,19 @@
|
|||
</container>
|
||||
</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" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.remote}</property>
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
<auth.server.quarkus.cluster>false</auth.server.quarkus.cluster>
|
||||
|
||||
<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.jboss.crossdc>false</auth.server.jboss.crossdc>
|
||||
<cache.server.lifecycle.skip>false</cache.server.lifecycle.skip>
|
||||
|
@ -140,6 +141,9 @@
|
|||
<cache.server.console.output>true</cache.server.console.output>
|
||||
<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:
|
||||
~
|
||||
|
@ -666,6 +670,11 @@
|
|||
<auth.server.jboss.crossdc>${auth.server.jboss.crossdc}</auth.server.jboss.crossdc>
|
||||
<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.legacy>${cache.server.legacy}</cache.server.legacy>
|
||||
<cache.server.1.port.offset>${cache.server.1.port.offset}</cache.server.1.port.offset>
|
||||
|
@ -1422,6 +1431,80 @@
|
|||
</build>
|
||||
</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>
|
||||
<id>auth-server-profile</id>
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
package org.keycloak.testsuite.model;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientProvider;
|
||||
|
@ -51,6 +54,42 @@ public class ClientModelTest extends KeycloakModelTest {
|
|||
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
|
||||
public void testScopeMappingRoleRemoval() {
|
||||
// create two clients, one realm role and one client role and assign both to one of the clients
|
||||
|
|
|
@ -60,7 +60,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
|
|||
|
||||
@Before
|
||||
public void initMapStorageProviderId() {
|
||||
MapStorageProviderFactory ms = (MapStorageProviderFactory) getFactory().getProviderFactory(MapStorageProvider.class);
|
||||
MapStorageProviderFactory ms = (MapStorageProviderFactory) getFactory().getProviderFactory(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID);
|
||||
mapStorageProviderId = ms.getId();
|
||||
assertThat(mapStorageProviderId, Matchers.notNullValue());
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ public class ConcurrentHashMapStorageTest extends KeycloakModelTest {
|
|||
String component2Id = createMapStorageComponent("component2", "keyType", "string");
|
||||
|
||||
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<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
|
||||
withRealm(realmId, (session, realm) -> {
|
||||
@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")
|
||||
ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel> storage1 = (ConcurrentHashMapStorage<K1, MapClientEntity, ClientModel>) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
|
@ -38,7 +38,7 @@ import org.keycloak.models.map.realm.MapRealmProviderFactory;
|
|||
import org.keycloak.models.map.role.MapRoleProviderFactory;
|
||||
import org.keycloak.models.map.storage.MapStorageSpi;
|
||||
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.userSession.MapUserSessionProviderFactory;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
@ -61,9 +61,9 @@ public class HotRodMapStorage extends KeycloakModelParameters {
|
|||
.build();
|
||||
|
||||
static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
|
||||
//.add(HotRodMapStorageProviderFactory.class)
|
||||
.add(HotRodMapStorageProviderFactory.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();
|
||||
|
||||
private static final String STORAGE_CONFIG = "storage.provider";
|
||||
|
@ -73,8 +73,7 @@ public class HotRodMapStorage extends KeycloakModelParameters {
|
|||
@Override
|
||||
public void updateConfig(Config cf) {
|
||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
//.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
|
||||
|
|
|
@ -67,8 +67,6 @@ public class Map extends KeycloakModelParameters {
|
|||
.add(MapUserSessionProviderFactory.class)
|
||||
.add(MapUserLoginFailureProviderFactory.class)
|
||||
.add(NoLockingDBLockProviderFactory.class)
|
||||
|
||||
.add(MapStorageProviderFactory.class)
|
||||
.build();
|
||||
|
||||
public Map() {
|
||||
|
|
Loading…
Reference in a new issue