Skip to content

Commit a4f5809

Browse files
committed
Expose security-sensitive properties for PostgreSQL connector
Exposed properties fall into one of the following categories: they are either explicitly marked as security-sensitive or are unknown. The connector assumes that unknown properties might be misspelled security-sensitive properties. The purpose of the included test is to identify security-sensitive properties that may be used by the connector. It uses the output generated by the maven-dependency-plugin, configured in the connector's pom.xml file. This output contains the connector's runtime classpath, which is then scanned to identify all property names annotated with @config. Scanning the classpath ensures that all configuration classes are included, even those used conditionally or contributed by other modules.
1 parent 77c36ea commit a4f5809

File tree

6 files changed

+244
-3
lines changed

6 files changed

+244
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package io.trino.plugin.base.config;
16+
17+
import io.airlift.configuration.ConfigurationMetadata;
18+
19+
import java.util.Set;
20+
21+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
22+
import static io.airlift.configuration.ConfigurationMetadata.getConfigurationMetadata;
23+
import static java.util.Objects.requireNonNull;
24+
25+
public record ConfigPropertyMetadata(String name, boolean sensitive)
26+
{
27+
public ConfigPropertyMetadata
28+
{
29+
requireNonNull(name, "name is null");
30+
}
31+
32+
public static Set<ConfigPropertyMetadata> getConfigProperties(Class<?> configClass)
33+
{
34+
ConfigurationMetadata<?> configurationMetadata = getConfigurationMetadata(configClass);
35+
return configurationMetadata.getAttributes().values().stream()
36+
.map(attribute -> new ConfigPropertyMetadata(attribute.getInjectionPoint().getProperty(), attribute.isSecuritySensitive()))
37+
.collect(toImmutableSet());
38+
}
39+
}

Diff for: plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/JdbcConnectorFactory.java

+49-1
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,21 @@
1313
*/
1414
package io.trino.plugin.jdbc;
1515

16+
import com.google.common.collect.ImmutableSet;
17+
import com.google.common.collect.Sets;
1618
import com.google.inject.Injector;
1719
import com.google.inject.Module;
1820
import io.airlift.bootstrap.Bootstrap;
21+
import io.airlift.bootstrap.BootstrapConfig;
1922
import io.opentelemetry.api.OpenTelemetry;
23+
import io.trino.plugin.base.config.ConfigPropertyMetadata;
24+
import io.trino.plugin.base.mapping.MappingConfig;
25+
import io.trino.plugin.jdbc.credential.CredentialConfig;
26+
import io.trino.plugin.jdbc.credential.CredentialProviderTypeConfig;
27+
import io.trino.plugin.jdbc.credential.ExtraCredentialConfig;
28+
import io.trino.plugin.jdbc.credential.file.ConfigFileBasedCredentialProviderConfig;
29+
import io.trino.plugin.jdbc.credential.keystore.KeyStoreBasedCredentialProviderConfig;
30+
import io.trino.plugin.jdbc.logging.FormatBasedRemoteQueryModifierConfig;
2031
import io.trino.spi.NodeManager;
2132
import io.trino.spi.VersionEmbedder;
2233
import io.trino.spi.catalog.CatalogName;
@@ -26,24 +37,55 @@
2637
import io.trino.spi.type.TypeManager;
2738

2839
import java.util.Map;
40+
import java.util.Set;
2941
import java.util.function.Supplier;
42+
import java.util.stream.Stream;
3043

3144
import static com.google.common.base.Preconditions.checkArgument;
3245
import static com.google.common.base.Strings.isNullOrEmpty;
46+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
3347
import static io.trino.plugin.base.Versions.checkStrictSpiVersionMatch;
48+
import static io.trino.plugin.base.config.ConfigPropertyMetadata.getConfigProperties;
3449
import static java.util.Objects.requireNonNull;
3550

3651
public class JdbcConnectorFactory
3752
implements ConnectorFactory
3853
{
3954
private final String name;
4055
private final Supplier<Module> module;
56+
private final Set<String> nonSecuritySensitivePropertyNames;
4157

42-
public JdbcConnectorFactory(String name, Supplier<Module> module)
58+
public JdbcConnectorFactory(String name, Supplier<Module> module, Set<ConfigPropertyMetadata> additionalProperties)
4359
{
4460
checkArgument(!isNullOrEmpty(name), "name is null or empty");
4561
this.name = name;
4662
this.module = requireNonNull(module, "module is null");
63+
Set<Class<?>> configClasses = ImmutableSet.<Class<?>>builder()
64+
.add(BaseJdbcConfig.class)
65+
.add(CredentialConfig.class)
66+
.add(JdbcStatisticsConfig.class)
67+
.add(JdbcWriteConfig.class)
68+
.add(QueryConfig.class)
69+
.add(RemoteQueryCancellationConfig.class)
70+
.add(TypeHandlingJdbcConfig.class)
71+
.add(JdbcMetadataConfig.class)
72+
.add(JdbcJoinPushdownConfig.class)
73+
.add(DecimalConfig.class)
74+
.add(JdbcDynamicFilteringConfig.class)
75+
.add(KeyStoreBasedCredentialProviderConfig.class)
76+
.add(FormatBasedRemoteQueryModifierConfig.class)
77+
.add(ConfigFileBasedCredentialProviderConfig.class)
78+
.add(CredentialProviderTypeConfig.class)
79+
.add(ExtraCredentialConfig.class)
80+
.add(MappingConfig.class)
81+
.add(BootstrapConfig.class)
82+
.build();
83+
this.nonSecuritySensitivePropertyNames = Stream.concat(
84+
configClasses.stream().flatMap(clazz -> getConfigProperties(clazz).stream()),
85+
requireNonNull(additionalProperties, "additionalProperties is null").stream())
86+
.filter(property -> !property.sensitive())
87+
.map(ConfigPropertyMetadata::name)
88+
.collect(toImmutableSet());
4789
}
4890

4991
@Override
@@ -74,4 +116,10 @@ public Connector create(String catalogName, Map<String, String> requiredConfig,
74116

75117
return injector.getInstance(JdbcConnector.class);
76118
}
119+
120+
@Override
121+
public Set<String> getSecuritySensitivePropertyNames(Set<String> propertyNames)
122+
{
123+
return Sets.difference(propertyNames, nonSecuritySensitivePropertyNames);
124+
}
77125
}

Diff for: plugin/trino-base-jdbc/src/main/java/io/trino/plugin/jdbc/JdbcPlugin.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@
1414
package io.trino.plugin.jdbc;
1515

1616
import com.google.common.collect.ImmutableList;
17+
import com.google.common.collect.ImmutableSet;
1718
import com.google.inject.Module;
19+
import io.trino.plugin.base.config.ConfigPropertyMetadata;
1820
import io.trino.plugin.jdbc.credential.CredentialProviderModule;
1921
import io.trino.spi.Plugin;
2022
import io.trino.spi.connector.ConnectorFactory;
2123

24+
import java.util.Set;
2225
import java.util.function.Supplier;
2326

2427
import static com.google.common.base.Preconditions.checkArgument;
@@ -31,12 +34,19 @@ public class JdbcPlugin
3134
{
3235
private final String name;
3336
private final Supplier<Module> module;
37+
private final Set<ConfigPropertyMetadata> additionalProperties;
3438

3539
public JdbcPlugin(String name, Supplier<Module> module)
40+
{
41+
this(name, module, ImmutableSet.of());
42+
}
43+
44+
public JdbcPlugin(String name, Supplier<Module> module, Set<ConfigPropertyMetadata> additionalProperties)
3645
{
3746
checkArgument(!isNullOrEmpty(name), "name is null or empty");
3847
this.name = name;
3948
this.module = requireNonNull(module, "module is null");
49+
this.additionalProperties = ImmutableSet.copyOf(requireNonNull(additionalProperties, "additionalProperties is null"));
4050
}
4151

4252
@Override
@@ -47,6 +57,7 @@ public Iterable<ConnectorFactory> getConnectorFactories()
4757
() -> combine(
4858
new CredentialProviderModule(),
4959
new ExtraCredentialsBasedIdentityCacheMappingModule(),
50-
module.get())));
60+
module.get()),
61+
additionalProperties));
5162
}
5263
}

Diff for: plugin/trino-postgresql/pom.xml

+30
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@
158158
<scope>test</scope>
159159
</dependency>
160160

161+
<dependency>
162+
<groupId>io.github.classgraph</groupId>
163+
<artifactId>classgraph</artifactId>
164+
<version>4.8.174</version>
165+
<scope>test</scope>
166+
</dependency>
167+
161168
<dependency>
162169
<groupId>io.trino</groupId>
163170
<artifactId>trino-base-jdbc</artifactId>
@@ -276,4 +283,27 @@
276283
<scope>test</scope>
277284
</dependency>
278285
</dependencies>
286+
287+
<build>
288+
<plugins>
289+
<plugin>
290+
<groupId>org.apache.maven.plugins</groupId>
291+
<artifactId>maven-dependency-plugin</artifactId>
292+
<executions>
293+
<execution>
294+
<id>print-runtime-dependencies</id>
295+
<goals>
296+
<goal>build-classpath</goal>
297+
</goals>
298+
<phase>generate-sources</phase>
299+
<configuration>
300+
<skip>false</skip>
301+
<includeScope>runtime</includeScope>
302+
<outputFile>${project.build.testOutputDirectory}/runtime-dependencies.txt</outputFile>
303+
</configuration>
304+
</execution>
305+
</executions>
306+
</plugin>
307+
</plugins>
308+
</build>
279309
</project>

Diff for: plugin/trino-postgresql/src/main/java/io/trino/plugin/postgresql/PostgreSqlPlugin.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
import io.trino.plugin.jdbc.JdbcPlugin;
1717

1818
import static io.airlift.configuration.ConfigurationAwareModule.combine;
19+
import static io.trino.plugin.base.config.ConfigPropertyMetadata.getConfigProperties;
1920

2021
public class PostgreSqlPlugin
2122
extends JdbcPlugin
2223
{
2324
public PostgreSqlPlugin()
2425
{
25-
super("postgresql", () -> combine(new PostgreSqlClientModule(), new PostgreSqlConnectionFactoryModule()));
26+
super("postgresql", () -> combine(new PostgreSqlClientModule(), new PostgreSqlConnectionFactoryModule()), getConfigProperties(PostgreSqlConfig.class));
2627
}
2728
}

Diff for: plugin/trino-postgresql/src/test/java/io/trino/plugin/postgresql/TestPostgreSqlPlugin.java

+112
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,30 @@
1414
package io.trino.plugin.postgresql;
1515

1616
import com.google.common.collect.ImmutableMap;
17+
import com.google.common.collect.ImmutableSet;
18+
import io.airlift.configuration.Config;
19+
import io.airlift.configuration.ConfigSecuritySensitive;
20+
import io.github.classgraph.AnnotationInfo;
21+
import io.github.classgraph.AnnotationParameterValueList;
22+
import io.github.classgraph.ClassGraph;
23+
import io.github.classgraph.ScanResult;
24+
import io.trino.plugin.base.config.ConfigPropertyMetadata;
1725
import io.trino.spi.Plugin;
1826
import io.trino.spi.connector.ConnectorFactory;
1927
import org.junit.jupiter.api.Test;
2028

29+
import java.io.File;
30+
import java.io.IOException;
31+
import java.net.URISyntaxException;
32+
import java.net.URL;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
35+
import java.util.Set;
36+
37+
import static com.google.common.base.Preconditions.checkState;
38+
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2139
import static com.google.common.collect.Iterables.getOnlyElement;
40+
import static org.assertj.core.api.Assertions.assertThat;
2241

2342
public class TestPostgreSqlPlugin
2443
{
@@ -34,4 +53,97 @@ public void testCreateConnector()
3453
"bootstrap.quiet", "true"),
3554
new TestingPostgreSqlConnectorContext()).shutdown();
3655
}
56+
57+
@Test
58+
void testUnknownPropertiesAreSecuritySensitive()
59+
{
60+
Plugin plugin = new PostgreSqlPlugin();
61+
ConnectorFactory connectorFactory = getOnlyElement(plugin.getConnectorFactories());
62+
Set<String> unknownProperties = ImmutableSet.of("unknown");
63+
64+
Set<String> sensitiveProperties = connectorFactory.getSecuritySensitivePropertyNames(unknownProperties);
65+
66+
assertThat(sensitiveProperties).isEqualTo(unknownProperties);
67+
}
68+
69+
@Test
70+
void testSecuritySensitiveProperties()
71+
throws Exception
72+
{
73+
// The purpose of this test is to help identify security-sensitive properties that
74+
// may be used by the connector. These properties are detected by scanning the
75+
// plugin's runtime classpath and collecting all property names annotated with
76+
// @ConfigSecuritySensitive. The scan includes all configuration classes, whether
77+
// they are always used, conditionally used, or never used. This approach has both
78+
// advantages and disadvantages.
79+
//
80+
// One advantage is that we don't need to rely on the plugin's configuration to
81+
// retrieve properties that are used conditionally. However, this method may also
82+
// capture properties that are not used at all but are pulled into the classpath
83+
// by dependencies. With that in mind, if this test fails, it likely indicates that
84+
// either a property needs to be added to the connector's security-sensitive
85+
// property names list, or it should be added to the excluded properties list below.
86+
Set<String> excludedClasses = ImmutableSet.of(
87+
"io.trino.plugin.base.ldap.LdapClientConfig",
88+
"io.airlift.http.client.HttpClientConfig",
89+
"io.airlift.node.NodeConfig",
90+
"io.airlift.log.LoggingConfiguration",
91+
"io.trino.plugin.base.security.FileBasedAccessControlConfig",
92+
"io.airlift.configuration.secrets.SecretsPluginConfig",
93+
"io.trino.plugin.base.jmx.ObjectNameGeneratorConfig");
94+
Plugin plugin = new PostgreSqlPlugin();
95+
ConnectorFactory connectorFactory = getOnlyElement(plugin.getConnectorFactories());
96+
97+
Set<ConfigPropertyMetadata> propertiesFoundInClasspath = findPropertiesInRuntimeClasspath(excludedClasses);
98+
Set<String> allPropertyNames = propertiesFoundInClasspath.stream()
99+
.map(ConfigPropertyMetadata::name)
100+
.collect(toImmutableSet());
101+
Set<String> expectedProperties = propertiesFoundInClasspath.stream()
102+
.filter(ConfigPropertyMetadata::sensitive)
103+
.map(ConfigPropertyMetadata::name)
104+
.collect(toImmutableSet());
105+
106+
Set<String> sensitiveProperties = connectorFactory.getSecuritySensitivePropertyNames(allPropertyNames);
107+
108+
assertThat(sensitiveProperties).isEqualTo(expectedProperties);
109+
}
110+
111+
private static Set<ConfigPropertyMetadata> findPropertiesInRuntimeClasspath(Set<String> excludedClassNames)
112+
throws URISyntaxException, IOException
113+
{
114+
try (ScanResult scanResult = new ClassGraph()
115+
.overrideClasspath(buildRuntimeClasspath())
116+
.enableAllInfo()
117+
.scan()) {
118+
return scanResult.getClassesWithMethodAnnotation(Config.class).stream()
119+
.filter(classInfo -> !excludedClassNames.contains(classInfo.getName()))
120+
.flatMap(classInfo -> classInfo.getMethodInfo().stream())
121+
.filter(methodInfo -> methodInfo.hasAnnotation(Config.class))
122+
.map(methodInfo -> {
123+
boolean sensitive = methodInfo.hasAnnotation(ConfigSecuritySensitive.class);
124+
AnnotationInfo annotationInfo = methodInfo.getAnnotationInfo(Config.class);
125+
checkState(annotationInfo != null, "Missing @Config annotation for %s", methodInfo);
126+
AnnotationParameterValueList parameterValues = annotationInfo.getParameterValues();
127+
checkState(parameterValues.size() == 1, "Expected exactly one parameter for %s", annotationInfo);
128+
String propertyName = (String) parameterValues.getFirst().getValue();
129+
return new ConfigPropertyMetadata(propertyName, sensitive);
130+
})
131+
.collect(toImmutableSet());
132+
}
133+
}
134+
135+
private static String buildRuntimeClasspath()
136+
throws URISyntaxException, IOException
137+
{
138+
// This file is generated by the maven-dependency-plugin, which is configured in the connector's pom.xml file.
139+
String runtimeDependenciesFile = "runtime-dependencies.txt";
140+
URL runtimeDependenciesUrl = TestPostgreSqlPlugin.class.getClassLoader().getResource(runtimeDependenciesFile);
141+
checkState(runtimeDependenciesUrl != null, "Missing %s file", runtimeDependenciesUrl);
142+
String runtimeDependenciesClasspath = Files.readString(Path.of(runtimeDependenciesUrl.toURI()));
143+
144+
File classDirectory = new File(new File(runtimeDependenciesUrl.toURI()).getParentFile().getParentFile(), "classes/");
145+
checkState(classDirectory.exists(), "Missing %s directory", classDirectory.getAbsolutePath());
146+
147+
return "%s:%s".formatted(runtimeDependenciesClasspath, classDirectory.getAbsolutePath());
148+
}
37149
}

0 commit comments

Comments
 (0)