Skip to main content

s3_wire/client/
presign.rs

1use std::time::Duration;
2
3use secrecy::ExposeSecret;
4use time::OffsetDateTime;
5
6use super::S3Client;
7use crate::error::{ErrorCategory, RetryClassification, S3Error};
8use crate::operation::{ObjectKey, PresignedUrl};
9use crate::signing::{
10    Header, PresigningRequest, SigningCredentials, SigningPath, SigningScope, sign_presigned,
11};
12
13impl S3Client {
14    /// Generates a presigned GET URL whose standard formatting is redacted.
15    pub async fn presigned_get(
16        &self,
17        key: &ObjectKey,
18        expires: Duration,
19    ) -> Result<PresignedUrl, S3Error> {
20        self.presign("GET", key, expires).await
21    }
22
23    /// Generates a presigned PUT URL whose standard formatting is redacted.
24    pub async fn presigned_put(
25        &self,
26        key: &ObjectKey,
27        expires: Duration,
28    ) -> Result<PresignedUrl, S3Error> {
29        self.presign("PUT", key, expires).await
30    }
31
32    async fn presign(
33        &self,
34        method: &'static str,
35        key: &ObjectKey,
36        expires: Duration,
37    ) -> Result<PresignedUrl, S3Error> {
38        let target = self.inner.config.endpoint().object_url(
39            self.inner.config.bucket(),
40            Some(key.as_str()),
41            self.inner.config.addressing_style(),
42        )?;
43        let credentials = self
44            .inner
45            .config
46            .credentials_provider()
47            .provide_credentials()
48            .await?;
49        if credentials.expires_by(OffsetDateTime::now_utc()) {
50            return Err(S3Error::new(
51                ErrorCategory::Authentication,
52                "credential provider returned expired credentials",
53                RetryClassification::Never,
54            ));
55        }
56        let secret = credentials.secret_access_key().expose_secret();
57        let session_token = credentials.session_token().map(ExposeSecret::expose_secret);
58        let signing_credentials = SigningCredentials::new(
59            credentials.access_key_id(),
60            secret.as_bytes(),
61            session_token,
62        );
63        let headers = [Header::new("host", target.authority())];
64        let signing_request = PresigningRequest {
65            method,
66            uri_path: SigningPath::encoded(target.path_and_query()),
67            query: &[],
68            headers: &headers,
69            expires,
70            payload_hash: None,
71        };
72        let query: crate::signing::PresignedQuery = sign_presigned(
73            &signing_credentials,
74            SigningScope::new(self.inner.config.region(), "s3"),
75            &signing_request,
76            OffsetDateTime::now_utc(),
77        )
78        .map_err(|error| {
79            S3Error::new(
80                ErrorCategory::Authentication,
81                "presigned URL generation failed",
82                RetryClassification::Never,
83            )
84            .with_source(error)
85        })?;
86        Ok(PresignedUrl::new(format!(
87            "{}?{}",
88            target.as_str(),
89            query.as_str()
90        )))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use std::sync::Arc;
97
98    use super::*;
99    use crate::config::S3Config;
100    use crate::credentials::{Credentials, StaticCredentialsProvider};
101
102    #[tokio::test]
103    async fn presigned_output_requires_explicit_exposure() {
104        let credentials = Credentials::new("access", "secret", None).unwrap();
105        let config = S3Config::builder()
106            .bucket("bucket")
107            .credentials_provider(Arc::new(StaticCredentialsProvider::new(credentials)))
108            .build()
109            .unwrap();
110        let client = S3Client::new(config).unwrap();
111        let url = client
112            .presigned_get(&ObjectKey::new("a/../b").unwrap(), Duration::from_secs(60))
113            .await
114            .unwrap();
115        assert!(!format!("{url:?}").contains("X-Amz-Signature"));
116        assert!(url.expose().contains("/bucket/a/../b?"));
117        assert!(url.expose().contains("X-Amz-Signature="));
118    }
119}