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.operation_target(Some(key.as_str()))?;
39        let credentials = self
40            .inner
41            .config
42            .credentials_provider()
43            .provide_credentials()
44            .await?;
45        if credentials.expires_by(OffsetDateTime::now_utc()) {
46            return Err(S3Error::new(
47                ErrorCategory::Authentication,
48                "credential provider returned expired credentials",
49                RetryClassification::Never,
50            ));
51        }
52        let secret = credentials.secret_access_key().expose_secret();
53        let session_token = credentials.session_token().map(ExposeSecret::expose_secret);
54        let signing_credentials = SigningCredentials::new(
55            credentials.access_key_id(),
56            secret.as_bytes(),
57            session_token,
58        );
59        let headers = [Header::new("host", target.authority())];
60        let signing_request = PresigningRequest {
61            method,
62            uri_path: SigningPath::encoded(target.path_and_query()),
63            query: &[],
64            headers: &headers,
65            expires,
66            payload_hash: None,
67        };
68        let query: crate::signing::PresignedQuery = sign_presigned(
69            &signing_credentials,
70            SigningScope::new(self.inner.config.region(), "s3"),
71            &signing_request,
72            OffsetDateTime::now_utc(),
73        )
74        .map_err(|error| {
75            S3Error::new(
76                ErrorCategory::Authentication,
77                "presigned URL generation failed",
78                RetryClassification::Never,
79            )
80            .with_source(error)
81        })?;
82        Ok(PresignedUrl::new(format!(
83            "{}?{}",
84            target.as_str(),
85            query.as_str()
86        )))
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use std::sync::Arc;
93
94    use super::*;
95    use crate::config::S3Config;
96    use crate::credentials::{Credentials, StaticCredentialsProvider};
97
98    #[tokio::test]
99    async fn presigned_output_requires_explicit_exposure() {
100        let credentials = Credentials::new("access", "secret", None).unwrap();
101        let config = S3Config::builder()
102            .bucket("bucket")
103            .credentials_provider(Arc::new(StaticCredentialsProvider::new(credentials)))
104            .build()
105            .unwrap();
106        let client = S3Client::new(config).unwrap();
107        let url = client
108            .presigned_get(&ObjectKey::new("a/../b").unwrap(), Duration::from_secs(60))
109            .await
110            .unwrap();
111        assert!(!format!("{url:?}").contains("X-Amz-Signature"));
112        assert!(url.expose().contains("/bucket/a/../b?"));
113        assert!(url.expose().contains("X-Amz-Signature="));
114    }
115}