This is an automated email from the ASF dual-hosted git repository. lburgazzoli pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
commit 24a6e274347be6a8b0819b278e1e0321accc54be Author: Luca Burgazzoli <lburgazz...@gmail.com> AuthorDate: Fri Oct 9 23:50:03 2020 +0200 CAMEL-15664: Automatically wrap secret properites with RAW when computing the URI --- .../org/apache/camel/spi/EndpointUriFactory.java | 5 ++ .../camel/catalog/impl/AbstractCamelCatalog.java | 41 ++++++++++- .../catalog/CustomEndpointUriFactoryTest.java | 80 ++++++++++++++++++++-- .../camel/catalog/RuntimeCamelCatalogTest.java | 32 +++++++++ .../component/EndpointUriFactorySupport.java | 10 +++ .../packaging/EndpointUriFactoryGenerator.java | 75 +++++++++++++++----- 6 files changed, 220 insertions(+), 23 deletions(-) diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/EndpointUriFactory.java b/core/camel-api/src/main/java/org/apache/camel/spi/EndpointUriFactory.java index a5f2dbf..1eb4d99 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/EndpointUriFactory.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/EndpointUriFactory.java @@ -50,6 +50,11 @@ public interface EndpointUriFactory extends CamelContextAware { Set<String> propertyNames(); /** + * Returns the names of the secret properties this endpoin supports. + */ + Set<String> secretPropertyNames(); + + /** * Whether the endpoint is lenient or not. * * @see Endpoint#isLenientProperties() diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index c765f49..1a7571a 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -32,6 +32,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -799,6 +800,7 @@ public abstract class AbstractCamelCatalog { // build at first according to syntax (use a tree map as we want the uri options sorted) Map<String, String> copy = new TreeMap<>(properties); + Matcher syntaxMatcher = COMPONENT_SYNTAX_PARSER.matcher(originalSyntax); while (syntaxMatcher.find()) { syntax += syntaxMatcher.group(1); @@ -821,6 +823,23 @@ public abstract class AbstractCamelCatalog { sb.append(syntax); if (!copy.isEmpty()) { + // wrap secret values with RAW to avoid breaking URI encoding in case of encoded values + copy.replaceAll((key, val) -> { + if (val == null) { + return val; + } + BaseOptionModel option = rows.get(key); + if (option == null) { + return val; + } + + if (option.isSecret() && !val.startsWith("#") && !val.startsWith("RAW(")) { + return "RAW(" + val + ")"; + } + + return val; + }); + boolean hasQuestionMark = sb.toString().contains("?"); // the last option may already contain a ? char, if so we should use & instead of ? sb.append(hasQuestionMark ? ampersand : '?'); @@ -905,8 +924,28 @@ public abstract class AbstractCamelCatalog { range++; } - if (!copy.isEmpty()) { + // wrap secret values with RAW to avoid breaking URI encoding in case of encoded values + copy.replaceAll(new BiFunction<String, String, String>() { + @Override + public String apply(String key, String val) { + + if (val == null) { + return val; + } + BaseOptionModel option = rows.get(key); + if (option == null) { + return val; + } + + if (option.isSecret() && !val.startsWith("#") && !val.startsWith("RAW(")) { + return "RAW(" + val + ")"; + } + + return val; + } + }); + // the last option may already contain a ? char, if so we should use & instead of ? sb.append(hasQuestionmark ? ampersand : '?'); String query = URISupport.createQueryString(copy, ampersand, encode); diff --git a/core/camel-core/src/test/java/org/apache/camel/catalog/CustomEndpointUriFactoryTest.java b/core/camel-core/src/test/java/org/apache/camel/catalog/CustomEndpointUriFactoryTest.java index c9f37ee..a6eafaf 100644 --- a/core/camel-core/src/test/java/org/apache/camel/catalog/CustomEndpointUriFactoryTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/catalog/CustomEndpointUriFactoryTest.java @@ -17,7 +17,10 @@ package org.apache.camel.catalog; import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -211,7 +214,22 @@ public class CustomEndpointUriFactoryTest extends ContextTestSupport { Assertions.assertEquals("jms2:foo?deliveryPersistent=true", uri); } - private class MyAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { + @Test + public void testJmsSecrets() throws Exception { + EndpointUriFactory assembler = new MyJmsxAssembler(); + assembler.setCamelContext(context); + + Map<String, Object> params = new LinkedHashMap<>(); + params.put("destinationName", "foo"); + params.put("deliveryPersistent", true); + params.put("username", "usr"); + params.put("password", "pwd"); + + String uri = assembler.buildUri("jmsx", params); + Assertions.assertEquals("jmsx:foo?deliveryPersistent=true&password=RAW(pwd)&username=RAW(usr)", uri); + } + + private static class MyAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { private static final String SYNTAX = "acme:name:port"; @@ -237,7 +255,12 @@ public class CustomEndpointUriFactoryTest extends ContextTestSupport { @Override public Set<String> propertyNames() { - return null; + return Collections.emptySet(); + } + + @Override + public Set<String> secretPropertyNames() { + return Collections.emptySet(); } @Override @@ -247,7 +270,7 @@ public class CustomEndpointUriFactoryTest extends ContextTestSupport { } - private class MySecondAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { + private static class MySecondAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { private static final String SYNTAX = "acme2:name/path:port"; @@ -274,7 +297,12 @@ public class CustomEndpointUriFactoryTest extends ContextTestSupport { @Override public Set<String> propertyNames() { - return null; + return Collections.emptySet(); + } + + @Override + public Set<String> secretPropertyNames() { + return Collections.emptySet(); } @Override @@ -284,7 +312,7 @@ public class CustomEndpointUriFactoryTest extends ContextTestSupport { } - private class MyJmsAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { + private static class MyJmsAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { private static final String SYNTAX = "jms2:destinationType:destinationName"; @@ -307,7 +335,47 @@ public class CustomEndpointUriFactoryTest extends ContextTestSupport { @Override public Set<String> propertyNames() { - return null; + return Collections.emptySet(); + } + + @Override + public Set<String> secretPropertyNames() { + return Collections.emptySet(); + } + + @Override + public boolean isLenientProperties() { + return false; + } + + } + + private static class MyJmsxAssembler extends EndpointUriFactorySupport implements EndpointUriFactory { + private static final String SYNTAX = "jmsx:destinationType:destinationName"; + + @Override + public boolean isEnabled(String scheme) { + return "jmsx".equals(scheme); + } + + @Override + public String buildUri(String scheme, Map<String, Object> properties) throws URISyntaxException { + String uri = SYNTAX; + uri = buildPathParameter(SYNTAX, uri, "destinationType", "queue", false, properties); + uri = buildPathParameter(SYNTAX, uri, "destinationName", null, true, properties); + uri = buildQueryParameters(uri, properties); + + return uri; + } + + @Override + public Set<String> propertyNames() { + return new HashSet<>(Arrays.asList("destinationType", "destinationName", "username", "password")); + } + + @Override + public Set<String> secretPropertyNames() { + return new HashSet<>(Arrays.asList("username", "password")); } @Override diff --git a/core/camel-core/src/test/java/org/apache/camel/catalog/RuntimeCamelCatalogTest.java b/core/camel-core/src/test/java/org/apache/camel/catalog/RuntimeCamelCatalogTest.java index 0a662d7..a831b61 100644 --- a/core/camel-core/src/test/java/org/apache/camel/catalog/RuntimeCamelCatalogTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/catalog/RuntimeCamelCatalogTest.java @@ -18,12 +18,15 @@ package org.apache.camel.catalog; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import org.apache.camel.catalog.impl.DefaultRuntimeCamelCatalog; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.tooling.model.ComponentModel; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.apache.camel.util.CollectionHelper.mapOf; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -113,6 +116,35 @@ public class RuntimeCamelCatalogTest { } @Test + public void testAsEndpointUriSecrets() throws Exception { + // create a custom instance of the catalog to hack the model to post + // process options. + catalog = new DefaultRuntimeCamelCatalog() { + @Override + public ComponentModel componentModel(String name) { + ComponentModel model = super.componentModel(name); + if (model != null && "timer".equals(name)) { + model.getEndpointParameterOptions().stream() + .filter(o -> Objects.equals(o.getName(), "period")) + .forEach(o -> o.setSecret(true)); + model.getEndpointPathOptions().stream() + .filter(o -> Objects.equals(o.getName(), "timerName")) + .forEach(o -> o.setDefaultValue("defaultName")); + } + return model; + } + }; + catalog.setCamelContext(new DefaultCamelContext()); + + assertEquals( + "timer:foo?period=RAW(5000)", + catalog.asEndpointUri("timer", mapOf("timerName", "foo", "period", "5000"), false)); + assertEquals( + "timer:defaultName?period=RAW(5000)", + catalog.asEndpointUri("timer", mapOf("period", "5000"), false)); + } + + @Test public void testAsEndpointUriLogShort() throws Exception { Map<String, String> map = new HashMap<>(); map.put("loggerName", "foo"); diff --git a/core/camel-support/src/main/java/org/apache/camel/support/component/EndpointUriFactorySupport.java b/core/camel-support/src/main/java/org/apache/camel/support/component/EndpointUriFactorySupport.java index 7e944a2..bcc3811 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/component/EndpointUriFactorySupport.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/component/EndpointUriFactorySupport.java @@ -78,6 +78,16 @@ public abstract class EndpointUriFactorySupport implements CamelContextAware, En throws URISyntaxException { // we want sorted parameters Map<String, Object> map = new TreeMap<>(parameters); + for (String secretParameter : secretPropertyNames()) { + Object val = map.get(secretParameter); + if (val instanceof String) { + String answer = (String) val; + if (!answer.startsWith("#") && !answer.startsWith("RAW(")) { + map.put(secretParameter, "RAW(" + val + ")"); + } + } + } + String query = URISupport.createQueryString(map); if (ObjectHelper.isNotEmpty(query)) { // there may be a ? sign in the context path then use & instead diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/EndpointUriFactoryGenerator.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/EndpointUriFactoryGenerator.java index 3103aa3..228dcd32 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/EndpointUriFactoryGenerator.java +++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/EndpointUriFactoryGenerator.java @@ -18,9 +18,9 @@ package org.apache.camel.maven.packaging; import java.io.IOException; import java.io.Writer; +import java.util.HashSet; import java.util.Set; import java.util.StringJoiner; -import java.util.TreeSet; import org.apache.camel.tooling.model.BaseOptionModel; import org.apache.camel.tooling.model.ComponentModel; @@ -39,6 +39,7 @@ public final class EndpointUriFactoryGenerator { w.write("package " + pn + ";\n"); w.write("\n"); w.write("import java.net.URISyntaxException;\n"); + w.write("import java.util.Collections;\n"); w.write("import java.util.HashMap;\n"); w.write("import java.util.HashSet;\n"); w.write("import java.util.Map;\n"); @@ -59,7 +60,11 @@ public final class EndpointUriFactoryGenerator { } w.write("\n"); w.write(" private static final Set<String> PROPERTY_NAMES;\n"); + w.write(" private static final Set<String> SECRET_PROPERTY_NAMES;\n"); + w.write(" static {\n"); w.write(generatePropertyNames(model)); + w.write(generateSecretPropertyNames(model)); + w.write(" }\n"); w.write("\n"); w.write(" @Override\n"); w.write(" public boolean isEnabled(String scheme) {\n"); @@ -96,6 +101,11 @@ public final class EndpointUriFactoryGenerator { w.write(" }\n"); w.write("\n"); w.write(" @Override\n"); + w.write(" public Set<String> secretPropertyNames() {\n"); + w.write(" return SECRET_PROPERTY_NAMES;\n"); + w.write(" }\n"); + w.write("\n"); + w.write(" @Override\n"); w.write(" public boolean isLenientProperties() {\n"); w.write(" return " + model.isLenientProperties() + ";\n"); w.write(" }\n"); @@ -104,25 +114,58 @@ public final class EndpointUriFactoryGenerator { } private static String generatePropertyNames(ComponentModel model) { - int size = model.getEndpointOptions().size(); - // use sorted set so the code is always generated the same way - Set<String> apis = new TreeSet<>(); - if (model.isApi()) { - // gather all the option names from the api (they can be duplicated as the same name can be used by multiple methods) - model.getApiOptions().forEach(a -> a.getMethods().forEach(m -> m.getOptions().forEach(o -> apis.add(o.getName())))); - size += apis.size(); + Set<String> properties = new HashSet<>(); + model.getEndpointOptions().stream() + .map(ComponentModel.EndpointOptionModel::getName) + .forEach(properties::add); + + // gather all the option names from the api (they can be duplicated as the same name + // can be used by multiple methods) + model.getApiOptions().stream() + .flatMap(a -> a.getMethods().stream()) + .flatMap(m -> m.getOptions().stream()) + .map(ComponentModel.ApiOptionModel::getName) + .forEach(properties::add); + + if (properties.isEmpty()) { + return " PROPERTY_NAMES = Collections.emptySet();\n"; } + StringBuilder sb = new StringBuilder(); - sb.append(" static {\n"); - sb.append(" Set<String> set = new HashSet<>(").append(size).append(");\n"); - for (ComponentModel.EndpointOptionModel option : model.getEndpointOptions()) { - sb.append(" set.add(\"").append(option.getName()).append("\");\n"); + sb.append(" Set<String> props = new HashSet<>(").append(properties.size()).append(");\n"); + for (String property : properties) { + sb.append(" props.add(\"").append(property).append("\");\n"); } - for (String name : apis) { - sb.append(" set.add(\"").append(name).append("\");\n"); + sb.append(" PROPERTY_NAMES = Collections.unmodifiableSet(props);\n"); + return sb.toString(); + } + + private static String generateSecretPropertyNames(ComponentModel model) { + Set<String> properties = new HashSet<>(); + model.getEndpointOptions().stream() + .filter(ComponentModel.EndpointOptionModel::isSecret) + .map(ComponentModel.EndpointOptionModel::getName) + .forEach(properties::add); + + // gather all the option names from the api (they can be duplicated as the same name + // can be used by multiple methods) + model.getApiOptions().stream() + .flatMap(a -> a.getMethods().stream()) + .flatMap(m -> m.getOptions().stream()) + .filter(ComponentModel.ApiOptionModel::isSecret) + .map(ComponentModel.ApiOptionModel::getName) + .forEach(properties::add); + + if (properties.isEmpty()) { + return " SECRET_PROPERTY_NAMES = Collections.emptySet();\n"; + } + + StringBuilder sb = new StringBuilder(); + sb.append(" Set<String> secretProps = new HashSet<>(").append(properties.size()).append(");\n"); + for (String property : properties) { + sb.append(" secretProps.add(\"").append(property).append("\");\n"); } - sb.append(" PROPERTY_NAMES = set;\n"); - sb.append(" }\n"); + sb.append(" SECRET_PROPERTY_NAMES = Collections.unmodifiableSet(secretProps);\n"); return sb.toString(); }