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]


Reply via email to