This is an automated email from the ASF dual-hosted git repository.

chibenwa 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 98222025ba JAMES-4223 Default S3 If-None-Match to true to prevent 
silent blob overwrite
98222025ba is described below

commit 98222025ba6b94e79e50b46a5308d856506c1755
Author: ilya terskov <[email protected]>
AuthorDate: Mon Sep 28 13:38:19 2026 +0700

    JAMES-4223 Default S3 If-None-Match to true to prevent silent blob overwrite
---
 .../servers/partials/configure/blobstore.adoc      |  2 +-
 .../sample-configuration/blob.properties           |  4 +-
 .../sample-configuration/blob.properties           |  4 +-
 .../blob.properties                                |  4 +-
 .../sample-configuration/blob.properties           |  4 +-
 .../aws/S3BlobStoreConfiguration.java              |  2 +-
 .../aws/S3BlobStoreConfigurationTest.java          | 15 ++++++
 .../aws/S3BlobStoreDAOIfNoneMatchTest.java         | 12 +++++
 .../S3BlobStoreConfigurationReader.java            |  2 +-
 .../S3BlobStoreConfigurationReaderTest.java        | 59 ++++++++++++++++++++++
 10 files changed, 97 insertions(+), 11 deletions(-)

diff --git a/docs/modules/servers/partials/configure/blobstore.adoc 
b/docs/modules/servers/partials/configure/blobstore.adoc
index c68aad5979..1ef24ae231 100644
--- a/docs/modules/servers/partials/configure/blobstore.adoc
+++ b/docs/modules/servers/partials/configure/blobstore.adoc
@@ -148,7 +148,7 @@ This property determines the duration (in milliseconds) to 
wait between retry at
 This delay is known as backoff. The jitter factor is 0.5
 
 | objectstorage.s3.ifNoneMatch.enable
-| optional: Boolean. Defaults to false. When enabled, James adds the 
`If-None-Match: *` header to every object upload,
+| optional: Boolean. Defaults to true. When enabled, James adds the 
`If-None-Match: *` header to every object upload,
 turning writes into conditional writes: the object is only written if it does 
not exist yet. A `412 Precondition failed`
 answer is then treated as a success as the object is already stored.
 
diff --git a/server/apps/distributed-app/sample-configuration/blob.properties 
b/server/apps/distributed-app/sample-configuration/blob.properties
index 6bb307bbf2..01cdb72352 100644
--- a/server/apps/distributed-app/sample-configuration/blob.properties
+++ b/server/apps/distributed-app/sample-configuration/blob.properties
@@ -120,13 +120,13 @@ objectstorage.s3.secretKey=secretKey1
 # from being loaded in memory. This settings complements protocol limits.
 # objectstorage.s3.in.read.limit=50M
 
-# Optional, defaults to false. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
+# Optional, defaults to true. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
 # an object that is already stored is not written again, a `412 Precondition 
failed` answer being treated as a success.
 # This spares the bandwidth and the storage cost of re-uploading known blobs 
and, combined with a bucket policy denying
 # `s3:DeleteObject` and requiring `s3:if-none-match` upon PUT, prevents stored 
objects from ever being rewritten.
 # Requires an object storage supporting conditional writes (AWS S3, MinIO). 
Only brings benefits when
 # `deduplication.enable=true`: without deduplication each write uses a 
distinct blob id, thus none is ever skipped.
-# objectstorage.s3.ifNoneMatch.enable=false
+# objectstorage.s3.ifNoneMatch.enable=true
 
 # ============================================ Blobs Exporting 
==============================================
 # Read https://james.apache.org/server/config-blob-export.html for further 
details
diff --git 
a/server/apps/distributed-pop3-app/sample-configuration/blob.properties 
b/server/apps/distributed-pop3-app/sample-configuration/blob.properties
index d9587c5011..0ea36406fb 100644
--- a/server/apps/distributed-pop3-app/sample-configuration/blob.properties
+++ b/server/apps/distributed-pop3-app/sample-configuration/blob.properties
@@ -89,13 +89,13 @@ objectstorage.s3.secretKey=secretKey1
 # from being loaded in memory. This settings complements protocol limits.
 # objectstorage.s3.in.read.limit=50M
 
-# Optional, defaults to false. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
+# Optional, defaults to true. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
 # an object that is already stored is not written again, a `412 Precondition 
failed` answer being treated as a success.
 # This spares the bandwidth and the storage cost of re-uploading known blobs 
and, combined with a bucket policy denying
 # `s3:DeleteObject` and requiring `s3:if-none-match` upon PUT, prevents stored 
objects from ever being rewritten.
 # Requires an object storage supporting conditional writes (AWS S3, MinIO). 
Only brings benefits when
 # `deduplication.enable=true`: without deduplication each write uses a 
distinct blob id, thus none is ever skipped.
-# objectstorage.s3.ifNoneMatch.enable=false
+# objectstorage.s3.ifNoneMatch.enable=true
 
 # ============================================ Blobs Exporting 
==============================================
 # Read https://james.apache.org/server/config-blob-export.html for further 
details
diff --git 
a/server/apps/postgres-app/sample-configuration-distributed/blob.properties 
b/server/apps/postgres-app/sample-configuration-distributed/blob.properties
index 795b0f8bd2..43b4d7859a 100644
--- a/server/apps/postgres-app/sample-configuration-distributed/blob.properties
+++ b/server/apps/postgres-app/sample-configuration-distributed/blob.properties
@@ -82,13 +82,13 @@ objectstorage.s3.secretKey=secretKey1
 # from being loaded in memory. This settings complements protocol limits.
 # objectstorage.s3.in.read.limit=50M
 
-# Optional, defaults to false. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
+# Optional, defaults to true. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
 # an object that is already stored is not written again, a `412 Precondition 
failed` answer being treated as a success.
 # This spares the bandwidth and the storage cost of re-uploading known blobs 
and, combined with a bucket policy denying
 # `s3:DeleteObject` and requiring `s3:if-none-match` upon PUT, prevents stored 
objects from ever being rewritten.
 # Requires an object storage supporting conditional writes (AWS S3, MinIO). 
Only brings benefits when
 # `deduplication.enable=true`: without deduplication each write uses a 
distinct blob id, thus none is ever skipped.
-# objectstorage.s3.ifNoneMatch.enable=false
+# objectstorage.s3.ifNoneMatch.enable=true
 
 # ============================================ Blobs Exporting 
==============================================
 # Read https://james.apache.org/server/config-blob-export.html for further 
details
diff --git 
a/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties 
b/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties
index 3e6e92c079..41e234b158 100644
--- a/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties
+++ b/server/apps/scaling-pulsar-smtp/sample-configuration/blob.properties
@@ -101,13 +101,13 @@ 
objectstorage.s3.secretKey=${env:OBJECTSTORAGE_S3_SECRETKEY}
 # from being loaded in memory. This settings complements protocol limits.
 # objectstorage.s3.in.read.limit=50M
 
-# Optional, defaults to false. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
+# Optional, defaults to true. Turn uploads into conditional writes by adding 
the `If-None-Match: *` header:
 # an object that is already stored is not written again, a `412 Precondition 
failed` answer being treated as a success.
 # This spares the bandwidth and the storage cost of re-uploading known blobs 
and, combined with a bucket policy denying
 # `s3:DeleteObject` and requiring `s3:if-none-match` upon PUT, prevents stored 
objects from ever being rewritten.
 # Requires an object storage supporting conditional writes (AWS S3, MinIO). 
Only brings benefits when
 # `deduplication.enable=true`: without deduplication each write uses a 
distinct blob id, thus none is ever skipped.
-# objectstorage.s3.ifNoneMatch.enable=false
+# objectstorage.s3.ifNoneMatch.enable=true
 
 # ============================================ Blobs Exporting 
==============================================
 # Read https://james.apache.org/server/config-blob-export.html for further 
details
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 0ed068e79a..9d0c74b354 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
@@ -92,7 +92,7 @@ public class S3BlobStoreConfiguration {
                 this.inMemoryReadLimit = Optional.empty();
                 this.uploadRetrySpec = Optional.empty();
                 this.fallbackBucketName = Optional.empty();
-                this.ifNoneMatchEnabled = false;
+                this.ifNoneMatchEnabled = true;
             }
 
             public ReadyToBuild defaultBucketName(Optional<BucketName> 
defaultBucketName) {
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
index b9b771a029..c8edb80cad 100644
--- 
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
@@ -20,6 +20,7 @@
 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.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.net.URI;
@@ -60,4 +61,18 @@ public class S3BlobStoreConfigurationTest {
             .isInstanceOf(IllegalArgumentException.class)
             .hasMessage("SSEC configuration is mandatory when SSEC is 
enabled");
     }
+
+    @Test
+    void shouldDefaultIfNoneMatchToTrue() {
+        S3BlobStoreConfiguration configuration = 
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"))
+            .build();
+
+        assertThat(configuration.ifNoneMatchEnabled()).isTrue();
+    }
 }
\ No newline at end of file
diff --git 
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOIfNoneMatchTest.java
 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOIfNoneMatchTest.java
index de791ea0eb..f0aedcf9d0 100644
--- 
a/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOIfNoneMatchTest.java
+++ 
b/server/blob/blob-s3/src/test/java/org/apache/james/blob/objectstorage/aws/S3BlobStoreDAOIfNoneMatchTest.java
@@ -106,4 +106,16 @@ class S3BlobStoreDAOIfNoneMatchTest {
 
         verify(client, times(2)).putObject(any(PutObjectRequest.class), 
any(AsyncRequestBody.class));
     }
+
+    @Test
+    void saveByteSourceShouldRetryConditionalRequestConflict() {
+        when(client.putObject(any(PutObjectRequest.class), 
any(AsyncRequestBody.class)))
+            
.thenReturn(failedFuture(s3Exception(CONDITIONAL_REQUEST_CONFLICT_STATUS_CODE, 
CONDITIONAL_REQUEST_CONFLICT_ERROR_CODE)))
+            
.thenReturn(CompletableFuture.completedFuture(PutObjectResponse.builder().build()));
+
+        assertThatCode(() -> Mono.from(testee.save(TEST_BUCKET_NAME, 
TEST_BLOB_ID, SHORT_BYTEARRAY.asByteSource())).block())
+            .doesNotThrowAnyException();
+
+        verify(client, times(2)).putObject(any(PutObjectRequest.class), 
any(AsyncRequestBody.class));
+    }
 }
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 1299d62115..3d744256ca 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
@@ -82,7 +82,7 @@ public class S3BlobStoreConfigurationReader {
 
         Optional<String> fallbackNamespace = 
Optional.ofNullable(configuration.getString(OBJECTSTORAGE_NAMESPACE_READ_FALLBACK,
 null));
 
-        boolean ifNoneMatchEnabled = 
configuration.getBoolean(OBJECTSTORAGE_S3_IF_NONE_MATCH_ENABLE, false);
+        boolean ifNoneMatchEnabled = 
configuration.getBoolean(OBJECTSTORAGE_S3_IF_NONE_MATCH_ENABLE, true);
 
         S3BlobStoreConfiguration.Builder.ReadyToBuild configBuilder = 
S3BlobStoreConfiguration.builder()
             .authConfiguration(AwsS3ConfigurationReader.from(configuration))
diff --git 
a/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReaderTest.java
 
b/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReaderTest.java
new file mode 100644
index 0000000000..a851b6511c
--- /dev/null
+++ 
b/server/container/guice/blob/s3/src/test/java/org/apache/james/modules/objectstorage/S3BlobStoreConfigurationReaderTest.java
@@ -0,0 +1,59 @@
+/****************************************************************
+ * 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.modules.objectstorage;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.commons.configuration2.Configuration;
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.james.blob.objectstorage.aws.S3BlobStoreConfiguration;
+import org.junit.jupiter.api.Test;
+
+class S3BlobStoreConfigurationReaderTest {
+
+    private Configuration baseConfiguration() {
+        Configuration configuration = new PropertiesConfiguration();
+        configuration.addProperty("objectstorage.s3.endPoint", 
"http://myEndpoint";);
+        configuration.addProperty("objectstorage.s3.accessKeyId", 
"myAccessKeyId");
+        configuration.addProperty("objectstorage.s3.secretKey", "mySecretKey");
+        configuration.addProperty("objectstorage.namespace", "myNamespace");
+        configuration.addProperty("objectstorage.s3.region", "us-east-1");
+        return configuration;
+    }
+
+    @Test
+    void shouldDefaultIfNoneMatchToTrue() throws Exception {
+        Configuration configuration = baseConfiguration();
+
+        S3BlobStoreConfiguration s3Configuration = 
S3BlobStoreConfigurationReader.from(configuration);
+
+        assertThat(s3Configuration.ifNoneMatchEnabled()).isTrue();
+    }
+
+    @Test
+    void shouldRespectIfNoneMatchExplicitlyDisabled() throws Exception {
+        Configuration configuration = baseConfiguration();
+        configuration.addProperty("objectstorage.s3.ifNoneMatch.enable", 
false);
+
+        S3BlobStoreConfiguration s3Configuration = 
S3BlobStoreConfigurationReader.from(configuration);
+
+        assertThat(s3Configuration.ifNoneMatchEnabled()).isFalse();
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to