You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by lb...@apache.org on 2020/10/12 10:37:57 UTC

[camel] 01/02: CAMEL-15664: Automatically wrap secret properites with RAW when computing the URI

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 <lb...@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();
     }