diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 052b283010..71c91a737c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" diff --git a/model/hot-rod/pom.xml b/model/map-hot-rod/pom.xml similarity index 85% rename from model/hot-rod/pom.xml rename to model/map-hot-rod/pom.xml index 74e27e0a04..8f642b4fbf 100644 --- a/model/hot-rod/pom.xml +++ b/model/map-hot-rod/pom.xml @@ -47,6 +47,17 @@ protostream-processor provided + + + junit + junit + test + + + org.hamcrest + hamcrest-all + test + diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodAttributeEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodAttributeEntity.java new file mode 100644 index 0000000000..3b6f9e0b97 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodAttributeEntity.java @@ -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 values = new LinkedList<>(); + + public HotRodAttributeEntity() { + } + + public HotRodAttributeEntity(String name, List values) { + this.name = name; + this.values.addAll(values); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getValues() { + return values; + } + + public void setValues(List 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); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodClientEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodClientEntity.java new file mode 100644 index 0000000000..493a2a77a0 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodClientEntity.java @@ -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 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 attributes = new HashSet<>(); + + @ProtoField(number = 15) + public Set> 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 scope = new HashSet<>(); + + @ProtoField(number = 21) + public Set webOrigins = new HashSet<>(); + + @ProtoField(number = 22) + public Set protocolMappers = new HashSet<>(); + + @ProtoField(number = 23) + public Set> clientScopes = new HashSet<>(); + + @ProtoField(number = 24) + public Set 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 getAttribute(String name) { + return attributes.stream() + .filter(attributeEntity -> Objects.equals(attributeEntity.getName(), name)) + .findFirst() + .map(HotRodAttributeEntity::getValues) + .orElse(Collections.emptyList()); + } + + @Override + public Map> getAttributes() { + return attributes.stream().collect(Collectors.toMap(HotRodAttributeEntity::getName, HotRodAttributeEntity::getValues)); + } + + @Override + public void setAttribute(String name, List values) { + boolean valueUndefined = values == null || values.isEmpty(); + + Optional 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 getRedirectUris() { + return redirectUris; + } + + @Override + public void setRedirectUris(Set 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 getAuthFlowBindings() { + return authFlowBindings.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); + } + + @Override + public void setAuthFlowBindings(Map 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 getScope() { + return scope; + } + + @Override + public void setScope(Set 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 getWebOrigins() { + return webOrigins; + } + + @Override + public void setWebOrigins(Set 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 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 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 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 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 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; + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodProtocolMapperEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodProtocolMapperEntity.java new file mode 100644 index 0000000000..96683184c0 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/client/HotRodProtocolMapperEntity.java @@ -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> 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 getConfig() { + return config.stream().collect(Collectors.toMap(HotRodPair::getFirst, HotRodPair::getSecond)); + } + + @Override + public void setConfig(Map 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; + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodEntityDescriptor.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodEntityDescriptor.java new file mode 100644 index 0000000000..2423dd4fc1 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodEntityDescriptor.java @@ -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 { + private final Class modelTypeClass; + private final Class entityTypeClass; + private final List> hotRodClasses; + private final String cacheName; + + public HotRodEntityDescriptor(Class modelTypeClass, Class entityTypeClass, List> hotRodClasses, String cacheName) { + this.modelTypeClass = modelTypeClass; + this.entityTypeClass = entityTypeClass; + this.hotRodClasses = hotRodClasses; + this.cacheName = cacheName; + } + + public Class getModelTypeClass() { + return modelTypeClass; + } + + public Class getEntityTypeClass() { + return entityTypeClass; + } + + public Stream> getHotRodClasses() { + return hotRodClasses.stream(); + } + + public String getCacheName() { + return cacheName; + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodPair.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodPair.java new file mode 100644 index 0000000000..906d9bffbc --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodPair.java @@ -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 { + + @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); + } +} diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java similarity index 92% rename from model/hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java index a6e7a2ff4f..cfb9f4c699 100644 --- a/model/hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/HotRodUtils.java @@ -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 Query paginateQuery(Query 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; + } } diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java similarity index 75% rename from model/hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java index ec19172ae0..762a6fceb7 100644 --- a/model/hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/ProtoSchemaInitializer.java @@ -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 Martin Kanis */ @AutoProtoSchemaBuilder( includeClasses = { - //HotRodAttributeEntity.class, - //HotRodClientEntity.class, - //HotRodProtocolMapperEntity.class, - //HotRodPair.class + HotRodAttributeEntity.class, + HotRodClientEntity.class, + HotRodProtocolMapperEntity.class, + HotRodPair.class }, schemaFileName = "KeycloakHotRodMapStorage.proto", schemaFilePath = "proto/", diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/Versioned.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/Versioned.java new file mode 100644 index 0000000000..c048381fa4 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/common/Versioned.java @@ -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(); +} diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java similarity index 100% rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProvider.java diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java similarity index 92% rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java index 4508095af2..e5e3030568 100644 --- a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/DefaultHotRodConnectionProviderFactory.java @@ -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)); } } diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java similarity index 100% rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProvider.java diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java similarity index 100% rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionProviderFactory.java diff --git a/model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java similarity index 100% rename from model/hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java rename to model/map-hot-rod/src/main/java/org/keycloak/models/map/connections/HotRodConnectionSpi.java diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java new file mode 100644 index 0000000000..775ef84f41 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorage.java @@ -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 implements MapStorage, ConcurrentHashMapCrudOperations { + + private static final Logger LOG = Logger.getLogger(HotRodMapStorage.class); + + private final RemoteCache remoteCache; + private final StringKeyConvertor keyConvertor; + private final HotRodEntityDescriptor storedEntityDescriptor; + private final DeepCloner cloner; + + public HotRodMapStorage(RemoteCache remoteCache, StringKeyConvertor keyConvertor, HotRodEntityDescriptor 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 read(QueryParameters queryParameters) { + IckleQueryMapModelCriteriaBuilder 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 query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), + queryParameters.getLimit()); + + query.setParameters(iqmcb.getParameters()); + + CloseableIterator iterator = query.iterator(); + return closing(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false)) + .onClose(iterator::close); + } + + @Override + public long getCount(QueryParameters queryParameters) { + IckleQueryMapModelCriteriaBuilder iqmcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(createCriteriaBuilder()); + String queryString = iqmcb.getIckleQuery(); + + LOG.tracef("Executing count Ickle query: %s", queryString); + + QueryFactory queryFactory = Search.getQueryFactory(remoteCache); + + Query query = queryFactory.create(queryString); + query.setParameters(iqmcb.getParameters()); + + return query.execute().hitCount().orElse(0); + } + + @Override + public long delete(QueryParameters queryParameters) { + IckleQueryMapModelCriteriaBuilder 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 query = paginateQuery(queryFactory.create(queryString), queryParameters.getOffset(), + queryParameters.getLimit()); + + query.setParameters(iqmcb.getParameters()); + + AtomicLong result = new AtomicLong(); + + CloseableIterator 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 createCriteriaBuilder() { + return new IckleQueryMapModelCriteriaBuilder<>(); + } + + @Override + public MapKeycloakTransaction createTransaction(KeycloakSession session) { + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass()); + return new ConcurrentHashMapKeycloakTransaction<>(this, keyConvertor, cloner, fieldPredicates); + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java new file mode 100644 index 0000000000..1f4e94c062 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProvider.java @@ -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 MapStorage getStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { + HotRodMapStorage storage = getHotRodStorage(modelType, flags); + return storage; + } + + @SuppressWarnings("unchecked") + public HotRodMapStorage getHotRodStorage(Class modelType, MapStorageProviderFactory.Flag... flags) { + HotRodEntityDescriptor entityDescriptor = (HotRodEntityDescriptor) factory.getEntityDescriptor(modelType); + return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConvertor.StringKey.INSTANCE, entityDescriptor, cloner); + } + + @Override + public void close() { + + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java new file mode 100644 index 0000000000..94c2d4ea47 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java @@ -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, 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, 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"; + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java new file mode 100644 index 0000000000..07cbc4b355 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilder.java @@ -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 implements ModelCriteriaBuilder> { + + private static final int INITIAL_BUILDER_CAPACITY = 250; + private final StringBuilder whereClauseBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY); + private final Map parameters; + public static final Map, 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 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 compare(SearchableModelField modelField, Operator op, Object... value) { + StringBuilder newBuilder = new StringBuilder(INITIAL_BUILDER_CAPACITY); + newBuilder.append("("); + + if (notEmpty(whereClauseBuilder)) { + newBuilder.append(whereClauseBuilder).append(" AND ("); + } + + Map 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[] 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 joinParameters(IckleQueryMapModelCriteriaBuilder[] 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[] resolveNamedQueryConflicts(IckleQueryMapModelCriteriaBuilder[] builders) { + final Set existingKeys = new HashSet<>(); + + return Arrays.stream(builders).map(builder -> { + Map oldParameters = builder.getParameters(); + + if (oldParameters.keySet().stream().noneMatch(existingKeys::contains)) { + existingKeys.addAll(oldParameters.keySet()); + return builder; + } + + String newWhereClause = builder.getWhereClauseBuilder().toString(); + Map 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 and(IckleQueryMapModelCriteriaBuilder... builders) { + if (builders.length == 0) { + return new IckleQueryMapModelCriteriaBuilder<>(); + } + + builders = resolveNamedQueryConflicts(builders); + + return new IckleQueryMapModelCriteriaBuilder<>(joinBuilders(builders, " AND "), + joinParameters(builders)); + } + + @Override + public IckleQueryMapModelCriteriaBuilder or(IckleQueryMapModelCriteriaBuilder... builders) { + if (builders.length == 0) { + return new IckleQueryMapModelCriteriaBuilder<>(); + } + + builders = resolveNamedQueryConflicts(builders); + + return new IckleQueryMapModelCriteriaBuilder<>(joinBuilders(builders, " OR "), + joinParameters(builders)); + } + + @Override + public IckleQueryMapModelCriteriaBuilder not(IckleQueryMapModelCriteriaBuilder 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 getParameters() { + return parameters; + } +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java new file mode 100644 index 0000000000..34202dfbd4 --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryOperators.java @@ -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}. + *

+ * For example, + *

+ * for operator {@link ModelCriteriaBuilder.Operator.EQ} we concatenate left operand and right operand with equal sign: + * {@code fieldName = :parameterName} + *

+ * however, for operator {@link ModelCriteriaBuilder.Operator.EXISTS} we add following: + *

+ * {@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} 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 OPERATOR_TO_STRING = new HashMap<>(); + private static final Map 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 parameters); + } + + private static String exists(String modelField, Object[] values, Map parameters) { + String field = C + "." + modelField; + return field + " IS NOT NULL AND " + field + " IS NOT EMPTY"; + } + + private static String notExists(String modelField, Object[] values, Map parameters) { + String field = C + "." + modelField; + return field + " IS NULL OR " + field + " IS EMPTY"; + } + + private static String in(String modelField, Object[] values, Map 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 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 parameters) { + return operatorToExpressionCombinator(op).combine(filedName, values, parameters); + } + +} \ No newline at end of file diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java new file mode 100644 index 0000000000..6bcafa853d --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/IckleQueryWhereClauses.java @@ -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, + *

+ * 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} + *

+ * 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, 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 parameters); + } + + private static String produceWhereClause(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map 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 parameters) { + return whereClauseProducerForModelField(modelField) + .produceWhereClause(IckleQueryMapModelCriteriaBuilder.getFieldName(modelField), op, values, parameters); + } + + private static String whereClauseForClientsAttributes(String modelFieldName, ModelCriteriaBuilder.Operator op, Object[] values, Map 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 + ")"; + } +} diff --git a/model/hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory similarity index 100% rename from model/hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory rename to model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.connections.HotRodConnectionProviderFactory diff --git a/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory new file mode 100644 index 0000000000..132921e337 --- /dev/null +++ b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory @@ -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 diff --git a/model/hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi similarity index 100% rename from model/hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi rename to model/map-hot-rod/src/main/resources/META-INF/services/org.keycloak.provider.Spi diff --git a/model/hot-rod/src/main/resources/config/cacheConfig.xml b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml similarity index 100% rename from model/hot-rod/src/main/resources/config/cacheConfig.xml rename to model/map-hot-rod/src/main/resources/config/cacheConfig.xml diff --git a/model/hot-rod/src/main/resources/config/infinispan.xml b/model/map-hot-rod/src/main/resources/config/infinispan.xml similarity index 100% rename from model/hot-rod/src/main/resources/config/infinispan.xml rename to model/map-hot-rod/src/main/resources/config/infinispan.xml diff --git a/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java new file mode 100644 index 0000000000..6ea10cc6cf --- /dev/null +++ b/model/map-hot-rod/src/test/java/org/keycloak/models/map/storage/hotRod/IckleQueryMapModelCriteriaBuilderTest.java @@ -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 v = new IckleQueryMapModelCriteriaBuilder<>(); + IckleQueryMapModelCriteriaBuilder 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 v = criteria(); + IckleQueryMapModelCriteriaBuilder 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))); + } +} \ No newline at end of file diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java new file mode 100644 index 0000000000..1b2a9d87f7 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapCrudOperations.java @@ -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 { + /** + * 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. + *
+ * TODO: Consider returning {@code Optional} 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 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 read(QueryParameters 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 queryParameters); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java index 7913805a1a..547c91c3de 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapKeycloakTransaction.java @@ -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 implements MapKeycloakTransaction { @@ -42,18 +43,20 @@ public class ConcurrentHashMapKeycloakTransaction tasks = new LinkedHashMap<>(); - protected final ConcurrentHashMapStorage map; + protected final ConcurrentHashMapCrudOperations map; protected final StringKeyConvertor keyConvertor; protected final DeepCloner cloner; + protected final Map, UpdatePredicatesFunc> fieldPredicates; enum MapOperation { CREATE, UPDATE, DELETE, } - public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapStorage map, StringKeyConvertor keyConvertor, DeepCloner cloner) { + public ConcurrentHashMapKeycloakTransaction(ConcurrentHashMapCrudOperations map, StringKeyConvertor keyConvertor, DeepCloner cloner, Map, UpdatePredicatesFunc> fieldPredicates) { this.map = map; this.keyConvertor = keyConvertor; this.cloner = cloner; + this.fieldPredicates = fieldPredicates; } @Override @@ -95,6 +98,10 @@ public class ConcurrentHashMapKeycloakTransaction createCriteriaBuilder() { + return new MapModelCriteriaBuilder(keyConvertor, fieldPredicates); + } + /** * Adds a given task if not exists for the given key */ @@ -168,7 +175,7 @@ public class ConcurrentHashMapKeycloakTransaction read(QueryParameters queryParameters) { DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder(); - MapModelCriteriaBuilder mapMcb = mcb.flashToModelCriteriaBuilder(map.createCriteriaBuilder()); + MapModelCriteriaBuilder mapMcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder()); Predicate filterOutAllBulkDeletedObjects = tasks.values().stream() .filter(BulkDeleteOperation.class::isInstance) @@ -196,7 +203,7 @@ public class ConcurrentHashMapKeycloakTransaction queryParameters) { log.tracef("Adding operation DELETE_BULK"); @@ -401,7 +407,7 @@ public class ConcurrentHashMapKeycloakTransaction getFilterForNonDeletedObjects() { DefaultModelCriteria mcb = queryParameters.getModelCriteriaBuilder(); - MapModelCriteriaBuilder mmcb = mcb.flashToModelCriteriaBuilder(map.createCriteriaBuilder()); + MapModelCriteriaBuilder mmcb = mcb.flashToModelCriteriaBuilder(createCriteriaBuilder()); Predicate entityFilter = mmcb.getEntityFilter(); Predicate keyFilter = mmcb.getKeyFilter(); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java index 93e0097874..44935a91ec 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorage.java @@ -49,7 +49,7 @@ import static org.keycloak.utils.StreamsUtil.paginatedStream; * * @author hmlnarik */ -public class ConcurrentHashMapStorage implements MapStorage { +public class ConcurrentHashMapStorage implements MapStorage, ConcurrentHashMapCrudOperations { protected final ConcurrentMap store = new ConcurrentHashMap<>(); @@ -64,15 +64,7 @@ public class ConcurrentHashMapStorage - * TODO: Consider returning {@code Optional} 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 queryParameters) { DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); @@ -158,7 +129,7 @@ public class ConcurrentHashMapStorage createTransaction(KeycloakSession session) { MapKeycloakTransaction 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 createCriteriaBuilder() { @@ -169,13 +140,7 @@ public class ConcurrentHashMapStorage read(QueryParameters queryParameters) { DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); @@ -188,18 +153,17 @@ public class ConcurrentHashMapStorage keyFilter = mcb.getKeyFilter(); Predicate entityFilter = mcb.getEntityFilter(); - stream = stream.filter(me -> keyFilter.test(me.getKey()) && entityFilter.test(me.getValue())); + Stream 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 queryParameters) { return read(queryParameters).count(); } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java index 0ebf1a07b6..95293f46c8 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/UserSessionConcurrentHashMapStorage.java @@ -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 extends ConcurrentHashMapSto private final MapKeycloakTransaction clientSessionTr; - public Transaction(MapKeycloakTransaction clientSessionTr, StringKeyConvertor keyConvertor, DeepCloner cloner) { - super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner); + public Transaction(MapKeycloakTransaction clientSessionTr, + StringKeyConvertor keyConvertor, + DeepCloner cloner, + Map, + UpdatePredicatesFunc> fieldPredicates) { + super(UserSessionConcurrentHashMapStorage.this, keyConvertor, cloner, fieldPredicates); this.clientSessionTr = clientSessionTr; } @@ -82,6 +92,6 @@ public class UserSessionConcurrentHashMapStorage extends ConcurrentHashMapSto @SuppressWarnings("unchecked") public MapKeycloakTransaction createTransaction(KeycloakSession session) { MapKeycloakTransaction 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; } } diff --git a/model/pom.xml b/model/pom.xml index a3932630ce..e2af09a965 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -35,6 +35,6 @@ infinispan map build-processor - hot-rod + map-hot-rod diff --git a/pom.xml b/pom.xml index d9e413a695..21e7972cdc 100644 --- a/pom.xml +++ b/pom.xml @@ -909,6 +909,21 @@ infinispan-jboss-marshalling ${infinispan.version} + + org.infinispan + infinispan-client-hotrod + ${infinispan.version} + + + org.infinispan + infinispan-query-dsl + ${infinispan.version} + + + org.infinispan + infinispan-remote-query-client + ${infinispan.version} + org.infinispan infinispan-server-core diff --git a/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml b/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml index ac336a199d..d9ec9785c1 100644 --- a/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml +++ b/testsuite/integration-arquillian/servers/cache-server/infinispan/pom.xml @@ -255,6 +255,8 @@ myuser -p "qwer1234!" + -g + admin diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java index f70c4aeecb..d08bea17d3 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java @@ -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); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/HotRodStoreTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/HotRodStoreTestEnricher.java new file mode 100644 index 0000000000..293ba5a033 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/HotRodStoreTestEnricher.java @@ -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 startContainerEvent; + + @Inject + private Event 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())); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java index b8bfec9950..afe2ac41a0 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java @@ -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) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java index 0641112184..71e8f497c3 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/SuiteContext.java @@ -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 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(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java index 59c7318831..37cb824861 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java @@ -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). */ diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 51d7a7c78d..198c0d29ee 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -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}" } }, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index 92a1311233..417a66cd7f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -641,6 +641,19 @@ + + + ${hotrod.store.enabled} + org.keycloak.testsuite.arquillian.containers.InfinispanServerDeployableContainer + ${cache.server.home}-hot-rod-store + + ${hotrod.store.port.offset} + ${hotrod.store.management.port} + ${cache.server.java.home} + + + + ${auth.server.remote} diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index f87a931075..db65775e93 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -49,6 +49,7 @@ false false + false false false false @@ -140,6 +141,9 @@ true false + 3010 + 13000 + + ${hotrod.store.enabled} + ${hotrod.store.port.offset} + ${hotrod.store.management.port} + ${cache.server} ${cache.server.legacy} ${cache.server.1.port.offset} @@ -1422,6 +1431,80 @@ + + map-storage-hot-rod + + true + false + infinispan + + + + + + + + maven-dependency-plugin + + + unpack-cache-server-standalone-infinispan + generate-resources + + unpack + + + + + org.keycloak.testsuite + integration-arquillian-servers-cache-server-infinispan-infinispan + ${project.version} + zip + ${containers.home} + + + true + + + + + + + maven-antrun-plugin + + + copy-cache-server-to-hot-rod-directory + process-resources + + run + + + ${skip.copy.hotrod.server} + + + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + hotrod + + + + + + + + auth-server-profile diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java index 6344d57199..8ca08de0c8 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java @@ -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 diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java index 59e3d1bf6c..c3fa42d450 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/ConcurrentHashMapStorageTest.java @@ -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 storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class).getStorage(ClientModel.class); + ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class); ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class); ConcurrentHashMapStorage storage2 = (ConcurrentHashMapStorage) (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 storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class).getStorage(ClientModel.class); + ConcurrentHashMapStorage storageMain = (ConcurrentHashMapStorage) (MapStorage) session.getProvider(MapStorageProvider.class, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).getStorage(ClientModel.class); @SuppressWarnings("unchecked") ConcurrentHashMapStorage storage1 = (ConcurrentHashMapStorage) (MapStorage) session.getComponentProvider(MapStorageProvider.class, component1Id).getStorage(ClientModel.class); @SuppressWarnings("unchecked") diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java index 5c989aa7ad..1b57fbf38b 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java @@ -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> ALLOWED_FACTORIES = ImmutableSet.>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) diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java index d6ce5fd07f..97eb6f1433 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java @@ -67,8 +67,6 @@ public class Map extends KeycloakModelParameters { .add(MapUserSessionProviderFactory.class) .add(MapUserLoginFailureProviderFactory.class) .add(NoLockingDBLockProviderFactory.class) - - .add(MapStorageProviderFactory.class) .build(); public Map() {