s3/
post_policy.rs

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