s3/
post_policy.rs

1//! This module provides functionality for creating and managing S3 POST policies, which are used to generate presigned URLs for securely uploading files to S3 with specific conditions and metadata. It handles the generation of necessary conditions, expiration policies, and signing of the POST policy.
2//!
3//! ## Key Components
4//!
5//! - **PostPolicy Struct**
6//!   - Represents a POST policy that specifies conditions and expiration for an S3 upload.
7//!   - Can be constructed with an expiration time and modified by adding conditions for fields like `key`, `acl`, and more.
8//!   - Supports building a policy with AWS credentials and signing it to generate a `PresignedPost` object.
9//!
10//! - **PostPolicyField Enum**
11//!   - Enumerates various fields that can be included in a POST policy, such as `key`, `acl`, `content-length-range`, and AWS-specific fields like `x-amz-meta-*`.
12//!   - Allows for the addition of custom fields not predefined in the enum.
13//!
14//! - **PostPolicyValue Enum**
15//!   - Represents the value type associated with a `PostPolicyField`, including exact matches, start-with conditions, ranges, and wildcard matches.
16//!
17//! - **PostPolicyExpiration Enum**
18//!   - Defines the expiration of the POST policy, either as a duration from the current time or a specific timestamp.
19//!
20//! - **PresignedPost Struct**
21//!   - Contains the URL and fields necessary for making a POST request to S3, generated after signing a `PostPolicy`.
22//!   - Includes dynamic fields for conditions that vary at upload time, such as the key or content length.
23//!
24//! ## Error Handling
25//!
26//! - **PostPolicyError Enum**
27//!   - Contains error variants that can occur when constructing a POST policy, particularly when there is a mismatch between the expected and provided condition types.
28
29use crate::error::S3Error;
30use crate::utils::now_utc;
31use crate::{Bucket, LONG_DATETIME, signing};
32
33use awscreds::Rfc3339OffsetDateTime;
34use awscreds::error::CredentialsError;
35use serde::ser;
36use serde::ser::{Serialize, SerializeMap, SerializeSeq, SerializeTuple, Serializer};
37use std::borrow::Cow;
38use std::collections::HashMap;
39use thiserror::Error;
40use time::{Duration, OffsetDateTime};
41
42#[derive(Clone, Debug)]
43pub struct PostPolicy<'a> {
44    expiration: PostPolicyExpiration,
45    conditions: ConditionsSerializer<'a>,
46}
47
48impl<'a> PostPolicy<'a> {
49    pub fn new<T>(expiration: T) -> Self
50    where
51        T: Into<PostPolicyExpiration>,
52    {
53        Self {
54            expiration: expiration.into(),
55            conditions: ConditionsSerializer(Vec::new()),
56        }
57    }
58
59    /// Build a finalized post policy with credentials
60    #[maybe_async::maybe_async]
61    async fn build(
62        &self,
63        now: &OffsetDateTime,
64        bucket: &Bucket,
65    ) -> Result<PostPolicy<'_>, S3Error> {
66        let access_key = bucket.access_key().await?.ok_or(S3Error::Credentials(
67            CredentialsError::ConfigMissingAccessKeyId,
68        ))?;
69        let credential = format!(
70            "{}/{}",
71            access_key,
72            signing::scope_string(now, &bucket.region)?
73        );
74
75        let mut post_policy = self
76            .clone()
77            .condition(
78                PostPolicyField::Bucket,
79                PostPolicyValue::Exact(Cow::from(bucket.name.clone())),
80            )?
81            .condition(
82                PostPolicyField::AmzAlgorithm,
83                PostPolicyValue::Exact(Cow::from("AWS4-HMAC-SHA256")),
84            )?
85            .condition(
86                PostPolicyField::AmzCredential,
87                PostPolicyValue::Exact(Cow::from(credential)),
88            )?
89            .condition(
90                PostPolicyField::AmzDate,
91                PostPolicyValue::Exact(Cow::from(now.format(LONG_DATETIME)?)),
92            )?;
93
94        if let Some(security_token) = bucket.security_token().await? {
95            post_policy = post_policy.condition(
96                PostPolicyField::AmzSecurityToken,
97                PostPolicyValue::Exact(Cow::from(security_token)),
98            )?;
99        }
100        Ok(post_policy.clone())
101    }
102
103    fn policy_string(&self) -> Result<String, S3Error> {
104        use base64::Engine;
105        use base64::engine::general_purpose;
106
107        let data = serde_json::to_string(self)?;
108
109        Ok(general_purpose::STANDARD.encode(data))
110    }
111
112    #[maybe_async::maybe_async]
113    pub async fn sign(&self, bucket: Box<Bucket>) -> Result<PresignedPost, S3Error> {
114        use hmac::Mac;
115
116        bucket.credentials_refresh().await?;
117        let now = now_utc();
118
119        let policy = self.build(&now, &bucket).await?;
120        let policy_string = policy.policy_string()?;
121
122        let signing_key = signing::signing_key(
123            &now,
124            &bucket.secret_key().await?.ok_or(S3Error::Credentials(
125                CredentialsError::ConfigMissingSecretKey,
126            ))?,
127            &bucket.region,
128            "s3",
129        )?;
130
131        let mut hmac = signing::HmacSha256::new_from_slice(&signing_key)?;
132        hmac.update(policy_string.as_bytes());
133        let signature = hex::encode(hmac.finalize().into_bytes());
134        let mut fields: HashMap<String, String> = HashMap::new();
135        let mut dynamic_fields = HashMap::new();
136        for field in policy.conditions.0.iter() {
137            let f: Cow<str> = field.field.clone().into();
138            match &field.value {
139                PostPolicyValue::Anything => {
140                    dynamic_fields.insert(f.to_string(), "".to_string());
141                }
142                PostPolicyValue::StartsWith(e) => {
143                    dynamic_fields.insert(f.to_string(), e.clone().into_owned());
144                }
145                PostPolicyValue::Range(b, e) => {
146                    dynamic_fields.insert(f.to_string(), format!("{},{}", b, e));
147                }
148                PostPolicyValue::Exact(e) => {
149                    fields.insert(f.to_string(), e.clone().into_owned());
150                }
151            }
152        }
153        fields.insert("x-amz-signature".to_string(), signature);
154        fields.insert("Policy".to_string(), policy_string);
155        let url = bucket.url();
156        Ok(PresignedPost {
157            url,
158            fields,
159            dynamic_fields,
160            expiration: policy.expiration.into(),
161        })
162    }
163
164    /// Adds another condition to the policy by consuming this object
165    pub fn condition(
166        mut self,
167        field: PostPolicyField<'a>,
168        value: PostPolicyValue<'a>,
169    ) -> Result<Self, S3Error> {
170        if matches!(field, PostPolicyField::ContentLengthRange)
171            != matches!(value, PostPolicyValue::Range(_, _))
172        {
173            Err(PostPolicyError::MismatchedCondition)?
174        }
175        self.conditions.0.push(PostPolicyCondition { field, value });
176        Ok(self)
177    }
178}
179
180impl Serialize for PostPolicy<'_> {
181    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: Serializer,
184    {
185        let mut map = serializer.serialize_map(Some(2))?;
186        map.serialize_entry("expiration", &self.expiration)?;
187        map.serialize_entry("conditions", &self.conditions)?;
188        map.end()
189    }
190}
191
192#[derive(Clone, Debug)]
193struct ConditionsSerializer<'a>(Vec<PostPolicyCondition<'a>>);
194
195impl Serialize for ConditionsSerializer<'_> {
196    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
197    where
198        S: Serializer,
199    {
200        let mut seq = serializer.serialize_seq(None)?;
201        for e in self.0.iter() {
202            if let PostPolicyField::AmzChecksumAlgorithm(checksum) = &e.field {
203                let checksum: Cow<str> = (*checksum).into();
204                seq.serialize_element(&PostPolicyCondition {
205                    field: PostPolicyField::Custom(Cow::from("x-amz-checksum-algorithm")),
206                    value: PostPolicyValue::Exact(Cow::from(checksum.to_uppercase())),
207                })?;
208            }
209            seq.serialize_element(&e)?;
210        }
211        seq.end()
212    }
213}
214
215#[derive(Clone, Debug)]
216struct PostPolicyCondition<'a> {
217    field: PostPolicyField<'a>,
218    value: PostPolicyValue<'a>,
219}
220
221impl Serialize for PostPolicyCondition<'_> {
222    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
223    where
224        S: Serializer,
225    {
226        let f: Cow<str> = self.field.clone().into();
227
228        match &self.value {
229            PostPolicyValue::Exact(e) => {
230                let mut map = serializer.serialize_map(Some(1))?;
231                map.serialize_entry(&f, e)?;
232                map.end()
233            }
234            PostPolicyValue::StartsWith(e) => {
235                let mut seq = serializer.serialize_tuple(3)?;
236                seq.serialize_element("starts-with")?;
237                let field = format!("${}", f);
238                seq.serialize_element(&field)?;
239                seq.serialize_element(e)?;
240                seq.end()
241            }
242            PostPolicyValue::Anything => {
243                let mut seq = serializer.serialize_tuple(3)?;
244                seq.serialize_element("starts-with")?;
245                let field = format!("${}", f);
246                seq.serialize_element(&field)?;
247                seq.serialize_element("")?;
248                seq.end()
249            }
250            PostPolicyValue::Range(b, e) => {
251                if matches!(self.field, PostPolicyField::ContentLengthRange) {
252                    let mut seq = serializer.serialize_tuple(3)?;
253                    seq.serialize_element("content-length-range")?;
254                    seq.serialize_element(b)?;
255                    seq.serialize_element(e)?;
256                    seq.end()
257                } else {
258                    Err(ser::Error::custom(
259                        "Range is only valid for ContentLengthRange",
260                    ))
261                }
262            }
263        }
264    }
265}
266
267/// Policy fields to add to the conditions of the policy
268#[derive(Clone, Debug)]
269#[non_exhaustive]
270pub enum PostPolicyField<'a> {
271    /// The destination path. Supports [`PostPolicyValue::StartsWith`]
272    Key,
273    /// The ACL policy. Supports [`PostPolicyValue::StartsWith`]
274    Acl,
275    /// Custom tag XML document
276    Tagging,
277    /// Successful redirect URL. Supports [`PostPolicyValue::StartsWith`]
278    SuccessActionRedirect,
279    /// Successful action status (e.g. 200, 201, or 204).
280    SuccessActionStatus,
281
282    /// The cache control  Supports [`PostPolicyValue::StartsWith`]
283    CacheControl,
284    /// The content length (must use the [`PostPolicyValue::Range`])
285    ContentLengthRange,
286    /// The content type. Supports [`PostPolicyValue::StartsWith`]
287    ContentType,
288    /// Content Disposition. Supports [`PostPolicyValue::StartsWith`]
289    ContentDisposition,
290    /// The content encoding. Supports [`PostPolicyValue::StartsWith`]
291    ContentEncoding,
292    /// The Expires header to respond when fetching. Supports [`PostPolicyValue::StartsWith`]
293    Expires,
294
295    /// The server-side encryption type
296    AmzServerSideEncryption,
297    /// The SSE key ID to use (if the algorithm specified requires it)
298    AmzServerSideEncryptionKeyId,
299    /// The SSE context to use (if the algorithm specified requires it)
300    AmzServerSideEncryptionContext,
301    /// The storage class to use
302    AmzStorageClass,
303    /// Specify a bucket relative or absolute UR redirect to redirect to when fetching this object
304    AmzWebsiteRedirectLocation,
305    /// Checksum algorithm, the value is the checksum
306    AmzChecksumAlgorithm(PostPolicyChecksum),
307    /// Any user-defined meta fields (AmzMeta("uuid".to_string) creates an x-amz-meta-uuid)
308    AmzMeta(Cow<'a, str>),
309
310    /// The credential. Auto added by the presign_post
311    AmzCredential,
312    /// The signing algorithm. Auto added by the presign_post
313    AmzAlgorithm,
314    /// The signing date. Auto added by the presign_post
315    AmzDate,
316    /// The Security token (for Amazon DevPay)
317    AmzSecurityToken,
318    /// The Bucket. Auto added by the presign_post
319    Bucket,
320
321    /// Custom field. Any other string not enumerated above
322    Custom(Cow<'a, str>),
323}
324
325#[allow(clippy::from_over_into)]
326impl<'a> Into<Cow<'a, str>> for PostPolicyField<'a> {
327    fn into(self) -> Cow<'a, str> {
328        match self {
329            PostPolicyField::Key => Cow::from("key"),
330            PostPolicyField::Acl => Cow::from("acl"),
331            PostPolicyField::Tagging => Cow::from("tagging"),
332            PostPolicyField::SuccessActionRedirect => Cow::from("success_action_redirect"),
333            PostPolicyField::SuccessActionStatus => Cow::from("success_action_status"),
334            PostPolicyField::CacheControl => Cow::from("Cache-Control"),
335            PostPolicyField::ContentLengthRange => Cow::from("content-length-range"),
336            PostPolicyField::ContentType => Cow::from("Content-Type"),
337            PostPolicyField::ContentDisposition => Cow::from("Content-Disposition"),
338            PostPolicyField::ContentEncoding => Cow::from("Content-Encoding"),
339            PostPolicyField::Expires => Cow::from("Expires"),
340
341            PostPolicyField::AmzServerSideEncryption => Cow::from("x-amz-server-side-encryption"),
342            PostPolicyField::AmzServerSideEncryptionKeyId => {
343                Cow::from("x-amz-server-side-encryption-aws-kms-key-id")
344            }
345            PostPolicyField::AmzServerSideEncryptionContext => {
346                Cow::from("x-amz-server-side-encryption-context")
347            }
348            PostPolicyField::AmzStorageClass => Cow::from("x-amz-storage-class"),
349            PostPolicyField::AmzWebsiteRedirectLocation => {
350                Cow::from("x-amz-website-redirect-location")
351            }
352            PostPolicyField::AmzChecksumAlgorithm(e) => {
353                let e: Cow<str> = e.into();
354                Cow::from(format!("x-amz-checksum-{}", e))
355            }
356            PostPolicyField::AmzMeta(e) => Cow::from(format!("x-amz-meta-{}", e)),
357            PostPolicyField::AmzCredential => Cow::from("x-amz-credential"),
358            PostPolicyField::AmzAlgorithm => Cow::from("x-amz-algorithm"),
359            PostPolicyField::AmzDate => Cow::from("x-amz-date"),
360            PostPolicyField::AmzSecurityToken => Cow::from("x-amz-security-token"),
361            PostPolicyField::Bucket => Cow::from("bucket"),
362            PostPolicyField::Custom(e) => e,
363        }
364    }
365}
366
367#[derive(Clone, Copy, Debug)]
368pub enum PostPolicyChecksum {
369    CRC32,
370    CRC32c,
371    SHA1,
372    SHA256,
373}
374
375#[allow(clippy::from_over_into)]
376impl<'a> Into<Cow<'a, str>> for PostPolicyChecksum {
377    fn into(self) -> Cow<'a, str> {
378        match self {
379            PostPolicyChecksum::CRC32 => Cow::from("crc32"),
380            PostPolicyChecksum::CRC32c => Cow::from("crc32c"),
381            PostPolicyChecksum::SHA1 => Cow::from("sha1"),
382            PostPolicyChecksum::SHA256 => Cow::from("sha256"),
383        }
384    }
385}
386
387#[derive(Clone, Debug)]
388pub enum PostPolicyValue<'a> {
389    /// Shortcut for StartsWith("".to_string())
390    Anything,
391    /// A string starting with a value
392    StartsWith(Cow<'a, str>),
393    /// A range of integer values. Only valid for some fields
394    Range(u32, u32),
395    /// An exact string value
396    Exact(Cow<'a, str>),
397}
398
399#[derive(Clone, Debug)]
400pub enum PostPolicyExpiration {
401    /// Expires in X seconds from "now"
402    ExpiresIn(u32),
403    /// Expires at exactly this time
404    ExpiresAt(Rfc3339OffsetDateTime),
405}
406
407impl From<u32> for PostPolicyExpiration {
408    fn from(value: u32) -> Self {
409        Self::ExpiresIn(value)
410    }
411}
412
413impl From<Rfc3339OffsetDateTime> for PostPolicyExpiration {
414    fn from(value: Rfc3339OffsetDateTime) -> Self {
415        Self::ExpiresAt(value)
416    }
417}
418
419impl From<PostPolicyExpiration> for Rfc3339OffsetDateTime {
420    fn from(value: PostPolicyExpiration) -> Self {
421        match value {
422            PostPolicyExpiration::ExpiresIn(d) => {
423                Rfc3339OffsetDateTime(now_utc().saturating_add(Duration::seconds(d as i64)))
424            }
425            PostPolicyExpiration::ExpiresAt(t) => t,
426        }
427    }
428}
429
430impl Serialize for PostPolicyExpiration {
431    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
432    where
433        S: Serializer,
434    {
435        Rfc3339OffsetDateTime::from(self.clone()).serialize(serializer)
436    }
437}
438
439#[derive(Debug)]
440pub struct PresignedPost {
441    pub url: String,
442    pub fields: HashMap<String, String>,
443    pub dynamic_fields: HashMap<String, String>,
444    pub expiration: Rfc3339OffsetDateTime,
445}
446
447#[derive(Error, Debug)]
448#[non_exhaustive]
449pub enum PostPolicyError {
450    #[error("This value is not supported for this field")]
451    MismatchedCondition,
452}
453
454#[cfg(test)]
455mod test {
456    use super::*;
457
458    use crate::creds::Credentials;
459    use crate::region::Region;
460    use crate::utils::with_timestamp;
461
462    use serde_json::json;
463
464    fn test_bucket() -> Box<Bucket> {
465        Bucket::new(
466            "rust-s3",
467            Region::UsEast1,
468            Credentials::new(
469                Some("AKIAIOSFODNN7EXAMPLE"),
470                Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
471                None,
472                None,
473                None,
474            )
475            .unwrap(),
476        )
477        .unwrap()
478    }
479
480    fn test_bucket_with_security_token() -> Box<Bucket> {
481        Bucket::new(
482            "rust-s3",
483            Region::UsEast1,
484            Credentials::new(
485                Some("AKIAIOSFODNN7EXAMPLE"),
486                Some("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
487                Some("SomeSecurityToken"),
488                None,
489                None,
490            )
491            .unwrap(),
492        )
493        .unwrap()
494    }
495
496    mod conditions {
497        use super::*;
498
499        #[test]
500        fn starts_with_condition() {
501            let policy = PostPolicy::new(300)
502                .condition(
503                    PostPolicyField::Key,
504                    PostPolicyValue::StartsWith(Cow::from("users/user1/")),
505                )
506                .unwrap();
507
508            let data = serde_json::to_value(&policy).unwrap();
509
510            assert!(data["expiration"].is_string());
511            assert_eq!(
512                data["conditions"],
513                json!([["starts-with", "$key", "users/user1/"]])
514            );
515        }
516
517        #[test]
518        fn exact_condition() {
519            let policy = PostPolicy::new(300)
520                .condition(
521                    PostPolicyField::Acl,
522                    PostPolicyValue::Exact(Cow::from("public-read")),
523                )
524                .unwrap();
525
526            let data = serde_json::to_value(&policy).unwrap();
527
528            assert!(data["expiration"].is_string());
529            assert_eq!(data["conditions"], json!([{"acl":"public-read"}]));
530        }
531
532        #[test]
533        fn anything_condition() {
534            let policy = PostPolicy::new(300)
535                .condition(PostPolicyField::Key, PostPolicyValue::Anything)
536                .unwrap();
537
538            let data = serde_json::to_value(&policy).unwrap();
539
540            assert!(data["expiration"].is_string());
541            assert_eq!(data["conditions"], json!([["starts-with", "$key", ""]]));
542        }
543
544        #[test]
545        fn range_condition() {
546            let policy = PostPolicy::new(300)
547                .condition(
548                    PostPolicyField::ContentLengthRange,
549                    PostPolicyValue::Range(0, 3_000_000),
550                )
551                .unwrap();
552
553            let data = serde_json::to_value(&policy).unwrap();
554
555            assert!(data["expiration"].is_string());
556            assert_eq!(
557                data["conditions"],
558                json!([["content-length-range", 0, 3_000_000]])
559            );
560        }
561
562        #[test]
563        fn range_condition_for_non_content_length_range() -> Result<(), S3Error> {
564            let result = PostPolicy::new(86400)
565                .condition(PostPolicyField::ContentType, PostPolicyValue::Range(0, 100));
566
567            assert!(matches!(
568                result,
569                Err(S3Error::PostPolicyError(
570                    PostPolicyError::MismatchedCondition
571                ))
572            ));
573
574            Ok(())
575        }
576
577        #[test]
578        fn starts_with_condition_for_content_length_range() -> Result<(), S3Error> {
579            let result = PostPolicy::new(86400).condition(
580                PostPolicyField::ContentLengthRange,
581                PostPolicyValue::StartsWith(Cow::from("")),
582            );
583
584            assert!(matches!(
585                result,
586                Err(S3Error::PostPolicyError(
587                    PostPolicyError::MismatchedCondition
588                ))
589            ));
590
591            Ok(())
592        }
593
594        #[test]
595        fn exact_condition_for_content_length_range() -> Result<(), S3Error> {
596            let result = PostPolicy::new(86400).condition(
597                PostPolicyField::ContentLengthRange,
598                PostPolicyValue::Exact(Cow::from("test")),
599            );
600
601            assert!(matches!(
602                result,
603                Err(S3Error::PostPolicyError(
604                    PostPolicyError::MismatchedCondition
605                ))
606            ));
607
608            Ok(())
609        }
610
611        #[test]
612        fn anything_condition_for_content_length_range() -> Result<(), S3Error> {
613            let result = PostPolicy::new(86400).condition(
614                PostPolicyField::ContentLengthRange,
615                PostPolicyValue::Anything,
616            );
617
618            assert!(matches!(
619                result,
620                Err(S3Error::PostPolicyError(
621                    PostPolicyError::MismatchedCondition
622                ))
623            ));
624
625            Ok(())
626        }
627
628        #[test]
629        fn checksum_policy() {
630            let policy = PostPolicy::new(300)
631                .condition(
632                    PostPolicyField::AmzChecksumAlgorithm(PostPolicyChecksum::SHA256),
633                    PostPolicyValue::Exact(Cow::from("abcdef1234567890")),
634                )
635                .unwrap();
636
637            let data = serde_json::to_value(&policy).unwrap();
638
639            assert!(data["expiration"].is_string());
640            assert_eq!(
641                data["conditions"],
642                json!([
643                    {"x-amz-checksum-algorithm": "SHA256"},
644                    {"x-amz-checksum-sha256": "abcdef1234567890"}
645                ])
646            );
647        }
648    }
649
650    mod build {
651        use super::*;
652
653        #[maybe_async::test(
654            feature = "sync",
655            async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
656            async(
657                all(not(feature = "sync"), feature = "with-async-std"),
658                async_std::test
659            )
660        )]
661        async fn adds_credentials() {
662            let policy = PostPolicy::new(86400)
663                .condition(
664                    PostPolicyField::Key,
665                    PostPolicyValue::StartsWith(Cow::from("user/user1/")),
666                )
667                .unwrap();
668
669            let bucket = test_bucket();
670
671            let _ts = with_timestamp(1_451_347_200);
672            let policy = policy.build(&now_utc(), &bucket).await.unwrap();
673
674            let data = serde_json::to_value(&policy).unwrap();
675
676            assert_eq!(
677                data["conditions"],
678                json!([
679                    ["starts-with", "$key", "user/user1/"],
680                    {"bucket": "rust-s3"},
681                    {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
682                    {"x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request"},
683                    {"x-amz-date": "20151229T000000Z"},
684                ])
685            );
686        }
687
688        #[maybe_async::test(
689            feature = "sync",
690            async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
691            async(
692                all(not(feature = "sync"), feature = "with-async-std"),
693                async_std::test
694            )
695        )]
696        async fn with_security_token() {
697            let policy = PostPolicy::new(86400)
698                .condition(
699                    PostPolicyField::Key,
700                    PostPolicyValue::StartsWith(Cow::from("user/user1/")),
701                )
702                .unwrap();
703
704            let bucket = test_bucket_with_security_token();
705
706            let _ts = with_timestamp(1_451_347_200);
707            let policy = policy.build(&now_utc(), &bucket).await.unwrap();
708
709            let data = serde_json::to_value(&policy).unwrap();
710
711            assert_eq!(
712                data["conditions"],
713                json!([
714                    ["starts-with", "$key", "user/user1/"],
715                    {"bucket": "rust-s3"},
716                    {"x-amz-algorithm": "AWS4-HMAC-SHA256"},
717                    {"x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request"},
718                    {"x-amz-date": "20151229T000000Z"},
719                    {"x-amz-security-token": "SomeSecurityToken"},
720                ])
721            );
722        }
723    }
724
725    mod policy_string {
726        use super::*;
727
728        #[test]
729        fn returns_base64_encoded() {
730            let policy = PostPolicy::new(129600)
731                .condition(
732                    PostPolicyField::Key,
733                    PostPolicyValue::StartsWith(Cow::from("user/user1/")),
734                )
735                .unwrap();
736
737            let _ts = with_timestamp(1_451_347_200);
738
739            let expected = "eyJleHBpcmF0aW9uIjoiMjAxNS0xMi0zMFQxMjowMDowMFoiLCJjb25kaXRpb25zIjpbWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJ1c2VyL3VzZXIxLyJdXX0=";
740
741            assert_eq!(policy.policy_string().unwrap(), expected);
742        }
743    }
744
745    mod sign {
746        use super::*;
747
748        #[maybe_async::test(
749            feature = "sync",
750            async(all(not(feature = "sync"), feature = "with-tokio"), tokio::test),
751            async(
752                all(not(feature = "sync"), feature = "with-async-std"),
753                async_std::test
754            )
755        )]
756        async fn returns_full_details() {
757            let policy = PostPolicy::new(86400)
758                .condition(
759                    PostPolicyField::Key,
760                    PostPolicyValue::StartsWith(Cow::from("user/user1/")),
761                )
762                .unwrap()
763                .condition(
764                    PostPolicyField::ContentLengthRange,
765                    PostPolicyValue::Range(0, 3_000_000),
766                )
767                .unwrap();
768
769            let bucket = test_bucket();
770
771            let _ts = with_timestamp(1_451_347_200);
772            let post = policy.sign(bucket).await.unwrap();
773
774            assert_eq!(post.url, "https://rust-s3.s3.amazonaws.com");
775            assert_eq!(
776                serde_json::to_value(&post.fields).unwrap(),
777                json!({
778                    "x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20151229/us-east-1/s3/aws4_request",
779                    "bucket": "rust-s3",
780                    "Policy": "eyJleHBpcmF0aW9uIjoiMjAxNS0xMi0zMFQwMDowMDowMFoiLCJjb25kaXRpb25zIjpbWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJ1c2VyL3VzZXIxLyJdLFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMzAwMDAwMF0seyJidWNrZXQiOiJydXN0LXMzIn0seyJ4LWFtei1hbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMjI5VDAwMDAwMFoifV19",
781                    "x-amz-date": "20151229T000000Z",
782                    "x-amz-signature": "0ff9c50ab7e543a841e91e5c663fd32117c5243e56e7a69db88f94ee95c4706f",
783                    "x-amz-algorithm": "AWS4-HMAC-SHA256"
784                })
785            );
786            assert_eq!(
787                serde_json::to_value(&post.dynamic_fields).unwrap(),
788                json!({
789                    "key": "user/user1/",
790                    "content-length-range": "0,3000000",
791                })
792            );
793        }
794    }
795}