From d33d19b87b616f8ac1bc1f087007b7d993f02244 Mon Sep 17 00:00:00 2001 From: Anu Sudarsan Date: Mon, 23 Dec 2024 13:54:37 -0600 Subject: [PATCH] Add SSE-C option on native-filesystem security mapping --- .../sphinx/object-storage/file-system-s3.md | 2 + .../io/trino/filesystem/s3/S3Context.java | 11 ++ .../filesystem/s3/S3FileSystemLoader.java | 6 + .../filesystem/s3/S3SecurityMapping.java | 20 +++ .../s3/S3SecurityMappingConfig.java | 14 ++ .../s3/S3SecurityMappingProvider.java | 31 ++++ .../s3/S3SecurityMappingResult.java | 2 + .../filesystem/s3/TestS3SecurityMapping.java | 147 ++++++++++++++++-- .../s3/TestS3SecurityMappingConfig.java | 5 + .../trino/filesystem/s3/security-mapping.json | 20 +++ 10 files changed, 248 insertions(+), 10 deletions(-) diff --git a/docs/src/main/sphinx/object-storage/file-system-s3.md b/docs/src/main/sphinx/object-storage/file-system-s3.md index 926d31cb870d..d9592d6776b0 100644 --- a/docs/src/main/sphinx/object-storage/file-system-s3.md +++ b/docs/src/main/sphinx/object-storage/file-system-s3.md @@ -262,6 +262,8 @@ Example JSON configuration: - The name of the *extra credential* used to provide the IAM role. * - `s3.security-mapping.kms-key-id-credential-name` - The name of the *extra credential* used to provide the KMS-managed key ID. +* - `s3.security-mapping.sse-customer-key-credential-name` + - The name of the *extra credential* used to provide the server-side encryption with customer-provided keys (SSE-C). * - `s3.security-mapping.refresh-period` - How often to refresh the security mapping configuration, specified as a {ref}`prop-type-duration`. By default, the configuration is not refreshed. diff --git a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3Context.java b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3Context.java index eeb33295d832..cc9dbf4ffeeb 100644 --- a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3Context.java +++ b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3Context.java @@ -25,6 +25,7 @@ import java.util.Optional; import static com.google.common.base.Preconditions.checkArgument; +import static io.trino.filesystem.s3.S3FileSystemConfig.S3SseType.CUSTOMER; import static io.trino.filesystem.s3.S3FileSystemConfig.S3SseType.KMS; import static io.trino.filesystem.s3.S3FileSystemConstants.EXTRA_CREDENTIALS_ACCESS_KEY_PROPERTY; import static io.trino.filesystem.s3.S3FileSystemConstants.EXTRA_CREDENTIALS_SECRET_KEY_PROPERTY; @@ -70,6 +71,11 @@ public S3Context withCredentials(ConnectorIdentity identity) return this; } + public S3Context withSseCustomerKey(String key) + { + return new S3Context(partSize, requesterPays, S3SseContext.withSseCustomerKey(key), credentialsProviderOverride, cannedAcl, exclusiveWriteSupported); + } + public S3Context withCredentialsProviderOverride(AwsCredentialsProvider credentialsProviderOverride) { return new S3Context( @@ -109,5 +115,10 @@ public static S3SseContext withKmsKeyId(String kmsKeyId) { return new S3SseContext(KMS, Optional.ofNullable(kmsKeyId), Optional.empty()); } + + public static S3SseContext withSseCustomerKey(String key) + { + return new S3SseContext(CUSTOMER, Optional.empty(), Optional.ofNullable(key).map(S3SseCustomerKey::onAes256)); + } } } diff --git a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3FileSystemLoader.java b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3FileSystemLoader.java index bb011a336f5c..be1531030181 100644 --- a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3FileSystemLoader.java +++ b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3FileSystemLoader.java @@ -46,6 +46,7 @@ import java.util.concurrent.ExecutorService; import java.util.function.Function; +import static com.google.common.base.Preconditions.checkState; import static io.airlift.concurrent.Threads.daemonThreadsNamed; import static io.trino.filesystem.s3.S3FileSystemConfig.RetryMode.getRetryStrategy; import static java.lang.Math.toIntExact; @@ -108,9 +109,14 @@ public TrinoFileSystemFactory apply(Location location) S3Context context = this.context.withCredentials(identity); if (mapping.isPresent() && mapping.get().kmsKeyId().isPresent()) { + checkState(mapping.get().sseCustomerKey().isEmpty(), "Both SSE-C and KMS-managed keys cannot be used at the same time"); context = context.withKmsKeyId(mapping.get().kmsKeyId().get()); } + if (mapping.isPresent() && mapping.get().sseCustomerKey().isPresent()) { + context = context.withSseCustomerKey(mapping.get().sseCustomerKey().get()); + } + return new S3FileSystem(uploadExecutor, client, preSigner, context); }; } diff --git a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMapping.java b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMapping.java index 4b65e5fce887..064040d06295 100644 --- a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMapping.java +++ b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMapping.java @@ -42,6 +42,8 @@ public final class S3SecurityMapping private final Set allowedIamRoles; private final Optional kmsKeyId; private final Set allowedKmsKeyIds; + private final Optional sseCustomerKey; + private final Set allowedSseCustomerKeys; private final Optional credentials; private final boolean useClusterDefault; private final Optional endpoint; @@ -57,6 +59,8 @@ public S3SecurityMapping( @JsonProperty("allowedIamRoles") Optional> allowedIamRoles, @JsonProperty("kmsKeyId") Optional kmsKeyId, @JsonProperty("allowedKmsKeyIds") Optional> allowedKmsKeyIds, + @JsonProperty("sseCustomerKey") Optional sseCustomerKey, + @JsonProperty("allowedSseCustomerKeys") Optional> allowedSseCustomerKeys, @JsonProperty("accessKey") Optional accessKey, @JsonProperty("secretKey") Optional secretKey, @JsonProperty("useClusterDefault") Optional useClusterDefault, @@ -86,6 +90,10 @@ public S3SecurityMapping( this.allowedKmsKeyIds = ImmutableSet.copyOf(allowedKmsKeyIds.orElse(ImmutableList.of())); + this.sseCustomerKey = requireNonNull(sseCustomerKey, "sseCustomerKey is null"); + + this.allowedSseCustomerKeys = allowedSseCustomerKeys.map(ImmutableSet::copyOf).orElse(ImmutableSet.of()); + requireNonNull(accessKey, "accessKey is null"); requireNonNull(secretKey, "secretKey is null"); checkArgument(accessKey.isPresent() == secretKey.isPresent(), "accessKey and secretKey must be provided together"); @@ -96,6 +104,8 @@ public S3SecurityMapping( checkArgument(this.useClusterDefault != roleOrCredentialsArePresent, "must either allow useClusterDefault role or provide role and/or credentials"); checkArgument(!this.useClusterDefault || this.kmsKeyId.isEmpty(), "KMS key ID cannot be provided together with useClusterDefault"); + checkArgument(!this.useClusterDefault || this.sseCustomerKey.isEmpty(), "SSE Customer key cannot be provided together with useClusterDefault"); + checkArgument(this.kmsKeyId.isEmpty() || this.sseCustomerKey.isEmpty(), "SSE Customer key cannot be provided together with KMS key ID"); this.endpoint = requireNonNull(endpoint, "endpoint is null"); this.region = requireNonNull(region, "region is null"); @@ -133,6 +143,16 @@ public Set allowedKmsKeyIds() return allowedKmsKeyIds; } + public Optional sseCustomerKey() + { + return sseCustomerKey; + } + + public Set allowedSseCustomerKeys() + { + return allowedSseCustomerKeys; + } + public Optional credentials() { return credentials; diff --git a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingConfig.java b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingConfig.java index 2f1d6062f7cb..8bf2b8cbe499 100644 --- a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingConfig.java +++ b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingConfig.java @@ -31,6 +31,7 @@ public class S3SecurityMappingConfig private String jsonPointer = ""; private String roleCredentialName; private String kmsKeyIdCredentialName; + private String sseCustomerKeyCredentialName; private Duration refreshPeriod; private String colonReplacement; @@ -100,6 +101,19 @@ public S3SecurityMappingConfig setKmsKeyIdCredentialName(String kmsKeyIdCredenti return this; } + public Optional getSseCustomerKeyCredentialName() + { + return Optional.ofNullable(sseCustomerKeyCredentialName); + } + + @Config("s3.security-mapping.sse-customer-key-credential-name") + @ConfigDescription("Name of the extra credential used to provide SSE Customer key") + public S3SecurityMappingConfig setSseCustomerKeyCredentialName(String sseCustomerKeyCredentialName) + { + this.sseCustomerKeyCredentialName = sseCustomerKeyCredentialName; + return this; + } + public Optional getRefreshPeriod() { return Optional.ofNullable(refreshPeriod); diff --git a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingProvider.java b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingProvider.java index e0823c2c5fcc..008fb835f6bf 100644 --- a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingProvider.java +++ b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingProvider.java @@ -33,6 +33,7 @@ final class S3SecurityMappingProvider private final Supplier mappingsProvider; private final Optional roleCredentialName; private final Optional kmsKeyIdCredentialName; + private final Optional sseCustomerKeyCredentialName; private final Optional colonReplacement; @Inject @@ -41,6 +42,7 @@ public S3SecurityMappingProvider(S3SecurityMappingConfig config, Supplier mappingsProvider, Optional roleCredentialName, Optional kmsKeyIdCredentialName, + Optional sseCustomerKeyCredentialName, Optional colonReplacement) { this.mappingsProvider = requireNonNull(mappingsProvider, "mappingsProvider is null"); this.roleCredentialName = requireNonNull(roleCredentialName, "roleCredentialName is null"); this.kmsKeyIdCredentialName = requireNonNull(kmsKeyIdCredentialName, "kmsKeyIdCredentialName is null"); + this.sseCustomerKeyCredentialName = requireNonNull(sseCustomerKeyCredentialName, "customerKeyCredentialName is null"); this.colonReplacement = requireNonNull(colonReplacement, "colonReplacement is null"); } @@ -70,6 +74,7 @@ public Optional getMapping(ConnectorIdentity identity, selectRole(mapping, identity), mapping.roleSessionName().map(name -> name.replace("${USER}", identity.getUser())), selectKmsKeyId(mapping, identity), + getSseCustomerKey(mapping, identity), mapping.endpoint(), mapping.region())); } @@ -131,6 +136,32 @@ private Optional getKmsKeyIdFromExtraCredential(ConnectorIdentity identi return kmsKeyIdCredentialName.map(name -> identity.getExtraCredentials().get(name)); } + private Optional getSseCustomerKey(S3SecurityMapping mapping, ConnectorIdentity identity) + { + Optional providedKey = getSseCustomerKeyFromExtraCredential(identity); + + if (providedKey.isEmpty()) { + return mapping.sseCustomerKey(); + } + if (mapping.sseCustomerKey().isPresent() && mapping.allowedSseCustomerKeys().isEmpty()) { + throw new AccessDeniedException("allowedSseCustomerKeys must be set if sseCustomerKey is provided"); + } + + String selected = providedKey.get(); + + if (selected.equals(mapping.sseCustomerKey().orElse(null)) || + mapping.allowedSseCustomerKeys().contains(selected) || + mapping.allowedSseCustomerKeys().contains("*")) { + return providedKey; + } + throw new AccessDeniedException("Provided SSE Customer Key is not allowed"); + } + + private Optional getSseCustomerKeyFromExtraCredential(ConnectorIdentity identity) + { + return sseCustomerKeyCredentialName.map(name -> identity.getExtraCredentials().get(name)); + } + private static Supplier mappingsProvider(Supplier supplier, Optional refreshPeriod) { return refreshPeriod diff --git a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingResult.java b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingResult.java index 23514f2d1a83..2d2ef7dc2e92 100644 --- a/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingResult.java +++ b/lib/trino-filesystem-s3/src/main/java/io/trino/filesystem/s3/S3SecurityMappingResult.java @@ -26,6 +26,7 @@ record S3SecurityMappingResult( Optional iamRole, Optional roleSessionName, Optional kmsKeyId, + Optional sseCustomerKey, Optional endpoint, Optional region) { @@ -35,6 +36,7 @@ record S3SecurityMappingResult( requireNonNull(iamRole, "iamRole is null"); requireNonNull(roleSessionName, "roleSessionName is null"); requireNonNull(kmsKeyId, "kmsKeyId is null"); + requireNonNull(sseCustomerKey, "sseCustomerKey is null"); requireNonNull(endpoint, "endpoint is null"); requireNonNull(region, "region is null"); } diff --git a/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMapping.java b/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMapping.java index f301a83d9234..e4d60e09c060 100644 --- a/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMapping.java +++ b/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMapping.java @@ -37,6 +37,7 @@ public class TestS3SecurityMapping { private static final String IAM_ROLE_CREDENTIAL_NAME = "IAM_ROLE_CREDENTIAL_NAME"; private static final String KMS_KEY_ID_CREDENTIAL_NAME = "KMS_KEY_ID_CREDENTIAL_NAME"; + private static final String CUSTOMER_KEY_CREDENTIAL_NAME = "CUSTOMER_KEY_CREDENTIAL_NAME"; private static final String DEFAULT_PATH = "s3://default/"; private static final String DEFAULT_USER = "testuser"; @@ -47,6 +48,7 @@ public void testMapping() .setConfigFile(getResourceFile("security-mapping.json")) .setRoleCredentialName(IAM_ROLE_CREDENTIAL_NAME) .setKmsKeyIdCredentialName(KMS_KEY_ID_CREDENTIAL_NAME) + .setSseCustomerKeyCredentialName(CUSTOMER_KEY_CREDENTIAL_NAME) .setColonReplacement("#"); var provider = new S3SecurityMappingProvider(mappingConfig, new S3SecurityMappingsFileSource(mappingConfig)); @@ -104,6 +106,45 @@ public void testMapping() credentials("AKIAxxxaccess", "iXbXxxxsecret") .withKmsKeyId("kmsKey_12")); + // matches prefix exactly -- mapping provides credentials, customer key from extra credentials matching default + assertMapping( + provider, + path("s3://baz/") + .withExtraCredentialCustomerKey("customerKey_10"), + credentials("AKIAxxxaccess", "iXbXxxxsecret") + .withCustomerKey("customerKey_10")); + + // matches prefix exactly -- mapping provides credentials, customer key from extra credentials, allowed, different from default + assertMapping( + provider, + path("s3://baz/") + .withExtraCredentialCustomerKey("customerKey_11"), + credentials("AKIAxxxaccess", "iXbXxxxsecret") + .withCustomerKey("customerKey_11")); + + // matches prefix exactly -- mapping provides credentials, customer key from extra credentials, not allowed + assertMappingFails( + provider, + path("s3://baz/") + .withExtraCredentialCustomerKey("customerKey_not_allowed"), + "Provided SSE Customer Key is not allowed"); + + // matches prefix exactly -- mapping provides credentials, customer key from extra credentials, all keys are allowed, different from default + assertMapping( + provider, + path("s3://baz_all_customer_keys_allowed/") + .withExtraCredentialCustomerKey("customerKey_777"), + credentials("AKIAxxxaccess", "iXbXxxxsecret") + .withCustomerKey("customerKey_777")); + + // matches prefix exactly -- mapping provides credentials, customer key from extra credentials, allowed, no default key + assertMapping( + provider, + path("s3://baz_no_customer_default_key/") + .withExtraCredentialCustomerKey("customerKey_12"), + credentials("AKIAxxxaccess", "iXbXxxxsecret") + .withCustomerKey("customerKey_12")); + // no role selected and mapping has no default role assertMappingFails( provider, @@ -320,6 +361,8 @@ public void testMappingWithoutRoleCredentialsFallbackShouldFail() Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), + Optional.empty(), Optional.empty())) .isInstanceOf(IllegalArgumentException.class) .hasMessage("must either allow useClusterDefault role or provide role and/or credentials"); @@ -343,6 +386,8 @@ public void testMappingWithRoleAndFallbackShouldFail() Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), + Optional.empty(), useClusterDefault, Optional.empty(), Optional.empty())) @@ -368,6 +413,8 @@ public void testMappingWithEncryptionKeysAndFallbackShouldFail() Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), + Optional.empty(), useClusterDefault, Optional.empty(), Optional.empty())) @@ -375,6 +422,60 @@ public void testMappingWithEncryptionKeysAndFallbackShouldFail() .hasMessage("KMS key ID cannot be provided together with useClusterDefault"); } + @Test + public void testMappingWithSseCustomerKeyAndFallbackShouldFail() + { + Optional useClusterDefault = Optional.of(true); + Optional customerKey = Optional.of("CLIENT_S3CRT_CUSTOMER_KEY"); + + assertThatThrownBy(() -> + new S3SecurityMapping( + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + customerKey, + Optional.empty(), + Optional.empty(), + Optional.empty(), + useClusterDefault, + Optional.empty(), + Optional.empty())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("SSE Customer key cannot be provided together with useClusterDefault"); + } + + @Test + public void testMappingWithSseCustomerAndKMSKeysShouldFail() + { + Optional kmsKeyId = Optional.of("CLIENT_S3CRT_KEY_ID"); + Optional customerKey = Optional.of("CLIENT_S3CRT_CUSTOMER_KEY"); + + assertThatThrownBy(() -> + new S3SecurityMapping( + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of("arn:aws:iam::123456789101:role/allow_path"), + Optional.empty(), + Optional.empty(), + kmsKeyId, + Optional.empty(), + customerKey, + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("SSE Customer key cannot be provided together with KMS key ID"); + } + @Test public void testMappingWithRoleSessionNameWithoutIamRoleShouldFail() { @@ -394,6 +495,8 @@ public void testMappingWithRoleSessionNameWithoutIamRoleShouldFail() Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), + Optional.empty(), Optional.empty())) .isInstanceOf(IllegalArgumentException.class) .hasMessage("iamRole must be provided when roleSessionName is provided"); @@ -414,6 +517,7 @@ private static void assertMapping(S3SecurityMappingProvider provider, MappingSel assertThat(actual.iamRole()).isEqualTo(expected.iamRole()); assertThat(actual.roleSessionName()).isEqualTo(expected.roleSessionName()); assertThat(actual.kmsKeyId()).isEqualTo(expected.kmsKeyId()); + assertThat(actual.sseCustomerKey()).isEqualTo(expected.customerKey()); assertThat(actual.endpoint()).isEqualTo(expected.endpoint()); assertThat(actual.region()).isEqualTo(expected.region()); }); @@ -440,7 +544,7 @@ public static MappingSelector empty() public static MappingSelector path(String location) { - return new MappingSelector(DEFAULT_USER, ImmutableSet.of(), Location.of(location), Optional.empty(), Optional.empty()); + return new MappingSelector(DEFAULT_USER, ImmutableSet.of(), Location.of(location), Optional.empty(), Optional.empty(), Optional.empty()); } private final String user; @@ -448,14 +552,16 @@ public static MappingSelector path(String location) private final Location location; private final Optional extraCredentialIamRole; private final Optional extraCredentialKmsKeyId; + private final Optional extraCredentialCustomerKey; - private MappingSelector(String user, Set groups, Location location, Optional extraCredentialIamRole, Optional extraCredentialKmsKeyId) + private MappingSelector(String user, Set groups, Location location, Optional extraCredentialIamRole, Optional extraCredentialKmsKeyId, Optional extraCredentialCustomerKey) { this.user = requireNonNull(user, "user is null"); this.groups = ImmutableSet.copyOf(requireNonNull(groups, "groups is null")); this.location = requireNonNull(location, "location is null"); this.extraCredentialIamRole = requireNonNull(extraCredentialIamRole, "extraCredentialIamRole is null"); this.extraCredentialKmsKeyId = requireNonNull(extraCredentialKmsKeyId, "extraCredentialKmsKeyId is null"); + this.extraCredentialCustomerKey = requireNonNull(extraCredentialCustomerKey, "extraCredentialCustomerKey is null"); } public Location location() @@ -465,22 +571,27 @@ public Location location() public MappingSelector withExtraCredentialIamRole(String role) { - return new MappingSelector(user, groups, location, Optional.of(role), extraCredentialKmsKeyId); + return new MappingSelector(user, groups, location, Optional.of(role), extraCredentialKmsKeyId, extraCredentialCustomerKey); } public MappingSelector withExtraCredentialKmsKeyId(String kmsKeyId) { - return new MappingSelector(user, groups, location, extraCredentialIamRole, Optional.of(kmsKeyId)); + return new MappingSelector(user, groups, location, extraCredentialIamRole, Optional.of(kmsKeyId), Optional.empty()); + } + + public MappingSelector withExtraCredentialCustomerKey(String customerKey) + { + return new MappingSelector(user, groups, location, extraCredentialIamRole, Optional.empty(), Optional.of(customerKey)); } public MappingSelector withUser(String user) { - return new MappingSelector(user, groups, location, extraCredentialIamRole, extraCredentialKmsKeyId); + return new MappingSelector(user, groups, location, extraCredentialIamRole, extraCredentialKmsKeyId, extraCredentialCustomerKey); } public MappingSelector withGroups(String... groups) { - return new MappingSelector(user, ImmutableSet.copyOf(groups), location, extraCredentialIamRole, extraCredentialKmsKeyId); + return new MappingSelector(user, ImmutableSet.copyOf(groups), location, extraCredentialIamRole, extraCredentialKmsKeyId, extraCredentialCustomerKey); } public ConnectorIdentity identity() @@ -488,6 +599,7 @@ public ConnectorIdentity identity() Map extraCredentials = new HashMap<>(); extraCredentialIamRole.ifPresent(role -> extraCredentials.put(IAM_ROLE_CREDENTIAL_NAME, role)); extraCredentialKmsKeyId.ifPresent(kmsKeyId -> extraCredentials.put(KMS_KEY_ID_CREDENTIAL_NAME, kmsKeyId)); + extraCredentialCustomerKey.ifPresent(customerKey -> extraCredentials.put(CUSTOMER_KEY_CREDENTIAL_NAME, customerKey)); return ConnectorIdentity.forUser(user) .withGroups(groups) @@ -507,6 +619,7 @@ public static MappingResult credentials(String accessKey, String secretKey) Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -519,6 +632,7 @@ public static MappingResult iamRole(String role) Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty()); } @@ -527,6 +641,7 @@ public static MappingResult iamRole(String role) private final Optional iamRole; private final Optional roleSessionName; private final Optional kmsKeyId; + private final Optional customerKey; private final Optional endpoint; private final Optional region; @@ -536,6 +651,7 @@ private MappingResult( Optional iamRole, Optional roleSessionName, Optional kmsKeyId, + Optional customerKey, Optional endpoint, Optional region) { @@ -543,6 +659,7 @@ private MappingResult( this.secretKey = requireNonNull(secretKey, "secretKey is null"); this.iamRole = requireNonNull(iamRole, "role is null"); this.kmsKeyId = requireNonNull(kmsKeyId, "kmsKeyId is null"); + this.customerKey = requireNonNull(customerKey, "sseCustomerKey is null"); this.endpoint = requireNonNull(endpoint, "endpoint is null"); this.roleSessionName = requireNonNull(roleSessionName, "roleSessionName is null"); this.region = requireNonNull(region, "region is null"); @@ -550,22 +667,27 @@ private MappingResult( public MappingResult withEndpoint(String endpoint) { - return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), kmsKeyId, Optional.of(endpoint), region); + return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), kmsKeyId, customerKey, Optional.of(endpoint), region); } public MappingResult withKmsKeyId(String kmsKeyId) { - return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), Optional.of(kmsKeyId), endpoint, region); + return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), Optional.of(kmsKeyId), Optional.empty(), endpoint, region); + } + + public MappingResult withCustomerKey(String customerKey) + { + return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), Optional.empty(), Optional.of(customerKey), endpoint, region); } public MappingResult withRegion(String region) { - return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), kmsKeyId, endpoint, Optional.of(region)); + return new MappingResult(accessKey, secretKey, iamRole, Optional.empty(), kmsKeyId, customerKey, endpoint, Optional.of(region)); } public MappingResult withRoleSessionName(String roleSessionName) { - return new MappingResult(accessKey, secretKey, iamRole, Optional.of(roleSessionName), kmsKeyId, Optional.empty(), region); + return new MappingResult(accessKey, secretKey, iamRole, Optional.of(roleSessionName), kmsKeyId, customerKey, Optional.empty(), region); } public Optional accessKey() @@ -593,6 +715,11 @@ public Optional kmsKeyId() return kmsKeyId; } + public Optional customerKey() + { + return customerKey; + } + public Optional endpoint() { return endpoint; diff --git a/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMappingConfig.java b/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMappingConfig.java index 0aabecd00745..ec51e87fd5bf 100644 --- a/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMappingConfig.java +++ b/lib/trino-filesystem-s3/src/test/java/io/trino/filesystem/s3/TestS3SecurityMappingConfig.java @@ -44,6 +44,7 @@ public void testDefaults() .setConfigUri(null) .setRoleCredentialName(null) .setKmsKeyIdCredentialName(null) + .setSseCustomerKeyCredentialName(null) .setRefreshPeriod(null) .setColonReplacement(null)); } @@ -59,6 +60,7 @@ public void testExplicitPropertyMappingsWithFile() .put("s3.security-mapping.json-pointer", "/data") .put("s3.security-mapping.iam-role-credential-name", "iam-role-credential-name") .put("s3.security-mapping.kms-key-id-credential-name", "kms-key-id-credential-name") + .put("s3.security-mapping.sse-customer-key-credential-name", "sse-customer-key-credential-name") .put("s3.security-mapping.refresh-period", "13s") .put("s3.security-mapping.colon-replacement", "#") .buildOrThrow(); @@ -71,6 +73,7 @@ public void testExplicitPropertyMappingsWithFile() assertThat(config.getJsonPointer()).isEqualTo("/data"); assertThat(config.getRoleCredentialName()).contains("iam-role-credential-name"); assertThat(config.getKmsKeyIdCredentialName()).contains("kms-key-id-credential-name"); + assertThat(config.getSseCustomerKeyCredentialName()).contains("sse-customer-key-credential-name"); assertThat(config.getRefreshPeriod()).contains(new Duration(13, SECONDS)); assertThat(config.getColonReplacement()).contains("#"); } @@ -83,6 +86,7 @@ public void testExplicitPropertyMappingsWithUrl() .put("s3.security-mapping.json-pointer", "/data") .put("s3.security-mapping.iam-role-credential-name", "iam-role-credential-name") .put("s3.security-mapping.kms-key-id-credential-name", "kms-key-id-credential-name") + .put("s3.security-mapping.sse-customer-key-credential-name", "sse-customer-key-credential-name") .put("s3.security-mapping.refresh-period", "13s") .put("s3.security-mapping.colon-replacement", "#") .buildOrThrow(); @@ -95,6 +99,7 @@ public void testExplicitPropertyMappingsWithUrl() assertThat(config.getJsonPointer()).isEqualTo("/data"); assertThat(config.getRoleCredentialName()).contains("iam-role-credential-name"); assertThat(config.getKmsKeyIdCredentialName()).contains("kms-key-id-credential-name"); + assertThat(config.getSseCustomerKeyCredentialName()).contains("sse-customer-key-credential-name"); assertThat(config.getRefreshPeriod()).contains(new Duration(13, SECONDS)); assertThat(config.getColonReplacement()).contains("#"); } diff --git a/lib/trino-filesystem-s3/src/test/resources/io/trino/filesystem/s3/security-mapping.json b/lib/trino-filesystem-s3/src/test/resources/io/trino/filesystem/s3/security-mapping.json index a9ddf6e5638d..44e0d58d4a59 100644 --- a/lib/trino-filesystem-s3/src/test/resources/io/trino/filesystem/s3/security-mapping.json +++ b/lib/trino-filesystem-s3/src/test/resources/io/trino/filesystem/s3/security-mapping.json @@ -40,6 +40,26 @@ "secretKey": "iXbXxxxsecret", "allowedKmsKeyIds": ["kmsKey_11", "kmsKey_12"] }, + { + "prefix": "s3://baz/", + "accessKey": "AKIAxxxaccess", + "secretKey": "iXbXxxxsecret", + "sseCustomerKey": "customerKey_10", + "allowedSseCustomerKeys": ["customerKey_11"] + }, + { + "prefix": "s3://baz_all_customer_keys_allowed/", + "accessKey": "AKIAxxxaccess", + "secretKey": "iXbXxxxsecret", + "sseCustomerKey": "customerKey_10", + "allowedSseCustomerKeys": ["*"] + }, + { + "prefix": "s3://baz_no_customer_default_key/", + "accessKey": "AKIAxxxaccess", + "secretKey": "iXbXxxxsecret", + "allowedSseCustomerKeys": ["customerKey_11", "customerKey_12"] + }, { "user": "alice", "iamRole": "alice_role"