You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/08/24 07:59:48 UTC

(opendal-reqsign) 01/01: fix(aws): configure S3 Express session provider grants

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

Xuanwo pushed a commit to branch xuanwo/s3-express-session-provider-mode
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git

commit daf120784eded0a550fe119d5f5295772b87c1c3
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Mon Aug 24 15:59:36 2026 +0800

    fix(aws): configure S3 Express session provider grants
---
 .../src/provide_credential/s3_express_session.rs   | 96 +++++++++++++++++++---
 1 file changed, 86 insertions(+), 10 deletions(-)

diff --git a/services/aws-v4/src/provide_credential/s3_express_session.rs b/services/aws-v4/src/provide_credential/s3_express_session.rs
index 06d8a8a..96d7ad0 100644
--- a/services/aws-v4/src/provide_credential/s3_express_session.rs
+++ b/services/aws-v4/src/provide_credential/s3_express_session.rs
@@ -694,15 +694,15 @@ impl GrantCredential for S3ExpressSessionGranter {
     }
 }
 
-/// S3 Express fixed-flow credential provider.
+/// S3 Express session credential provider.
 ///
-/// This compatibility API loads its source through [`ProvideCredential`] and
-/// creates a `ReadWrite` session on every call. New explicit credential-vending
-/// flows should use [`S3ExpressSessionGranter`] through
-/// [`reqsign_core::Granter`].
+/// This API loads its source through [`ProvideCredential`] and creates a session
+/// on every call. It defaults to an explicit `ReadWrite` session for compatibility;
+/// use [`Self::with_grant`] to select another mode.
 pub struct S3ExpressSessionProvider {
     bucket: String,
     region: Option<String>,
+    grant: S3ExpressSessionGrantSelection,
     base_provider: Box<dyn ProvideCredentialDyn<Credential = Credential>>,
 }
 
@@ -714,7 +714,7 @@ impl Debug for S3ExpressSessionProvider {
 }
 
 impl S3ExpressSessionProvider {
-    /// Create a fixed `ReadWrite` session provider for a directory bucket.
+    /// Create a session provider that defaults to `ReadWrite` for a directory bucket.
     ///
     /// The legacy constructor infers the Region from well-known AWS Zone ID
     /// prefixes. Use [`S3ExpressSessionProvider::with_region`] when the Zone is
@@ -727,6 +727,7 @@ impl S3ExpressSessionProvider {
         Self {
             bucket: bucket.into(),
             region: None,
+            grant: S3ExpressSessionGrant::new(S3ExpressSessionMode::ReadWrite).into(),
             base_provider: Box::new(provider),
         }
     }
@@ -737,6 +738,12 @@ impl S3ExpressSessionProvider {
         self
     }
 
+    /// Select how S3 Express determines the `CreateSession` permission mode.
+    pub fn with_grant(mut self, grant: impl Into<S3ExpressSessionGrantSelection>) -> Self {
+        self.grant = grant.into();
+        self
+    }
+
     fn operation(&self) -> Result<S3ExpressSessionGranter> {
         let zone_id = zone_id_from_bucket(&self.bucket)?;
         let region = match &self.region {
@@ -748,10 +755,7 @@ impl S3ExpressSessionProvider {
             })?,
         };
         let config = S3ExpressSessionConfig::from_bucket(&self.bucket, region)?;
-        Ok(S3ExpressSessionGranter::new(
-            config,
-            S3ExpressSessionGrant::new(S3ExpressSessionMode::ReadWrite),
-        ))
+        Ok(S3ExpressSessionGranter::new(config, self.grant.clone()))
     }
 }
 
@@ -2298,6 +2302,78 @@ mod tests {
         );
     }
 
+    #[tokio::test]
+    async fn configurable_provider_refreshes_maximum_allowed_sessions_for_signer() {
+        let first_expiration = (Timestamp::now() + Duration::from_secs(110)).format_rfc3339_zulu();
+        let second_expiration = (Timestamp::now() + Duration::from_secs(300)).format_rfc3339_zulu();
+        let http = MockHttpSend::new([
+            success_response(
+                "first-session-access-key",
+                "first-session-secret-key",
+                "first-session-token",
+                &first_expiration,
+            ),
+            success_response(
+                "second-session-access-key",
+                "second-session-secret-key",
+                "second-session-token",
+                &second_expiration,
+            ),
+        ]);
+        let ctx = Context::new().with_http_send(http.clone());
+        let (source_provider, source_calls) = FixedCredentialProvider::new(source_credential());
+        let provider = S3ExpressSessionProvider::new("example--usw2-az1--x-s3", source_provider)
+            .with_grant(S3ExpressSessionGrant::maximum_allowed());
+        let signer = Signer::new(
+            ctx,
+            provider,
+            crate::RequestSigner::new("s3express", "us-west-2"),
+        );
+
+        let mut first = Request::get(
+            "https://example--usw2-az1--x-s3.s3express-usw2-az1.us-west-2.amazonaws.com/first",
+        )
+        .body(())
+        .expect("first request must build")
+        .into_parts()
+        .0;
+        signer
+            .sign(&mut first, None)
+            .await
+            .expect("first request must use a maximum-allowed session");
+        assert_eq!(
+            first.headers["x-amz-s3session-token"],
+            "first-session-token"
+        );
+
+        let mut second = Request::get(
+            "https://example--usw2-az1--x-s3.s3express-usw2-az1.us-west-2.amazonaws.com/second",
+        )
+        .body(())
+        .expect("second request must build")
+        .into_parts()
+        .0;
+        signer
+            .sign(&mut second, None)
+            .await
+            .expect("second request must refresh the maximum-allowed session");
+        assert_eq!(
+            second.headers["x-amz-s3session-token"],
+            "second-session-token"
+        );
+
+        assert_eq!(source_calls.load(Ordering::SeqCst), 2);
+        assert_eq!(http.calls.load(Ordering::SeqCst), 2);
+        for index in 0..2 {
+            assert!(
+                !http
+                    .request(index)
+                    .headers
+                    .contains_key("x-amz-create-session-mode")
+            );
+        }
+    }
+
     #[tokio::test]
     async fn compatibility_provider_uses_explicit_region_for_new_zone_prefix() {
         let response = success_response(