Skip to content

Commit

Permalink
Catalog/S3: Allow custom trust and key stores
Browse files Browse the repository at this point in the history
  • Loading branch information
snazy committed Jul 4, 2024
1 parent 2af65b0 commit ce377a1
Show file tree
Hide file tree
Showing 10 changed files with 387 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void init() {
server = mockServer(mock -> {});

S3Config s3config = S3Config.builder().build();
httpClient = S3Clients.apacheHttpClient(s3config);
httpClient = S3Clients.apacheHttpClient(s3config, new SecretsProvider(names -> Map.of()));

S3Options<S3BucketOptions> s3options =
S3ProgrammaticOptions.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
Expand All @@ -41,6 +42,7 @@
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.projectnessie.catalog.secrets.SecretsProvider;
import org.projectnessie.objectstoragemock.ObjectStorageMock;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.regions.Region;
Expand Down Expand Up @@ -70,7 +72,7 @@ public void init() {
server = mockServer(mock -> {});

S3Config s3config = S3Config.builder().build();
httpClient = S3Clients.apacheHttpClient(s3config);
httpClient = S3Clients.apacheHttpClient(s3config, new SecretsProvider(names -> Map.of()));

S3Options<S3BucketOptions> s3options =
S3ProgrammaticOptions.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,40 @@
*/
package org.projectnessie.catalog.files.s3;

import static org.projectnessie.catalog.secrets.SecretType.KEY;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Optional;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.projectnessie.catalog.secrets.BasicCredentials;
import org.projectnessie.catalog.secrets.KeySecret;
import org.projectnessie.catalog.secrets.SecretAttribute;
import org.projectnessie.catalog.secrets.SecretsProvider;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.TlsTrustManagersProvider;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.internal.http.AbstractFileStoreTlsKeyManagersProvider;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.Validate;

public class S3Clients {

/** Builds an SDK Http client based on the Apache Http client. */
public static SdkHttpClient apacheHttpClient(S3Config s3Config) {
public static SdkHttpClient apacheHttpClient(S3Config s3Config, SecretsProvider secretsProvider) {
ApacheHttpClient.Builder httpClient = ApacheHttpClient.builder();
s3Config.maxHttpConnections().ifPresent(httpClient::maxConnections);
s3Config.readTimeout().ifPresent(httpClient::socketTimeout);
Expand All @@ -34,7 +57,65 @@ public static SdkHttpClient apacheHttpClient(S3Config s3Config) {
s3Config.connectionMaxIdleTime().ifPresent(httpClient::connectionMaxIdleTime);
s3Config.connectionTimeToLive().ifPresent(httpClient::connectionTimeToLive);
s3Config.expectContinueEnabled().ifPresent(httpClient::expectContinueEnabled);
return httpClient.build();
s3Config
.trustStorePath()
.ifPresent(
p -> {
S3Config withPassword =
secretsProvider
.applySecrets(
S3Config.builder().from(s3Config),
"s3.trust-store",
s3Config,
null,
null,
List.of(
SecretAttribute.secretAttribute(
"password",
KEY,
S3Config::trustStorePassword,
S3Config.Builder::trustStorePassword)))
.build();

httpClient.tlsTrustManagersProvider(
new FileStoreTlsTrustManagersProvider(
p,
withPassword
.trustStoreType()
.orElseThrow(() -> new IllegalArgumentException("No trust store type")),
withPassword.trustStorePassword().orElse(null)));
});
s3Config
.keyStorePath()
.ifPresent(
p -> {
S3Config withPassword =
secretsProvider
.applySecrets(
S3Config.builder().from(s3Config),
"s3.key-store",
s3Config,
null,
null,
List.of(
SecretAttribute.secretAttribute(
"password",
KEY,
S3Config::keyStorePassword,
S3Config.Builder::keyStorePassword)))
.build();

httpClient.tlsKeyManagersProvider(
new FileStoreTlsKeyManagersProvider(
p,
withPassword
.keyStoreType()
.orElseThrow(() -> new IllegalArgumentException("No key store type")),
withPassword.keyStorePassword().orElse(null)));
});
AttributeMap.Builder options = AttributeMap.builder();
s3Config.trustAllCertificates().ifPresent(v -> options.put(TRUST_ALL_CERTIFICATES, v));
return httpClient.buildWithDefaults(options.build());
}

public static AwsCredentialsProvider basicCredentialsProvider(
Expand Down Expand Up @@ -63,4 +144,60 @@ public static AwsCredentialsProvider awsCredentialsProvider(

return sessions.assumeRoleForServer(bucketOptions);
}

private static final class FileStoreTlsTrustManagersProvider implements TlsTrustManagersProvider {
private final Path path;
private final String type;
private final char[] password;

FileStoreTlsTrustManagersProvider(Path path, String type, KeySecret password) {
this.path = path;
this.type = type;
this.password = password != null ? password.key().toCharArray() : null;
}

@Override
public TrustManager[] trustManagers() {
try (InputStream storeInputStream = Files.newInputStream(path)) {
KeyStore keyStore = KeyStore.getInstance(type);
keyStore.load(storeInputStream, password);
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
return tmf.getTrustManagers();
} catch (KeyStoreException
| CertificateException
| NoSuchAlgorithmException
| IOException e) {
throw new RuntimeException(e);
}
}
}

private static final class FileStoreTlsKeyManagersProvider
extends AbstractFileStoreTlsKeyManagersProvider {

private final Path storePath;
private final String storeType;
private final char[] password;

FileStoreTlsKeyManagersProvider(Path storePath, String storeType, KeySecret password) {
this.storePath = Validate.paramNotNull(storePath, "storePath");
this.storeType = Validate.paramNotBlank(storeType, "storeType");
this.password = password != null ? password.key().toCharArray() : null;
}

@Override
public KeyManager[] keyManagers() {
try {
return createKeyManagers(storePath, storeType, password);
} catch (CertificateException
| UnrecoverableKeyException
| IOException
| KeyStoreException
| NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package org.projectnessie.catalog.files.s3;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;
import java.util.OptionalInt;
import org.immutables.value.Value;
import org.projectnessie.catalog.secrets.KeySecret;
import org.projectnessie.nessie.docgen.annotations.ConfigDocs.ConfigItem;

@Value.Immutable
Expand Down Expand Up @@ -55,6 +57,60 @@ public interface S3Config {
@ConfigItem(section = "transport")
Optional<Boolean> expectContinueEnabled();

/**
* Instruct the S3 HTTP client to accept all SSL certificates, if set to {@code true}. Enabling
* this option is dangerous, it is strongly recommended to leave this option unset or {@code
* false}.
*/
@ConfigItem(section = "transport")
Optional<Boolean> trustAllCertificates();

/**
* Override to set the file path to a custom SSL trust store. {@code
* nessie.catalog.service.s3.trust-store.type} and {@code
* nessie.catalog.service.s3.trust-store.password} must be supplied as well when providing a
* custom trust store.
*/
@ConfigItem(section = "transport")
Optional<Path> trustStorePath();

/**
* Override to set the type of the custom SSL trust store specified in {@code
* nessie.catalog.service.s3.trust-store.path}.
*/
@ConfigItem(section = "transport")
Optional<String> trustStoreType();

/**
* Override to set the password for the custom SSL trust store specified in {@code
* nessie.catalog.service.s3.trust-store.path}.
*/
@ConfigItem(section = "transport")
Optional<KeySecret> trustStorePassword();

/**
* Override to set the file path to a custom SSL key store. {@code
* nessie.catalog.service.s3.key-store.type} and {@code
* nessie.catalog.service.s3.key-store.password} must be supplied as well when providing a custom
* key store.
*/
@ConfigItem(section = "transport")
Optional<Path> keyStorePath();

/**
* Override to set the type of the custom SSL key store specified in {@code
* nessie.catalog.service.s3.key-store.path}.
*/
@ConfigItem(section = "transport")
Optional<String> keyStoreType();

/**
* Override to set the password for the custom SSL key store specified in {@code
* nessie.catalog.service.s3.key-store.path}.
*/
@ConfigItem(section = "transport")
Optional<KeySecret> keyStorePassword();

/**
* Interval after which a request is retried when S3 response with some "retry later" response.
*/
Expand All @@ -64,7 +120,11 @@ static Builder builder() {
return ImmutableS3Config.builder();
}

@SuppressWarnings("unused")
interface Builder {
@CanIgnoreReturnValue
Builder from(S3Config instance);

@CanIgnoreReturnValue
Builder maxHttpConnections(int maxHttpConnections);

Expand All @@ -86,6 +146,24 @@ interface Builder {
@CanIgnoreReturnValue
Builder expectContinueEnabled(boolean expectContinueEnabled);

@CanIgnoreReturnValue
Builder trustStorePath(Path trustStorePath);

@CanIgnoreReturnValue
Builder trustStoreType(String trustStoreType);

@CanIgnoreReturnValue
Builder trustStorePassword(KeySecret trustStorePassword);

@CanIgnoreReturnValue
Builder keyStorePath(Path keyStorePath);

@CanIgnoreReturnValue
Builder keyStoreType(String keyStoreType);

@CanIgnoreReturnValue
Builder keyStorePassword(KeySecret keyStorePassword);

@CanIgnoreReturnValue
Builder retryAfter(Duration retryAfter);

Expand Down
Loading

0 comments on commit ce377a1

Please sign in to comment.