This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new f09470d3ce JAMES-4085 [S3 SSEC] Guice binding & document (#2495)
f09470d3ce is described below
commit f09470d3ce7d466781567eb188cdcedd0c15c3c9
Author: vttran <[email protected]>
AuthorDate: Fri Nov 29 09:17:23 2024 +0700
JAMES-4085 [S3 SSEC] Guice binding & document (#2495)
---
.../servers/partials/configure/blobstore.adoc | 20 +++++++
.../java/org/apache/james/WithS3SSECTest.java} | 29 +++++-----
.../james/modules/S3SSECBlobStoreExtension.java} | 56 +++++-------------
.../aws/S3BlobStoreConfiguration.java | 66 ++++++++++++++++++----
.../blob/objectstorage/aws/S3BlobStoreDAO.java | 6 --
.../blob/objectstorage/aws/S3RequestOption.java | 2 +-
.../objectstorage/aws/sse/S3SSECConfiguration.java | 18 +++++-
.../aws/S3BlobStoreConfigurationTest.java | 63 +++++++++++++++++++++
.../blob/objectstorage/aws/S3BlobStoreDAOTest.java | 2 +-
.../aws/S3DeDuplicationBlobStoreTest.java | 2 +-
.../blob/objectstorage/aws/S3HealthCheckTest.java | 2 +-
.../james/blob/objectstorage/aws/S3MinioTest.java | 2 +-
.../blob/objectstorage/aws/S3NamespaceTest.java | 2 +-
.../aws/S3PassThroughBlobStoreTest.java | 2 +-
.../aws/S3PrefixAndNamespaceTest.java | 2 +-
.../james/blob/objectstorage/aws/S3PrefixTest.java | 2 +-
.../S3BlobStoreConfigurationReader.java | 13 ++++-
.../objectstorage/aws/s3/DockerAwsS3TestRule.java | 2 +
.../modules/blobstore/BlobStoreModulesChooser.java | 21 +++++++
src/site/xdoc/server/config-blobstore.xml | 28 +++++++++
20 files changed, 253 insertions(+), 87 deletions(-)
diff --git a/docs/modules/servers/partials/configure/blobstore.adoc
b/docs/modules/servers/partials/configure/blobstore.adoc
index e928386bbb..e6cfb4c725 100644
--- a/docs/modules/servers/partials/configure/blobstore.adoc
+++ b/docs/modules/servers/partials/configure/blobstore.adoc
@@ -126,6 +126,26 @@ BucketPrefix is the prefix of bucket names in James
BlobStore
Unless a special case like storing blobs of deleted messages.
|===
+==== SSE-C Configuration
+
+.SSE-C configuration
+|===
+| Property name | explanation
+
+| encryption.s3.sse.c.enable
+| optional: Boolean. Default is false. Controls whether to use Server-Side
Encryption with Customer-Provided Keys (SSE-C) for S3 blobs.
+
+| encryption.s3.sse.c.master.key.algorithm
+| String. Required if `encryption.s3.sse.c.enable` is true. The algorithm used
to derive the master key from the provided password. Eg: AES256
+
+| encryption.s3.sse.c.master.key.password
+| String. Required if `encryption.s3.sse.c.enable` is true. The password used
to generate the customer key.
+
+| encryption.s3.sse.c.master.key.salt
+| String. Required if `encryption.s3.sse.c.enable` is true. The salt used to
generate the customer key.
+
+|===
+
== Blob Export
Blob Exporting is the mechanism to help James to export a blob from an user to
another user.
diff --git
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
b/server/apps/distributed-app/src/test/java/org/apache/james/WithS3SSECTest.java
similarity index 60%
copy from
server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
copy to
server/apps/distributed-app/src/test/java/org/apache/james/WithS3SSECTest.java
index 5de59834d2..09fecb3b2e 100644
---
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
+++
b/server/apps/distributed-app/src/test/java/org/apache/james/WithS3SSECTest.java
@@ -17,22 +17,19 @@
* under the License. *
****************************************************************/
-package org.apache.james.blob.objectstorage.aws;
+package org.apache.james;
-import java.util.Optional;
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECConfiguration;
+import org.apache.james.jmap.JmapJamesServerContract;
+import org.apache.james.modules.S3SSECBlobStoreExtension;
+import org.junit.jupiter.api.extension.RegisterExtension;
-import org.apache.james.blob.objectstorage.aws.sse.S3SSECustomerKeyFactory;
+public class WithS3SSECTest implements JmapJamesServerContract,
MailsShouldBeWellReceivedConcreteContract {
+ static S3SSECConfiguration s3SSECConfiguration = new
S3SSECConfiguration.Basic("AES256", "masterPassword", "salt");
-import com.google.common.base.Preconditions;
-
-public record S3RequestOption(SSEC ssec) {
- static S3RequestOption DEFAULT = new
S3RequestOption(S3RequestOption.SSEC.DISABLED);
-
- public record SSEC(boolean enable,
java.util.Optional<S3SSECustomerKeyFactory> sseCustomerKeyFactory) {
- static S3RequestOption.SSEC DISABLED = new S3RequestOption.SSEC(false,
Optional.empty());
-
- public SSEC {
- Preconditions.checkArgument(!enable ||
sseCustomerKeyFactory.isPresent(), "SSE Customer Key Factory must be present
when SSE is enabled");
- }
- }
-}
+ @RegisterExtension
+ static JamesServerExtension jamesServerExtension =
CassandraRabbitMQJamesServerFixture.baseExtensionBuilder()
+ .extension(new S3SSECBlobStoreExtension(s3SSECConfiguration))
+ .lifeCycle(JamesServerExtension.Lifecycle.PER_TEST)
+ .build();
+}
\ No newline at end of file
diff --git
a/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
b/server/apps/distributed-app/src/test/java/org/apache/james/modules/S3SSECBlobStoreExtension.java
similarity index 64%
copy from
server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
copy to
server/apps/distributed-app/src/test/java/org/apache/james/modules/S3SSECBlobStoreExtension.java
index f001461452..aefb4adbd1 100644
---
a/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
+++
b/server/apps/distributed-app/src/test/java/org/apache/james/modules/S3SSECBlobStoreExtension.java
@@ -17,77 +17,51 @@
* under the License. *
****************************************************************/
-package org.apache.james.modules.objectstorage.aws.s3;
+package org.apache.james.modules;
import java.util.UUID;
-import org.apache.james.GuiceModuleTestRule;
+import org.apache.james.GuiceModuleTestExtension;
import org.apache.james.blob.api.BucketName;
import org.apache.james.blob.objectstorage.aws.AwsS3AuthConfiguration;
import org.apache.james.blob.objectstorage.aws.DockerAwsS3Container;
-import org.apache.james.blob.objectstorage.aws.DockerAwsS3Singleton;
import org.apache.james.blob.objectstorage.aws.Region;
import org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
+import org.apache.james.blob.objectstorage.aws.S3MinioDocker;
+import org.apache.james.blob.objectstorage.aws.S3MinioExtension;
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECConfiguration;
import com.google.inject.Module;
-public class DockerAwsS3TestRule implements GuiceModuleTestRule {
+public class S3SSECBlobStoreExtension extends S3MinioExtension implements
GuiceModuleTestExtension {
- public DockerAwsS3TestRule() {
- }
-
- @Override
- public Statement apply(Statement base, Description description) {
- return new Statement() {
- @Override
- public void evaluate() throws Throwable {
- ensureAwsS3started();
- base.evaluate();
- }
- };
- }
-
- private void ensureAwsS3started() {
- DockerAwsS3Singleton.singleton.dockerAwsS3();
- }
+ private final S3SSECConfiguration ssecConfiguration;
- @Override
- public void await() {
+ public S3SSECBlobStoreExtension(S3SSECConfiguration ssecConfiguration) {
+ this.ssecConfiguration = ssecConfiguration;
}
@Override
public Module getModule() {
+ S3MinioDocker s3MinioDocker = minioDocker();
BucketName defaultBucketName =
BucketName.of(UUID.randomUUID().toString());
- AwsS3AuthConfiguration authConfiguration =
AwsS3AuthConfiguration.builder()
- .endpoint(DockerAwsS3Singleton.singleton.getEndpoint())
- .accessKeyId(DockerAwsS3Container.ACCESS_KEY_ID)
- .secretKey(DockerAwsS3Container.SECRET_ACCESS_KEY)
- .build();
+ AwsS3AuthConfiguration awsS3AuthConfiguration =
s3MinioDocker.getAwsS3AuthConfiguration();
Region region = DockerAwsS3Container.REGION;
S3BlobStoreConfiguration configuration =
S3BlobStoreConfiguration.builder()
- .authConfiguration(authConfiguration)
+ .authConfiguration(awsS3AuthConfiguration)
.region(region)
.defaultBucketName(defaultBucketName)
.bucketPrefix(UUID.randomUUID().toString())
+ .ssecEnabled()
+ .ssecConfiguration(ssecConfiguration)
.build();
return binder -> {
binder.bind(BucketName.class).toInstance(defaultBucketName);
binder.bind(Region.class).toInstance(region);
-
binder.bind(AwsS3AuthConfiguration.class).toInstance(authConfiguration);
+
binder.bind(AwsS3AuthConfiguration.class).toInstance(awsS3AuthConfiguration);
binder.bind(S3BlobStoreConfiguration.class).toInstance(configuration);
};
}
-
- public void start() {
- ensureAwsS3started();
- }
-
- public void stop() {
- //nothing to stop
- }
}
-
diff --git
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfiguration.java
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfiguration.java
index 6cc71ed23a..407592bfe3 100644
---
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfiguration.java
+++
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfiguration.java
@@ -23,10 +23,13 @@ import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
+import org.apache.commons.configuration2.Configuration;
import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECConfiguration;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import reactor.util.retry.Retry;
@@ -50,6 +53,15 @@ public class S3BlobStoreConfiguration {
ReadyToBuild region(Region region);
}
+ @FunctionalInterface
+ interface RequireSSECConfiguration {
+ ReadyToBuild ssecConfiguration(S3SSECConfiguration
ssecConfiguration);
+
+ default ReadyToBuild ssecConfiguration(Configuration
configuration) {
+ return
ssecConfiguration(S3SSECConfiguration.from(configuration));
+ }
+ }
+
class ReadyToBuild {
private final AwsS3AuthConfiguration specificAuthConfiguration;
@@ -63,6 +75,8 @@ public class S3BlobStoreConfiguration {
private Optional<Long> inMemoryReadLimit;
private Region region;
private Optional<Retry> uploadRetrySpec;
+ private boolean ssecEnabled;
+ private Optional<S3SSECConfiguration> ssecConfiguration =
Optional.empty();
public ReadyToBuild(AwsS3AuthConfiguration
specificAuthConfiguration, Region region) {
this.specificAuthConfiguration = specificAuthConfiguration;
@@ -127,10 +141,25 @@ public class S3BlobStoreConfiguration {
return this;
}
+ public RequireSSECConfiguration ssecEnabled() {
+ this.ssecEnabled = true;
+ return ssecConfiguration -> {
+ Preconditions.checkArgument(ssecConfiguration != null,
"SSEC configuration is mandatory when SSEC is enabled");
+ this.ssecConfiguration = Optional.of(ssecConfiguration);
+ return this;
+ };
+ }
+
+ public ReadyToBuild ssecDisabled() {
+ this.ssecEnabled = false;
+ return this;
+ }
+
public S3BlobStoreConfiguration build() {
return new S3BlobStoreConfiguration(bucketPrefix,
defaultBucketName, region,
specificAuthConfiguration,
httpConcurrency.orElse(DEFAULT_HTTP_CONCURRENCY),
- inMemoryReadLimit, readTimeout, writeTimeout,
connectionTimeout, uploadRetrySpec.orElse(DEFAULT_UPLOAD_RETRY_SPEC));
+ inMemoryReadLimit, readTimeout, writeTimeout,
connectionTimeout, uploadRetrySpec.orElse(DEFAULT_UPLOAD_RETRY_SPEC),
+ ssecEnabled, ssecConfiguration);
}
}
@@ -148,10 +177,12 @@ public class S3BlobStoreConfiguration {
private final int httpConcurrency;
private final Optional<Long> inMemoryReadLimit;
private final Retry uploadRetrySpec;
+ private final boolean ssecEnabled;
+ private final Optional<S3SSECConfiguration> ssecConfiguration;
- private Optional<Duration> readTimeout;
- private Optional<Duration> writeTimeout;
- private Optional<Duration> connectionTimeout;
+ private final Optional<Duration> readTimeout;
+ private final Optional<Duration> writeTimeout;
+ private final Optional<Duration> connectionTimeout;
@VisibleForTesting
S3BlobStoreConfiguration(Optional<String> bucketPrefix,
@@ -163,7 +194,9 @@ public class S3BlobStoreConfiguration {
Optional<Duration> readTimeout,
Optional<Duration> writeTimeout,
Optional<Duration> connectionTimeout,
- Retry uploadRetrySpec) {
+ Retry uploadRetrySpec,
+ boolean ssecEnabled,
+ Optional<S3SSECConfiguration> ssecConfiguration) {
this.bucketPrefix = bucketPrefix;
this.namespace = namespace;
this.region = region;
@@ -174,6 +207,8 @@ public class S3BlobStoreConfiguration {
this.writeTimeout = writeTimeout;
this.connectionTimeout = connectionTimeout;
this.uploadRetrySpec = uploadRetrySpec;
+ this.ssecEnabled = ssecEnabled;
+ this.ssecConfiguration = ssecConfiguration;
}
public Optional<Long> getInMemoryReadLimit() {
@@ -216,11 +251,17 @@ public class S3BlobStoreConfiguration {
return uploadRetrySpec;
}
+ public boolean ssecEnabled() {
+ return ssecEnabled;
+ }
+
+ public Optional<S3SSECConfiguration> getSSECConfiguration() {
+ return ssecConfiguration;
+ }
+
@Override
public final boolean equals(Object o) {
- if (o instanceof S3BlobStoreConfiguration) {
- S3BlobStoreConfiguration that = (S3BlobStoreConfiguration) o;
-
+ if (o instanceof S3BlobStoreConfiguration that) {
return Objects.equals(this.namespace, that.namespace)
&& Objects.equals(this.bucketPrefix, that.bucketPrefix)
&& Objects.equals(this.region, that.region)
@@ -230,7 +271,9 @@ public class S3BlobStoreConfiguration {
&& Objects.equals(this.writeTimeout, that.writeTimeout)
&& Objects.equals(this.connectionTimeout,
that.connectionTimeout)
&& Objects.equals(this.uploadRetrySpec, that.uploadRetrySpec)
- && Objects.equals(this.specificAuthConfiguration,
that.specificAuthConfiguration);
+ && Objects.equals(this.specificAuthConfiguration,
that.specificAuthConfiguration)
+ && Objects.equals(this.ssecEnabled, that.ssecEnabled)
+ && Objects.equals(this.ssecConfiguration,
that.ssecConfiguration);
}
return false;
}
@@ -238,7 +281,8 @@ public class S3BlobStoreConfiguration {
@Override
public final int hashCode() {
return Objects.hash(namespace, bucketPrefix, httpConcurrency,
specificAuthConfiguration,
- readTimeout, writeTimeout, connectionTimeout, uploadRetrySpec);
+ readTimeout, writeTimeout, connectionTimeout, uploadRetrySpec,
ssecConfiguration, region,
+ inMemoryReadLimit, ssecEnabled);
}
@Override
@@ -254,6 +298,8 @@ public class S3BlobStoreConfiguration {
.add("writeTimeout", writeTimeout)
.add("connectionTimeout", connectionTimeout)
.add("uploadRetrySpec", uploadRetrySpec)
+ .add("ssecEnabled", ssecEnabled)
+ .add("ssecConfiguration", ssecConfiguration)
.toString();
}
}
diff --git
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
index 8c34c3a6b6..b11ef881d5 100644
---
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
+++
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAO.java
@@ -117,12 +117,6 @@ public class S3BlobStoreDAO implements BlobStoreDAO {
private final S3RequestOption s3RequestOption;
@Inject
- public S3BlobStoreDAO(S3ClientFactory s3ClientFactory,
- S3BlobStoreConfiguration configuration,
- BlobId.Factory blobIdFactory) {
- this(s3ClientFactory, configuration, blobIdFactory,
S3RequestOption.DEFAULT);
- }
-
public S3BlobStoreDAO(S3ClientFactory s3ClientFactory,
S3BlobStoreConfiguration configuration,
BlobId.Factory blobIdFactory,
diff --git
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
index 5de59834d2..a3f466090f 100644
---
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
+++
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/S3RequestOption.java
@@ -26,7 +26,7 @@ import
org.apache.james.blob.objectstorage.aws.sse.S3SSECustomerKeyFactory;
import com.google.common.base.Preconditions;
public record S3RequestOption(SSEC ssec) {
- static S3RequestOption DEFAULT = new
S3RequestOption(S3RequestOption.SSEC.DISABLED);
+ public static S3RequestOption DEFAULT = new
S3RequestOption(S3RequestOption.SSEC.DISABLED);
public record SSEC(boolean enable,
java.util.Optional<S3SSECustomerKeyFactory> sseCustomerKeyFactory) {
static S3RequestOption.SSEC DISABLED = new S3RequestOption.SSEC(false,
Optional.empty());
diff --git
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
index 9160fa8f70..d3ef8150fb 100644
---
a/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
+++
b/server/blob/blob-s3/src/main/java/org/apache/james/blob/objectstorage/aws/sse/S3SSECConfiguration.java
@@ -22,13 +22,25 @@ package org.apache.james.blob.objectstorage.aws.sse;
import java.util.List;
import java.util.Optional;
+import org.apache.commons.configuration2.Configuration;
+
import com.google.common.base.Preconditions;
public interface S3SSECConfiguration {
- String SSEC_ALGORITHM_DEFAULT = "AES256";
+ String ENCRYPTION_S3_SSEC_ALGORITHM_DEFAULT = "AES256";
String CUSTOMER_KEY_FACTORY_ALGORITHM_DEFAULT = "PBKDF2WithHmacSHA256";
- List<String> SUPPORTED_ALGORITHMS = List.of(SSEC_ALGORITHM_DEFAULT);
+ List<String> SUPPORTED_ALGORITHMS =
List.of(ENCRYPTION_S3_SSEC_ALGORITHM_DEFAULT);
+ String ENCRYPTION_S3_SSEC_MASTER_KEY_ALGORITHM_PROPERTY =
"encryption.s3.sse.c.master.key.algorithm";
+ String ENCRYPTION_S3_SSEC_MASTER_KEY_SALT_PROPERTY =
"encryption.s3.sse.c.master.key.salt";
+ String ENCRYPTION_S3_SSEC_MASTER_KEY_PASSWORD_PROPERTY =
"encryption.s3.sse.c.master.key.password";
+
+ static S3SSECConfiguration from(Configuration configuration) {
+ String algorithm =
configuration.getString(ENCRYPTION_S3_SSEC_MASTER_KEY_ALGORITHM_PROPERTY,
ENCRYPTION_S3_SSEC_ALGORITHM_DEFAULT);
+ return new Basic(algorithm,
+
configuration.getString(ENCRYPTION_S3_SSEC_MASTER_KEY_PASSWORD_PROPERTY, null),
+
configuration.getString(ENCRYPTION_S3_SSEC_MASTER_KEY_SALT_PROPERTY, null));
+ }
String ssecAlgorithm();
@@ -41,6 +53,8 @@ public interface S3SSECConfiguration {
String salt) implements S3SSECConfiguration {
public Basic {
Preconditions.checkArgument(SUPPORTED_ALGORITHMS.contains(ssecAlgorithm),
"Unsupported algorithm: " + ssecAlgorithm + ". The supported algorithms are: "
+ SUPPORTED_ALGORITHMS);
+ Preconditions.checkNotNull(masterPassword,
ENCRYPTION_S3_SSEC_MASTER_KEY_PASSWORD_PROPERTY + " cannot be null");
+ Preconditions.checkNotNull(salt,
ENCRYPTION_S3_SSEC_MASTER_KEY_SALT_PROPERTY + " cannot be null");
}
}
}
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfigurationTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfigurationTest.java
new file mode 100644
index 0000000000..b9b771a029
--- /dev/null
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreConfigurationTest.java
@@ -0,0 +1,63 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.blob.objectstorage.aws;
+
+import static
org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration.UPLOAD_RETRY_EXCEPTION_PREDICATE;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.net.URI;
+import java.util.Optional;
+
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECConfiguration;
+import org.junit.jupiter.api.Test;
+
+import com.github.fge.lambdas.Throwing;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import reactor.util.retry.Retry;
+
+public class S3BlobStoreConfigurationTest {
+
+ @Test
+ void configurationShouldRespectBeanContract() {
+ EqualsVerifier.forClass(S3BlobStoreConfiguration.class)
+ .verify();
+ }
+
+ @Test
+ void shouldThrowWhenSSECEnableAndNoSSECConfiguration() {
+ S3SSECConfiguration ssecConfiguration = null;
+
+ assertThatThrownBy(() -> S3BlobStoreConfiguration.builder()
+ .authConfiguration(AwsS3AuthConfiguration.builder()
+ .endpoint(Throwing.supplier(() -> new
URI("http://localhost:1234")).get())
+ .accessKeyId("accessKeyId")
+ .secretKey("secretKey1")
+ .build())
+ .region(Region.of("af-south-1"))
+ .uploadRetrySpec(Optional.of(Retry.backoff(3,
java.time.Duration.ofSeconds(1))
+ .filter(UPLOAD_RETRY_EXCEPTION_PREDICATE)))
+ .ssecEnabled()
+ .ssecConfiguration(ssecConfiguration)
+ .build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("SSEC configuration is mandatory when SSEC is
enabled");
+ }
+}
\ No newline at end of file
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java
index bca00dd75e..ad84633426 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOTest.java
@@ -70,7 +70,7 @@ public class S3BlobStoreDAOTest implements
BlobStoreDAOContract {
s3ClientFactory = new S3ClientFactory(s3Configuration, () -> new
JamesS3MetricPublisher(new RecordingMetricFactory(), new NoopGaugeRegistry(),
DEFAULT_S3_METRICS_PREFIX));
- testee = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, new
TestBlobId.Factory());
+ testee = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, new
TestBlobId.Factory(), S3RequestOption.DEFAULT);
}
@AfterEach
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
index ad10af232c..24beccdcba 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3DeDuplicationBlobStoreTest.java
@@ -68,7 +68,7 @@ class S3DeDuplicationBlobStoreTest implements
BlobStoreContract, DeduplicationBl
PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
s3ClientFactory = new S3ClientFactory(s3Configuration, new
RecordingMetricFactory(), new NoopGaugeRegistry());
- s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory);
+ s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory, S3RequestOption.DEFAULT);
return BlobStoreFactory.builder()
.blobStoreDAO(s3BlobStoreDAO)
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3HealthCheckTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3HealthCheckTest.java
index b660948195..11570eba3a 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3HealthCheckTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3HealthCheckTest.java
@@ -51,7 +51,7 @@ public class S3HealthCheckTest {
.build();
S3ClientFactory s3ClientFactory = new S3ClientFactory(s3Configuration,
new RecordingMetricFactory(), new NoopGaugeRegistry());
- BlobStoreDAO s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory,
s3Configuration, new TestBlobId.Factory());
+ BlobStoreDAO s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory,
s3Configuration, new TestBlobId.Factory(), S3RequestOption.DEFAULT);
s3HealthCheck = new ObjectStorageHealthCheck(s3BlobStoreDAO);
}
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
index ad506127d2..dc4a4e3ba3 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3MinioTest.java
@@ -67,7 +67,7 @@ public class S3MinioTest implements BlobStoreDAOContract {
.build();
s3ClientFactory = new S3ClientFactory(s3Configuration, new
RecordingMetricFactory(), new NoopGaugeRegistry());
- testee = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, new
TestBlobId.Factory());
+ testee = new S3BlobStoreDAO(s3ClientFactory, s3Configuration, new
TestBlobId.Factory(), S3RequestOption.DEFAULT);
}
@AfterAll
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3NamespaceTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3NamespaceTest.java
index ae73ae445a..72e51771ff 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3NamespaceTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3NamespaceTest.java
@@ -54,7 +54,7 @@ class S3NamespaceTest implements BlobStoreContract {
PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
s3ClientFactory = new S3ClientFactory(s3Configuration, new
RecordingMetricFactory(), new NoopGaugeRegistry());
- s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory);
+ s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory, S3RequestOption.DEFAULT);
testee = BlobStoreFactory.builder()
.blobStoreDAO(s3BlobStoreDAO)
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PassThroughBlobStoreTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PassThroughBlobStoreTest.java
index 45ba236d56..c99777782a 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PassThroughBlobStoreTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PassThroughBlobStoreTest.java
@@ -54,7 +54,7 @@ class S3PassThroughBlobStoreTest implements BlobStoreContract
{
PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
s3ClientFactory = new S3ClientFactory(s3Configuration, new
RecordingMetricFactory(), new NoopGaugeRegistry());
- s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory);
+ s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory, S3RequestOption.DEFAULT);
testee = BlobStoreFactory.builder()
.blobStoreDAO(s3BlobStoreDAO)
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
index 893d29b209..99183da991 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixAndNamespaceTest.java
@@ -63,7 +63,7 @@ class S3PrefixAndNamespaceTest implements BlobStoreContract,
DeduplicationBlobSt
PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
s3ClientFactory = new S3ClientFactory(s3Configuration, new
RecordingMetricFactory(), new NoopGaugeRegistry());
- s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory);
+ s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory, S3RequestOption.DEFAULT);
return BlobStoreFactory.builder()
.blobStoreDAO(s3BlobStoreDAO)
diff --git
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixTest.java
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixTest.java
index bef47890e4..01759adf27 100644
---
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixTest.java
+++
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3PrefixTest.java
@@ -53,7 +53,7 @@ class S3PrefixTest implements BlobStoreContract {
PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
s3ClientFactory = new S3ClientFactory(s3Configuration, new
RecordingMetricFactory(), new NoopGaugeRegistry());
- s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory);
+ s3BlobStoreDAO = new S3BlobStoreDAO(s3ClientFactory, s3Configuration,
blobIdFactory, S3RequestOption.DEFAULT);
testee = BlobStoreFactory.builder()
.blobStoreDAO(s3BlobStoreDAO)
diff --git
a/server/container/guice/blob/s3/src/main/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReader.java
b/server/container/guice/blob/s3/src/main/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReader.java
index c3c22b9161..a760ad0d28 100644
---
a/server/container/guice/blob/s3/src/main/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReader.java
+++
b/server/container/guice/blob/s3/src/main/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReader.java
@@ -50,6 +50,7 @@ public class S3BlobStoreConfigurationReader {
private static final String OBJECTSTORAGE_S3_IN_MEMORY_READ_LIMIT =
"objectstorage.s3.in.read.limit";
private static final String OBJECTSTORAGE_S3_UPLOAD_RETRY_MAX_ATTEMPTS =
"objectstorage.s3.upload.retry.maxAttempts";
private static final String
OBJECTSTORAGE_S3_UPLOAD_RETRY_BACKOFF_DURATION_MILLIS =
"objectstorage.s3.upload.retry.backoffDurationMillis";
+ private static final String
OBJECTSTORAGE_S3_ENCRYPTION_SSEC_ENABLE_PROPERTY = "encryption.s3.sse.c.enable";
public static S3BlobStoreConfiguration from(Configuration configuration)
throws ConfigurationException {
Optional<Integer> httpConcurrency =
Optional.ofNullable(configuration.getInteger(OBJECTSTORAGE_S3_HTTP_CONCURRENCY,
null));
@@ -75,7 +76,9 @@ public class S3BlobStoreConfigurationReader {
.jitter(UPLOAD_RETRY_BACKOFF_JETTY_DEFAULT)
.filter(SdkException.class::isInstance));
- return S3BlobStoreConfiguration.builder()
+ boolean ssecEnabled =
configuration.getBoolean(OBJECTSTORAGE_S3_ENCRYPTION_SSEC_ENABLE_PROPERTY,
false);
+
+ S3BlobStoreConfiguration.Builder.ReadyToBuild configBuilder =
S3BlobStoreConfiguration.builder()
.authConfiguration(AwsS3ConfigurationReader.from(configuration))
.region(region)
.defaultBucketName(namespace.map(BucketName::of))
@@ -85,8 +88,12 @@ public class S3BlobStoreConfigurationReader {
.readTimeout(readTimeout)
.writeTimeout(writeTimeout)
.connectionTimeout(connectionTimeout)
- .uploadRetrySpec(uploadRetrySpec)
- .build();
+ .uploadRetrySpec(uploadRetrySpec);
+
+ if (ssecEnabled) {
+ configBuilder.ssecEnabled().ssecConfiguration(configuration);
+ }
+ return configBuilder.build();
}
}
diff --git
a/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
b/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
index f001461452..0652b61394 100644
---
a/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
+++
b/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/aws/s3/DockerAwsS3TestRule.java
@@ -28,6 +28,7 @@ import
org.apache.james.blob.objectstorage.aws.DockerAwsS3Container;
import org.apache.james.blob.objectstorage.aws.DockerAwsS3Singleton;
import org.apache.james.blob.objectstorage.aws.Region;
import org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration;
+import org.apache.james.blob.objectstorage.aws.S3RequestOption;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
@@ -79,6 +80,7 @@ public class DockerAwsS3TestRule implements
GuiceModuleTestRule {
binder.bind(Region.class).toInstance(region);
binder.bind(AwsS3AuthConfiguration.class).toInstance(authConfiguration);
binder.bind(S3BlobStoreConfiguration.class).toInstance(configuration);
+
binder.bind(S3RequestOption.class).toInstance(S3RequestOption.DEFAULT);
};
}
diff --git
a/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
b/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
index 708187101d..7b5c461be0 100644
---
a/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
+++
b/server/container/guice/distributed/src/main/java/org/apache/james/modules/blobstore/BlobStoreModulesChooser.java
@@ -19,6 +19,8 @@
package org.apache.james.modules.blobstore;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
import java.util.List;
import java.util.Optional;
@@ -30,7 +32,12 @@ import org.apache.james.blob.api.ObjectStorageHealthCheck;
import org.apache.james.blob.cassandra.CassandraBlobStoreDAO;
import org.apache.james.blob.cassandra.cache.CachedBlobStore;
import org.apache.james.blob.file.FileBlobStoreDAO;
+import org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration;
import org.apache.james.blob.objectstorage.aws.S3BlobStoreDAO;
+import org.apache.james.blob.objectstorage.aws.S3RequestOption;
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECConfiguration;
+import org.apache.james.blob.objectstorage.aws.sse.S3SSECustomerKeyFactory;
+import
org.apache.james.blob.objectstorage.aws.sse.S3SSECustomerKeyFactory.SingleCustomerKeyFactory;
import org.apache.james.core.healthcheck.HealthCheck;
import
org.apache.james.modules.blobstore.validation.BlobStoreConfigurationValidationStartUpCheck.StorageStrategySupplier;
import
org.apache.james.modules.blobstore.validation.StoragePolicyConfigurationSanityEnforcementModule;
@@ -42,6 +49,7 @@ import org.apache.james.modules.objectstorage.S3BucketModule;
import org.apache.james.server.blob.deduplication.DeDuplicationBlobStore;
import org.apache.james.server.blob.deduplication.PassThroughBlobStore;
import org.apache.james.server.blob.deduplication.StorageStrategy;
+import org.apache.james.server.core.MissingArgumentException;
import com.google.common.collect.ImmutableList;
import com.google.inject.AbstractModule;
@@ -76,6 +84,19 @@ public class BlobStoreModulesChooser {
.in(Scopes.SINGLETON);
Multibinder.newSetBinder(binder(),
HealthCheck.class).addBinding().to(ObjectStorageHealthCheck.class);
}
+
+ @Provides
+ @Singleton
+ S3RequestOption provideS3RequestOption(S3BlobStoreConfiguration
configuration) throws InvalidKeySpecException, NoSuchAlgorithmException {
+ if (!configuration.ssecEnabled()) {
+ return S3RequestOption.DEFAULT;
+ }
+ S3SSECConfiguration ssecConfiguration =
configuration.getSSECConfiguration()
+ .orElseThrow(() -> new MissingArgumentException("SSEC is
enabled but no configuration is provided"));
+
+ S3SSECustomerKeyFactory sseCustomerKeyFactory = new
SingleCustomerKeyFactory((S3SSECConfiguration.Basic) ssecConfiguration);
+ return new S3RequestOption(new S3RequestOption.SSEC(true,
Optional.of(sseCustomerKeyFactory)));
+ }
}
static class FileBlobStoreDAODeclarationModule extends AbstractModule {
diff --git a/src/site/xdoc/server/config-blobstore.xml
b/src/site/xdoc/server/config-blobstore.xml
index 52d6a831c0..8427b45a26 100644
--- a/src/site/xdoc/server/config-blobstore.xml
+++ b/src/site/xdoc/server/config-blobstore.xml
@@ -214,6 +214,34 @@ generate salt with : openssl rand -hex 16
</dl>
</subsection>
</subsection>
+ <subsection name="SSE-C Configuration">
+ <dl>
+ <dt><strong>encryption.s3.sse.c.enable</strong></dt>
+ <dd>
+ optional: Boolean. Default is false.
+ Controls whether to use Server-Side Encryption with
Customer-Provided Keys (SSE-C) for S3 blobs
+ </dd>
+
+
<dt><strong>encryption.s3.sse.c.master.key.algorithm</strong></dt>
+ <dd>
+ String. Required if `encryption.s3.sse.c.enable` is
true.
+ The algorithm used to derive the master key from the
provided password. Eg: AES256
+ </dd>
+
+
<dt><strong>encryption.s3.sse.c.master.key.password</strong></dt>
+ <dd>
+ String. Required if `encryption.s3.sse.c.enable` is
true.
+ The password used to generate the customer key.
+ </dd>
+
+
<dt><strong>encryption.s3.sse.c.master.key.salt</strong></dt>
+ <dd>
+ String. Required if `encryption.s3.sse.c.enable` is
true.
+ The salt used to generate the customer key.
+ </dd>
+
+ </dl>
+ </subsection>
</section>
</body>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]