diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java index b6a9c33c20..a06178592c 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyConditionRepresentation.java @@ -19,6 +19,7 @@ package org.keycloak.representations.idm; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; /** * @author Marek Posolda @@ -28,15 +29,8 @@ public class ClientPolicyConditionRepresentation { @JsonProperty("condition") private String conditionProviderId; - private ClientPolicyConditionConfigurationRepresentation configuration; - - public ClientPolicyConditionRepresentation() { - } - - public ClientPolicyConditionRepresentation(String conditionProviderId, ClientPolicyConditionConfigurationRepresentation configuration) { - this.conditionProviderId = conditionProviderId; - this.configuration = configuration; - } + @JsonProperty("configuration") + private JsonNode configuration; public String getConditionProviderId() { return conditionProviderId; @@ -46,11 +40,11 @@ public class ClientPolicyConditionRepresentation { this.conditionProviderId = conditionProviderId; } - public ClientPolicyConditionConfigurationRepresentation getConfiguration() { + public JsonNode getConfiguration() { return configuration; } - public void setConfiguration(ClientPolicyConditionConfigurationRepresentation configuration) { + public void setConfiguration(JsonNode configuration) { this.configuration = configuration; } } diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java index ab0f96d084..c0215bf498 100644 --- a/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/ClientPolicyExecutorRepresentation.java @@ -19,6 +19,7 @@ package org.keycloak.representations.idm; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; /** * @author Marek Posolda @@ -28,15 +29,8 @@ public class ClientPolicyExecutorRepresentation { @JsonProperty("executor") private String executorProviderId; - private ClientPolicyExecutorConfigurationRepresentation configuration; - - public ClientPolicyExecutorRepresentation() { - } - - public ClientPolicyExecutorRepresentation(String executorProviderId, ClientPolicyExecutorConfigurationRepresentation configuration) { - this.executorProviderId = executorProviderId; - this.configuration = configuration; - } + @JsonProperty("configuration") + private JsonNode configuration; public String getExecutorProviderId() { return executorProviderId; @@ -46,11 +40,11 @@ public class ClientPolicyExecutorRepresentation { this.executorProviderId = providerId; } - public ClientPolicyExecutorConfigurationRepresentation getConfiguration() { + public JsonNode getConfiguration() { return configuration; } - public void setConfiguration(ClientPolicyExecutorConfigurationRepresentation configuration) { + public void setConfiguration(JsonNode configuration) { this.configuration = configuration; } } diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java index 819ea5d399..d61fe9f29f 100755 --- a/core/src/test/java/org/keycloak/JsonParserTest.java +++ b/core/src/test/java/org/keycloak/JsonParserTest.java @@ -22,6 +22,10 @@ import org.junit.Test; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.representations.idm.ClientPoliciesRepresentation; +import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation; +import org.keycloak.representations.idm.ClientPolicyConditionRepresentation; +import org.keycloak.representations.idm.ClientPolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.oidc.OIDCClientRepresentation; import org.keycloak.util.JsonSerialization; @@ -30,6 +34,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -185,5 +190,26 @@ public class JsonParserTest { return JsonSerialization.readValue(repp, Map.class); } + @Test + public void testReadClientPolicy() throws Exception { + InputStream is = getClass().getClassLoader().getResourceAsStream("sample-client-policy.json"); + ClientPoliciesRepresentation clientPolicies = JsonSerialization.readValue(is, ClientPoliciesRepresentation.class); -} + Assert.assertEquals(clientPolicies.getPolicies().size(), 1); + ClientPolicyRepresentation clientPolicy = clientPolicies.getPolicies().get(0); + Assert.assertEquals("some-policy", clientPolicy.getName()); + List conditions = clientPolicy.getConditions(); + Assert.assertEquals(conditions.size(), 1); + ClientPolicyConditionRepresentation condition = conditions.get(0); + Assert.assertEquals("some-condition", condition.getConditionProviderId()); + + ClientPolicyConditionConfigurationRepresentation configRep = JsonSerialization.mapper.convertValue(condition.getConfiguration(), ClientPolicyConditionConfigurationRepresentation.class); + Assert.assertEquals(true, configRep.isNegativeLogic()); + Assert.assertEquals("val1", configRep.getConfigAsMap().get("string-option")); + Assert.assertEquals(14, configRep.getConfigAsMap().get("int-option")); + Assert.assertEquals(true, configRep.getConfigAsMap().get("bool-option")); + Assert.assertNull(configRep.getConfigAsMap().get("not-existing-option")); + } + + +} \ No newline at end of file diff --git a/core/src/test/resources/sample-client-policy.json b/core/src/test/resources/sample-client-policy.json new file mode 100644 index 0000000000..f374a99184 --- /dev/null +++ b/core/src/test/resources/sample-client-policy.json @@ -0,0 +1,20 @@ +{ + "policies": [ + { + "name": "some-policy", + "description": "This is some client policy.", + "enabled": true, + "conditions": [ + { + "condition": "some-condition", + "configuration": { + "is-negative-logic": true, + "string-option": "val1", + "int-option": 14, + "bool-option": true + } + } + ] + } + ] +} \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/component/JsonConfigComponentModel.java b/server-spi/src/main/java/org/keycloak/component/JsonConfigComponentModel.java new file mode 100644 index 0000000000..3ecc5ffe61 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/component/JsonConfigComponentModel.java @@ -0,0 +1,113 @@ +/* + * 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.component; + +import com.fasterxml.jackson.databind.JsonNode; +import org.keycloak.provider.Provider; + +/** + * Component model backed by JSON configuration. Useful for providers, which rely on JSON configuration rather than on ComponentModel, which is directly + * persisted as entity in the DB (store). + * + * @author Marek Posolda + */ +public class JsonConfigComponentModel extends ComponentModel { + + private final String providerType; + private final String providerId; + private final String componentId; + private final JsonNode configNode; + + /** + * @param providerType + * @param realmId + * @param providerId + * @param configNode JSON configuration of this provider. For example if node corresponds to JSON like "{\"foo\":\"bar\"}", then + * component configuration is supposed to have one configuration option "foo" with value "bar" + */ + public JsonConfigComponentModel(Class providerType, String realmId, String providerId, JsonNode configNode) { + checkNotNull(providerType, "providerType must be not null"); + checkNotNull(realmId, "realmId must be not null"); + checkNotNull(providerId, "providerId must be not null"); + checkNotNull(configNode, "configNode must be not null for provider " + providerId); + this.providerType = providerType.getName(); + this.providerId = providerId; + this.configNode = configNode; + + // We don't have realm model ID of the component, so componentId based on the realmId, providerType, providerId and hashCode of configurations. + this.componentId = realmId + "::" + providerType + "::" + this.providerId + "::" + configNode.hashCode(); + } + + private void checkNotNull(Object value, String message) { + if (value == null) { + throw new IllegalArgumentException(message); + } + } + + + @Override + public String getProviderType() { + return providerType; + } + + @Override + public String getProviderId() { + return providerId; + } + + @Override + public String getName() { + return componentId + "-config"; + } + + @Override + public String getId() { + return componentId; + } + + @Override + public boolean get(String key, boolean defaultValue) { + JsonNode sub = configNode.get(key); + return sub == null ? defaultValue : sub.asBoolean(); + } + + @Override + public long get(String key, long defaultValue) { + JsonNode sub = configNode.get(key); + return sub == null ? defaultValue : sub.asLong(); + } + + @Override + public int get(String key, int defaultValue) { + JsonNode sub = configNode.get(key); + return sub == null ? defaultValue : sub.asInt(); + } + + @Override + public String get(String key, String defaultValue) { + JsonNode sub = configNode.get(key); + return sub == null ? defaultValue : sub.asText(); + } + + @Override + public String get(String key) { + return get(key, null); + } + +} diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index a1f85419bc..6442a2a8d4 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -27,6 +27,7 @@ import org.keycloak.storage.federated.UserFederatedStorageProvider; import org.keycloak.vault.VaultTranscriber; import java.util.Set; +import java.util.function.Function; /** * @author Bill Burke @@ -74,6 +75,18 @@ public interface KeycloakSession extends InvalidationHandler { */ T getComponentProvider(Class clazz, String componentId); + /** + * Returns a component provider for a component from the realm that is relevant to this session. + * The relevant realm must be set prior to calling this method in the context, see {@link KeycloakContext#getRealm()}. + * @param + * @param clazz + * @param componentId Component configuration + * @param modelGetter Getter to retrieve componentModel + * @throws IllegalArgumentException If the realm is not set in the context. + * @return Provider configured according to the {@link componentId}, {@code null} if it cannot be instantiated. + */ + T getComponentProvider(Class clazz, String componentId, Function modelGetter); + /** * * @param diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index da01849d9d..f95f75f572 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -65,6 +65,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -337,8 +338,19 @@ public class DefaultKeycloakSession implements KeycloakSession { } @Override - @SuppressWarnings("unchecked") public T getComponentProvider(Class clazz, String componentId) { + final RealmModel realm = getContext().getRealm(); + if (realm == null) { + throw new IllegalArgumentException("Realm not set in the context."); + } + + // Loads componentModel from the realm + return this.getComponentProvider(clazz, componentId, KeycloakModelUtils.componentModelGetter(realm.getId(), componentId)); + } + + @Override + @SuppressWarnings("unchecked") + public T getComponentProvider(Class clazz, String componentId, Function modelGetter) { Integer hash = clazz.hashCode() + componentId.hashCode(); T provider = (T) providers.get(hash); final RealmModel realm = getContext().getRealm(); @@ -351,7 +363,7 @@ public class DefaultKeycloakSession implements KeycloakSession { // allowed on JDK 1.8, attempt of such a modification throws ConcurrentModificationException with JDK 9+ if (provider == null) { final String realmId = realm.getId(); - ProviderFactory providerFactory = factory.getProviderFactory(clazz, realmId, componentId, KeycloakModelUtils.componentModelGetter(realmId, componentId)); + ProviderFactory providerFactory = factory.getProviderFactory(clazz, realmId, componentId, modelGetter); if (providerFactory != null) { provider = providerFactory.create(this); providers.put(hash, provider); diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/ClientPoliciesUtil.java b/services/src/main/java/org/keycloak/services/clientpolicy/ClientPoliciesUtil.java index 7035c08760..f348085009 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/ClientPoliciesUtil.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/ClientPoliciesUtil.java @@ -22,16 +22,17 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.JsonNode; import org.jboss.logging.Logger; import org.keycloak.common.Profile; +import org.keycloak.component.ComponentModel; +import org.keycloak.component.JsonConfigComponentModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -71,75 +72,57 @@ public class ClientPoliciesUtil { } /** - * gets existing client profiles in a realm as model. - * not return null. + * Gets existing client profile of given name with resolved executor providers. It can be profile from realm or from global client profiles. */ - static Map getClientProfilesModel(KeycloakSession session, RealmModel realm, List globalClientProfiles) { - // get existing profiles as json - String profilesJson = getClientProfilesJsonString(realm); - if (profilesJson == null) { - return Collections.emptyMap(); - } - - // deserialize existing profiles (json -> representation) - ClientProfilesRepresentation profilesRep = null; - try { - profilesRep = convertClientProfilesJsonToRepresentation(profilesJson); - } catch (ClientPolicyException e) { - logger.warnv("Failed to serialize client profiles json string. err={0}, errDetail={1}", e.getError(), e.getErrorDetail()); - return Collections.emptyMap(); - } - if (profilesRep == null || profilesRep.getProfiles() == null) { - return Collections.emptyMap(); - } + static ClientProfile getClientProfileModel(KeycloakSession session, RealmModel realm, ClientProfilesRepresentation profilesRep, List globalClientProfiles, String profileName) throws ClientPolicyException { + // Obtain profiles from realm List profiles = profilesRep.getProfiles(); + if (profiles == null) { + profiles = new ArrayList<>(); + } // Add global profiles as well profiles.addAll(globalClientProfiles); - // constructing existing profiles (representation -> model) - Map profileMap = new HashMap<>(); - for (ClientProfileRepresentation profileRep : profilesRep.getProfiles()) { - // ignore profile without name - if (profileRep.getName() == null) { - continue; - } - - ClientProfileModel profileModel = new ClientProfileModel(); - profileModel.setName(profileRep.getName()); - profileModel.setDescription(profileRep.getDescription()); - - if (profileRep.getExecutors() == null) { - profileModel.setExecutors(new ArrayList<>()); - profileMap.put(profileRep.getName(), profileModel); - continue; - } - - List executors = new ArrayList<>(); - if (profileRep.getExecutors() != null) { - for (ClientPolicyExecutorRepresentation executorRep : profileRep.getExecutors()) { - ClientPolicyExecutorProvider provider = session.getProvider(ClientPolicyExecutorProvider.class, executorRep.getExecutorProviderId()); - if (provider == null) { - // executor's provider not found. just skip it. - logger.warnf("Executor with provider ID %s not found", executorRep.getExecutorProviderId()); - continue; - } - - try { - ClientPolicyExecutorConfigurationRepresentation configuration = (ClientPolicyExecutorConfigurationRepresentation) JsonSerialization.mapper.convertValue(executorRep.getConfiguration(), provider.getExecutorConfigurationClass()); - provider.setupConfiguration(configuration); - executors.add(provider); - } catch (IllegalArgumentException iae) { - logger.warnv("failed for Configuration Setup during setup provider {0} :: error = {1}", executorRep.getExecutorProviderId(), iae.getMessage()); - } - } - } - profileModel.setExecutors(executors); - - profileMap.put(profileRep.getName(), profileModel); + ClientProfileRepresentation profileRep = profiles.stream() + .filter(clientProfile -> profileName.equals(clientProfile.getName())) + .findFirst().orElse(null); + if (profileRep == null) { + return null; } - return profileMap; + ClientProfile profileModel = new ClientProfile(); + profileModel.setName(profileRep.getName()); + profileModel.setDescription(profileRep.getDescription()); + + if (profileRep.getExecutors() == null) { + profileModel.setExecutors(new ArrayList<>()); + return profileModel; + } + + List executors = new ArrayList<>(); + if (profileRep.getExecutors() != null) { + for (ClientPolicyExecutorRepresentation executorRep : profileRep.getExecutors()) { + ClientPolicyExecutorProvider provider = getExecutorProvider(session, realm, executorRep.getExecutorProviderId(), executorRep.getConfiguration()); + executors.add(provider); + } + } + profileModel.setExecutors(executors); + + return profileModel; + } + + private static ClientPolicyExecutorProvider getExecutorProvider(KeycloakSession session, RealmModel realm, String providerId, JsonNode config) { + ComponentModel componentModel = new JsonConfigComponentModel(ClientPolicyExecutorProvider.class, realm.getId(), providerId, config); + ClientPolicyExecutorProvider executorProvider = session.getComponentProvider(ClientPolicyExecutorProvider.class, componentModel.getId(), sessionFactory -> componentModel); + if (executorProvider == null) { + // condition's provider not found. just skip it. + throw new IllegalStateException("Executor with provider ID " + providerId + " not found"); + } + + ClientPolicyExecutorConfigurationRepresentation configuration = (ClientPolicyExecutorConfigurationRepresentation) JsonSerialization.mapper.convertValue(config, executorProvider.getExecutorConfigurationClass()); + executorProvider.setupConfiguration(configuration); + return executorProvider; } /** @@ -306,10 +289,10 @@ public class ClientPoliciesUtil { } /** - * get existing enabled client policies in a realm as model. + * Gets existing enabled client policies in a realm. * not return null. */ - static List getEnabledClientPoliciesModel(KeycloakSession session, RealmModel realm) { + static List getEnabledClientPolicies(KeycloakSession session, RealmModel realm) { // get existing profiles as json String policiesJson = getClientPoliciesJsonString(realm); if (policiesJson == null) { @@ -329,7 +312,7 @@ public class ClientPoliciesUtil { } // constructing existing policies (representation -> model) - List policyList = new ArrayList<>(); + List policyList = new ArrayList<>(); for (ClientPolicyRepresentation policyRep: policiesRep.getPolicies()) { // ignore policy without name if (policyRep.getName() == null) { @@ -341,7 +324,7 @@ public class ClientPoliciesUtil { continue; } - ClientPolicyModel policyModel = new ClientPolicyModel(); + ClientPolicy policyModel = new ClientPolicy(); policyModel.setName(policyRep.getName()); policyModel.setDescription(policyRep.getDescription()); policyModel.setEnable(true); @@ -349,20 +332,8 @@ public class ClientPoliciesUtil { List conditions = new ArrayList<>(); if (policyRep.getConditions() != null) { for (ClientPolicyConditionRepresentation conditionRep : policyRep.getConditions()) { - ClientPolicyConditionProvider provider = session.getProvider(ClientPolicyConditionProvider.class, conditionRep.getConditionProviderId()); - if (provider == null) { - // condition's provider not found. just skip it. - logger.warnf("Condition with provider ID %s not found", conditionRep.getConditionProviderId()); - continue; - } - - try { - ClientPolicyConditionConfigurationRepresentation configuration = (ClientPolicyConditionConfigurationRepresentation) JsonSerialization.mapper.convertValue(conditionRep.getConfiguration(), provider.getConditionConfigurationClass()); - provider.setupConfiguration(configuration); - conditions.add(provider); - } catch (IllegalArgumentException iae) { - logger.warnv("failed for Configuration Setup :: error = {0}", iae.getMessage()); - } + ClientPolicyConditionProvider provider = getConditionProvider(session, realm, conditionRep.getConditionProviderId(), conditionRep.getConfiguration()); + conditions.add(provider); } } policyModel.setConditions(conditions); @@ -377,6 +348,19 @@ public class ClientPoliciesUtil { return policyList; } + private static ClientPolicyConditionProvider getConditionProvider(KeycloakSession session, RealmModel realm, String providerId, JsonNode config) { + ComponentModel componentModel = new JsonConfigComponentModel(ClientPolicyConditionProvider.class, realm.getId(), providerId, config); + ClientPolicyConditionProvider conditionProvider = session.getComponentProvider(ClientPolicyConditionProvider.class, componentModel.getId(), sessionFactory -> componentModel); + if (conditionProvider == null) { + // condition's provider not found. just skip it. + throw new IllegalStateException("Condition with provider ID " + providerId + " not found"); + } + + ClientPolicyConditionConfigurationRepresentation configuration = (ClientPolicyConditionConfigurationRepresentation) JsonSerialization.mapper.convertValue(config, conditionProvider.getConditionConfigurationClass()); + conditionProvider.setupConfiguration(configuration); + return conditionProvider; + } + /** * convert client policies as representation to json. * can return null. diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyModel.java b/services/src/main/java/org/keycloak/services/clientpolicy/ClientPolicy.java similarity index 97% rename from services/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyModel.java rename to services/src/main/java/org/keycloak/services/clientpolicy/ClientPolicy.java index 6859b1c81c..0d1916055b 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyModel.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/ClientPolicy.java @@ -13,6 +13,7 @@ * 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.services.clientpolicy; @@ -25,7 +26,7 @@ import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvide /** * @author Takashi Norimatsu */ -public class ClientPolicyModel implements Serializable { +class ClientPolicy implements Serializable { protected String name; protected String description; diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/ClientProfileModel.java b/services/src/main/java/org/keycloak/services/clientpolicy/ClientProfile.java similarity index 96% rename from services/src/main/java/org/keycloak/services/clientpolicy/ClientProfileModel.java rename to services/src/main/java/org/keycloak/services/clientpolicy/ClientProfile.java index 5379b128b6..07f249e29d 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/ClientProfileModel.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/ClientProfile.java @@ -26,7 +26,7 @@ import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider; /** * @author Takashi Norimatsu */ -public class ClientProfileModel implements Serializable { +class ClientProfile implements Serializable { protected String name; protected String description; diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/DefaultClientPolicyManager.java b/services/src/main/java/org/keycloak/services/clientpolicy/DefaultClientPolicyManager.java index 33201f4b1c..95653cfb64 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/DefaultClientPolicyManager.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/DefaultClientPolicyManager.java @@ -20,9 +20,7 @@ package org.keycloak.services.clientpolicy; import java.io.IOException; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.jboss.logging.Logger; import org.keycloak.common.Profile; @@ -68,15 +66,14 @@ public class DefaultClientPolicyManager implements ClientPolicyManager { } private void doPolicyOperation(ClientConditionOperation condition, ClientExecutorOperation executor, RealmModel realm) throws ClientPolicyException { - Map map = ClientPoliciesUtil.getClientProfilesModel(session, realm, globalClientProfilesSupplier.get()); - List list = ClientPoliciesUtil.getEnabledClientPoliciesModel(session, realm).stream().collect(Collectors.toList()); + List list = ClientPoliciesUtil.getEnabledClientPolicies(session, realm); if (list == null || list.isEmpty()) { logger.trace("POLICY OPERATION :: No enabled policy."); return; } - for (ClientPolicyModel policy: list) { + for (ClientPolicy policy: list) { logger.tracev("POLICY OPERATION :: policy name = {0}", policy.getName()); if (!isSatisfied(policy, condition)) { logger.tracev("POLICY UNSATISFIED :: policy name = {0}", policy.getName()); @@ -84,12 +81,12 @@ public class DefaultClientPolicyManager implements ClientPolicyManager { } logger.tracev("POLICY APPLIED :: policy name = {0}", policy.getName()); - execute(policy, executor, map); + execute(policy, executor, realm); } } private boolean isSatisfied( - ClientPolicyModel policy, + ClientPolicy policy, ClientConditionOperation op) throws ClientPolicyException { if (policy.getConditions() == null || policy.getConditions().isEmpty()) { @@ -133,16 +130,20 @@ public class DefaultClientPolicyManager implements ClientPolicyManager { } private void execute( - ClientPolicyModel policy, + ClientPolicy policy, ClientExecutorOperation op, - Map map) throws ClientPolicyException { + RealmModel realm) throws ClientPolicyException { if (policy.getProfiles() == null || policy.getProfiles().isEmpty()) { logger.tracev("NO PROFILE :: policy name = {0}", policy.getName()); + return; } + // Get profiles from realm + ClientProfilesRepresentation clientProfiles = ClientPoliciesUtil.getClientProfilesRepresentation(session, realm); + for (String profileName : policy.getProfiles()) { - ClientProfileModel profile = map.get(profileName); + ClientProfile profile = ClientPoliciesUtil.getClientProfileModel(session, realm, clientProfiles, globalClientProfilesSupplier.get(), profileName); if (profile == null) { logger.tracev("PROFILE NOT FOUND :: policy name = {0}, profile name = {1}", policy.getName(), profileName); continue; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java index be4abefe49..8ff114edaf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java @@ -279,7 +279,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { } - protected void assertExpectedLoadedProfiles(Consumer modifiedAssertion) { + protected void assertExpectedLoadedProfiles(Consumer modifiedAssertion) throws Exception { // retrieve loaded builtin profiles ClientProfilesRepresentation actualProfilesRep = getProfilesWithGlobals(); @@ -768,6 +768,11 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { profilesRep.setProfiles(new ArrayList<>()); } + // Create client profile from existing representation + public ClientProfilesBuilder(ClientProfilesRepresentation existingRep) { + this.profilesRep = existingRep; + } + public ClientProfilesBuilder addProfile(ClientProfileRepresentation rep) { profilesRep.getProfiles().add(rep); return this; @@ -809,11 +814,14 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { return this; } - public ClientProfileBuilder addExecutor(String providerId, ClientPolicyExecutorConfigurationRepresentation config) { + public ClientProfileBuilder addExecutor(String providerId, ClientPolicyExecutorConfigurationRepresentation config) throws Exception { if (config == null) { config = new ClientPolicyExecutorConfigurationRepresentation(); } - profileRep.getExecutors().add(new ClientPolicyExecutorRepresentation(providerId, config)); + ClientPolicyExecutorRepresentation executor = new ClientPolicyExecutorRepresentation(); + executor.setExecutorProviderId(providerId); + executor.setConfiguration(JsonSerialization.mapper.readValue(JsonSerialization.mapper.writeValueAsBytes(config), JsonNode.class)); + profileRep.getExecutors().add(executor); return this; } @@ -930,8 +938,11 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { return this; } - public ClientPolicyBuilder addCondition(String providerId, ClientPolicyConditionConfigurationRepresentation config) { - policyRep.getConditions().add(new ClientPolicyConditionRepresentation(providerId, config)); + public ClientPolicyBuilder addCondition(String providerId, ClientPolicyConditionConfigurationRepresentation config) throws Exception { + ClientPolicyConditionRepresentation condition = new ClientPolicyConditionRepresentation(); + condition.setConditionProviderId(providerId); + condition.setConfiguration(JsonSerialization.mapper.readValue(JsonSerialization.mapper.writeValueAsBytes(config), JsonNode.class)); + policyRep.getConditions().add(condition); return this; } @@ -1278,16 +1289,15 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { assertExpectedAugmenedExecutor(isAugment, PKCEEnforcerExecutorFactory.PROVIDER_ID, profileRep); } - protected void assertExpectedSecureClientAuthEnforceExecutor(List clientAuthns, boolean isAugment, String clientAuthnsAugment, ClientProfileRepresentation profileRep) { + protected void assertExpectedSecureClientAuthEnforceExecutor(List clientAuthns, boolean isAugment, String clientAuthnsAugment, ClientProfileRepresentation profileRep) throws Exception { assertExpectedAugmenedExecutor(isAugment, SecureClientAuthenticatorExecutorFactory.PROVIDER_ID, profileRep); assertNotNull(profileRep); - Map actualExecutorConfig = getConfigOfExecutor(SecureClientAuthenticatorExecutorFactory.PROVIDER_ID, profileRep); + JsonNode actualExecutorConfig = getConfigOfExecutor(SecureClientAuthenticatorExecutorFactory.PROVIDER_ID, profileRep); assertNotNull(actualExecutorConfig); - - Set actualClientAuthns = new HashSet<>((Collection) actualExecutorConfig.get("client-authns")); + Set actualClientAuthns = new HashSet<>((Collection) JsonSerialization.readValue(actualExecutorConfig.get("client-authns").toString(), List.class)); assertEquals(new HashSet<>(clientAuthns), actualClientAuthns); - String actualClientAuthnAugment = actualExecutorConfig.get("client-authns-augment").toString(); + String actualClientAuthnAugment = actualExecutorConfig.get("client-authns-augment").textValue(); assertEquals(clientAuthnsAugment, actualClientAuthnAugment); } @@ -1317,17 +1327,17 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { protected void assertExpectedAugmenedExecutor(boolean isAugment, String providerId, ClientProfileRepresentation profileRep) { assertNotNull(profileRep); - Map actualExecutorConfig = getConfigOfExecutor(providerId, profileRep); + JsonNode actualExecutorConfig = getConfigOfExecutor(providerId, profileRep); assertNotNull(actualExecutorConfig); - boolean actualIsAugment = actualExecutorConfig.get("is-augment") == null ? false : (Boolean) actualExecutorConfig.get("is-augment"); + boolean actualIsAugment = actualExecutorConfig.get("is-augment") == null ? false : actualExecutorConfig.get("is-augment").asBoolean(); assertEquals(isAugment, actualIsAugment); } - private Map getConfigOfExecutor(String providerId, ClientProfileRepresentation profileRep) { + private JsonNode getConfigOfExecutor(String providerId, ClientProfileRepresentation profileRep) { ClientPolicyExecutorRepresentation executorRep = profileRep.getExecutors().stream() .filter(profileRepp -> providerId.equals(profileRepp.getExecutorProviderId())) .findFirst().orElse(null); - return executorRep == null ? null : executorRep.getConfiguration().getConfigAsMap(); + return executorRep == null ? null : executorRep.getConfiguration(); } // Assertions about policies @@ -1455,7 +1465,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest { } private void assertExpectedEmptyConfig(String executorProviderId, ClientProfileRepresentation profileRep) { - Map config = getConfigOfExecutor(executorProviderId, profileRep); + JsonNode config = getConfigOfExecutor(executorProviderId, profileRep); Assert.assertTrue("Expected empty configuration for provider " + executorProviderId, config.isEmpty()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesImportExportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesImportExportTest.java index 3f22a80f83..bb4b4819ad 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesImportExportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesImportExportTest.java @@ -72,7 +72,7 @@ public class ClientPoliciesImportExportTest extends AbstractClientPoliciesTest { testRealmExportImport(); } - private void testRealmExportImport() throws LifecycleException { + private void testRealmExportImport() throws Exception { testingClient.testing().exportImport().setAction(ExportImportConfig.ACTION_EXPORT); testingClient.testing().exportImport().setRealmName("test"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java index e16b69e1ff..ad3398eda6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java @@ -207,6 +207,31 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { assertEquals(JWTClientSecretAuthenticator.PROVIDER_ID, getClientByAdmin(cId).getClientAuthenticatorType()); } + // KEYCLOAK-18108 + @Test + public void testTwoProfilesWithDifferentConfigurationOfSameExecutorType() throws Exception { + setupPolicyClientIdAndSecretNotAcceptableAuthType(POLICY_NAME); + + // register another profile with "SecureClientAuthEnforceExecutorFactory", but use different configuration of client authenticator. + // This profile won't allow JWTClientSecretAuthenticator.PROVIDER_ID + String profileName = "UnusedProfile"; + String json = (new ClientProfilesBuilder(getProfilesWithoutGlobals())).addProfile( + (new ClientProfileBuilder()).createProfile(profileName, "Profile with SecureClientAuthEnforceExecutorFactory") + .addExecutor(SecureClientAuthenticatorExecutorFactory.PROVIDER_ID, + createSecureClientAuthEnforceExecutorConfig(Boolean.FALSE, + Arrays.asList(JWTClientAuthenticator.PROVIDER_ID, X509ClientAuthenticator.PROVIDER_ID), + null)) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // Make sure it is still possible to create client with JWTClientSecretAuthenticator. The "UnusedProfile" should not be used as it is not referenced from any client policy + String cId = createClientByAdmin(generateSuffixedName(CLIENT_NAME), (ClientRepresentation clientRep) -> { + clientRep.setClientAuthenticatorType(JWTClientSecretAuthenticator.PROVIDER_ID); + }); + assertEquals(JWTClientSecretAuthenticator.PROVIDER_ID, getClientByAdmin(cId).getClientAuthenticatorType()); + } + @Test public void testAdminClientUpdateAcceptableAuthType() throws Exception { setupPolicyClientIdAndSecretNotAcceptableAuthType(POLICY_NAME); @@ -2051,7 +2076,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { } } - private void setupPolicyClientIdAndSecretNotAcceptableAuthType(String policyName) throws ClientPolicyException { + private void setupPolicyClientIdAndSecretNotAcceptableAuthType(String policyName) throws Exception { // register profiles String profileName = "MyProfile"; String json = (new ClientProfilesBuilder()).addProfile( @@ -2075,7 +2100,7 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { updatePolicies(json); } - private void setupPolicyAuthzCodeFlowUnderMultiPhasePolicy(String policyName) throws ClientPolicyException { + private void setupPolicyAuthzCodeFlowUnderMultiPhasePolicy(String policyName) throws Exception { // register profiles String profileName = "MyProfile"; String json = (new ClientProfilesBuilder()).addProfile(