Unverified Commit 05d9ec9a authored by John DiSanti's avatar John DiSanti Committed by GitHub
Browse files

Add support for S3 presigned requests (#731)

* Refactor inlineables so that they can be made public

* Clean up HttpProtocolGenerator

* Add presigning inline module

* Add synthetic presignable trait to known operations

* Stub out input `presigned` method

* Add example of presigning GetObject with the input

* Generate `presigned` on fluent builders

* Wire up `presign` to the AWS middleware

* Add example for S3 PutObject presigned

* Wire up middleware to query param signer

* Fix query param signing with security tokens

* Clean up AwsPresigningDecorator and add RustSettings to decorators

* Add test for presigned S3 GetObject and fix issues discovered

* Add doc comments to aws-inlineable

* Update changelog

* Revert addition of RustSettings to decorators

* Incorporate feedback

* Fix CI

* Improve doc comment
parent 1ca2b469
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -6,6 +6,10 @@ vNext (Month Day, Year)
**Tasks to cut release**
- [ ] Bump MSRV on aws-sdk-rust, then delete this line.

**New This Week**

- :tada: Add presigned request support and examples for S3 GetObject and PutObject (smithy-rs#731)

v0.0.19-alpha (September 24th, 2021)
====================================

+5 −3
Original line number Diff line number Diff line
@@ -9,7 +9,9 @@ are to allow this crate to be compilable and testable in isolation, no client co
"""

[dependencies]
smithy-xml = { path = "../../../rust-runtime/smithy-xml" }
smithy-types = { path = "../../../rust-runtime/smithy-types" }
http = "0.2.4"
aws-types = { path = "../../rust-runtime/aws-types" }
http = "0.2.4"
smithy-http = { path = "../../../rust-runtime/smithy-http" }
smithy-types = { path = "../../../rust-runtime/smithy-types" }
smithy-xml = { path = "../../../rust-runtime/smithy-xml" }
tower = { version = "0.4", no-default-features = true }
+22 −3
Original line number Diff line number Diff line
@@ -3,7 +3,26 @@
 * SPDX-License-Identifier: Apache-2.0.
 */

#![allow(dead_code)]
//! Collection of modules that get conditionally included directly into the code generated
//! SDK service crates. For example, when generating S3, the `s3_errors` module will get copied
//! into the generated S3 crate to support the code generator.
//!
//! This is _NOT_ intended to be an actual crate. It is a cargo project to solely to aid
//! with local development of the SDK.

mod no_credentials;
mod s3_errors;
#![warn(
    missing_docs,
    missing_crate_level_docs,
    missing_debug_implementations,
    rust_2018_idioms,
    unreachable_pub
)]

/// Stub credentials provider for use when no credentials provider is used.
pub mod no_credentials;

/// Support types required for adding presigning to an operation in a generated service.
pub mod presigning;

/// Special logic for handling S3's error responses.
pub mod s3_errors;
+1 −1
Original line number Diff line number Diff line
@@ -6,7 +6,7 @@
use aws_types::credentials::future;
use aws_types::credentials::{CredentialsError, ProvideCredentials};

/// Stub Provider for use when no credentials provider is used
/// Stub credentials provider for use when no credentials provider is used.
#[non_exhaustive]
#[derive(Debug)]
pub struct NoCredentials;
+255 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

/// Presigning config and builder
pub mod config {
    use std::fmt;
    use std::time::{Duration, SystemTime};

    const ONE_WEEK: Duration = Duration::from_secs(604800);

    /// Presigning config values required for creating a presigned request.
    #[non_exhaustive]
    #[derive(Debug, Clone)]
    pub struct PresigningConfig {
        start_time: SystemTime,
        expires_in: Duration,
    }

    impl PresigningConfig {
        /// Creates a `PresigningConfig` with the given `expires_in` duration as the total
        /// amount of time the presigned request should be valid for. Other config values are
        /// defaulted.
        ///
        /// # Note
        ///
        /// Credential expiration time takes priority over the `expires_in` value.
        /// If the credentials used to sign the request expire before the presigned request is
        /// set to expire, then the presigned request will become invalid.
        pub fn expires_in(expires_in: Duration) -> Result<PresigningConfig, Error> {
            Self::builder().expires_in(expires_in).build()
        }

        /// Creates a new builder for creating a `PresigningConfig`.
        pub fn builder() -> Builder {
            Builder::default()
        }

        /// Returns the amount of time the presigned request should be valid for.
        pub fn expires(&self) -> Duration {
            self.expires_in
        }

        /// Returns the start time. The presigned request will be valid between this and the end
        /// time produced by adding the `expires()` value to it.
        pub fn start_time(&self) -> SystemTime {
            self.start_time
        }
    }

    /// `PresigningConfig` build errors.
    #[non_exhaustive]
    #[derive(Debug)]
    pub enum Error {
        /// Presigned requests cannot be valid for longer than one week.
        ExpiresInDurationTooLong,

        /// The `PresigningConfig` builder requires a value for `expires_in`.
        ExpiresInRequired,
    }

    impl std::error::Error for Error {}

    impl fmt::Display for Error {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            match self {
                Error::ExpiresInDurationTooLong => {
                    write!(f, "`expires_in` must be no longer than one week")
                }
                Error::ExpiresInRequired => write!(f, "`expires_in` is required"),
            }
        }
    }

    /// Builder used to create `PresigningConfig`.
    #[non_exhaustive]
    #[derive(Default, Debug)]
    pub struct Builder {
        start_time: Option<SystemTime>,
        expires_in: Option<Duration>,
    }

    impl Builder {
        /// Sets the start time for the presigned request.
        ///
        /// The request will start to be valid at this time, and will cease to be valid after
        /// the end time, which can be determined by adding the `expires_in` duration to this
        /// start time. If not specified, this will default to the current time.
        ///
        /// Optional.
        pub fn start_time(mut self, start_time: SystemTime) -> Self {
            self.set_start_time(Some(start_time));
            self
        }

        /// Sets the start time for the presigned request.
        ///
        /// The request will start to be valid at this time, and will cease to be valid after
        /// the end time, which can be determined by adding the `expires_in` duration to this
        /// start time. If not specified, this will default to the current time.
        ///
        /// Optional.
        pub fn set_start_time(&mut self, start_time: Option<SystemTime>) {
            self.start_time = start_time;
        }

        /// Sets how long the request should be valid after the `start_time` (which defaults
        /// to the current time).
        ///
        /// Required.
        ///
        /// # Note
        ///
        /// Credential expiration time takes priority over the `expires_in` value.
        /// If the credentials used to sign the request expire before the presigned request is
        /// set to expire, then the presigned request will become invalid.
        pub fn expires_in(mut self, expires_in: Duration) -> Self {
            self.set_expires_in(Some(expires_in));
            self
        }

        /// Sets how long the request should be valid after the `start_time` (which defaults
        /// to the current time).
        ///
        /// Required.
        ///
        /// # Note
        ///
        /// Credential expiration time takes priority over the `expires_in` value.
        /// If the credentials used to sign the request expire before the presigned request is
        /// set to expire, then the presigned request will become invalid.
        pub fn set_expires_in(&mut self, expires_in: Option<Duration>) {
            self.expires_in = expires_in;
        }

        /// Builds the `PresigningConfig`. This will error if `expires_in` is not
        /// given, or if it's longer than one week.
        pub fn build(self) -> Result<PresigningConfig, Error> {
            let expires_in = self.expires_in.ok_or(Error::ExpiresInRequired)?;
            if expires_in > ONE_WEEK {
                return Err(Error::ExpiresInDurationTooLong);
            }
            Ok(PresigningConfig {
                start_time: self.start_time.unwrap_or_else(SystemTime::now),
                expires_in,
            })
        }
    }
}

/// Presigned request
pub mod request {
    use std::fmt::{Debug, Formatter};

    /// Represents a presigned request. This only includes the HTTP request method, URI, and headers.
    #[non_exhaustive]
    pub struct PresignedRequest(http::Request<()>);

    impl PresignedRequest {
        pub(crate) fn new(inner: http::Request<()>) -> Self {
            Self(inner)
        }

        /// Returns the HTTP request method.
        pub fn method(&self) -> &http::Method {
            self.0.method()
        }

        /// Returns the HTTP request URI.
        pub fn uri(&self) -> &http::Uri {
            self.0.uri()
        }

        /// Returns any HTTP headers that need to go along with the request, except for `Host`,
        /// which should be sent based on the endpoint in the URI by the HTTP client rather than
        /// added directly.
        pub fn headers(&self) -> &http::HeaderMap<http::HeaderValue> {
            self.0.headers()
        }
    }

    impl Debug for PresignedRequest {
        fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
            f.debug_struct("PresignedRequest")
                .field("method", self.method())
                .field("uri", self.uri())
                .field("headers", self.headers())
                .finish()
        }
    }
}

/// Tower middleware service for creating presigned requests
#[allow(dead_code)]
pub(crate) mod service {
    use crate::presigning::request::PresignedRequest;
    use http::header::{CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT};
    use smithy_http::operation;
    use std::future::{ready, Ready};
    use std::marker::PhantomData;
    use std::task::{Context, Poll};

    /// Tower [`Service`](tower::Service) for generated a [`PresignedRequest`] from the AWS middleware.
    #[derive(Default, Debug)]
    #[non_exhaustive]
    pub(crate) struct PresignedRequestService<E> {
        _phantom: PhantomData<E>,
    }

    // Required because of the derive Clone on MapRequestService.
    // Manually implemented to avoid requiring errors to implement Clone.
    impl<E> Clone for PresignedRequestService<E> {
        fn clone(&self) -> Self {
            Self {
                _phantom: Default::default(),
            }
        }
    }

    impl<E> PresignedRequestService<E> {
        /// Creates a new `PresignedRequestService`
        pub(crate) fn new() -> Self {
            Self {
                _phantom: Default::default(),
            }
        }
    }

    impl<E> tower::Service<operation::Request> for PresignedRequestService<E> {
        type Response = PresignedRequest;
        type Error = E;
        type Future = Ready<Result<PresignedRequest, E>>;

        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
            Poll::Ready(Ok(()))
        }

        fn call(&mut self, req: operation::Request) -> Self::Future {
            let (mut req, _) = req.into_parts();

            // Remove headers from input serialization that shouldn't be part of the presigned
            // request since the request body is unsigned and left up to the person making the final
            // HTTP request.
            req.headers_mut().remove(CONTENT_LENGTH);
            req.headers_mut().remove(CONTENT_TYPE);

            // Remove user agent headers since the request will not be executed by the AWS Rust SDK.
            req.headers_mut().remove(USER_AGENT);
            req.headers_mut().remove("X-Amz-User-Agent");

            ready(Ok(PresignedRequest::new(req.map(|_| ()))))
        }
    }
}
Loading