diff --git a/aws/rust-runtime/Cargo.lock b/aws/rust-runtime/Cargo.lock index 986f38dba7..1cd58e0136 100644 --- a/aws/rust-runtime/Cargo.lock +++ b/aws/rust-runtime/Cargo.lock @@ -118,6 +118,7 @@ dependencies = [ "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", + "aws-types", "bytes", "fastrand", "hex", @@ -132,6 +133,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "url", ] [[package]] @@ -182,7 +184,7 @@ version = "0.60.3" [[package]] name = "aws-sigv4" -version = "1.2.4" +version = "1.2.5" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -227,7 +229,7 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.12" +version = "0.60.13" dependencies = [ "aws-smithy-http", "aws-smithy-types", diff --git a/aws/rust-runtime/aws-config/Cargo.lock b/aws/rust-runtime/aws-config/Cargo.lock index 979c59de51..5a1aa2a578 100644 --- a/aws/rust-runtime/aws-config/Cargo.lock +++ b/aws/rust-runtime/aws-config/Cargo.lock @@ -173,7 +173,7 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.4" +version = "1.2.5" dependencies = [ "aws-credential-types", "aws-smithy-http", diff --git a/aws/rust-runtime/aws-inlineable/Cargo.toml b/aws/rust-runtime/aws-inlineable/Cargo.toml index 054d41123c..07a00c99a1 100644 --- a/aws/rust-runtime/aws-inlineable/Cargo.toml +++ b/aws/rust-runtime/aws-inlineable/Cargo.toml @@ -18,6 +18,7 @@ http_1x = ["dep:http-1x", "dep:http-body-1x", "aws-smithy-runtime-api/http-1x"] aws-credential-types = { path = "../aws-credential-types" } aws-runtime = { path = "../aws-runtime", features = ["http-02x"] } aws-sigv4 = { path = "../aws-sigv4" } +aws-types = { path = "../aws-types" } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["rt-tokio"] } aws-smithy-checksums = { path = "../../../rust-runtime/aws-smithy-checksums" } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http" } @@ -37,8 +38,10 @@ ring = "0.17.5" sha2 = "0.10" tokio = "1.23.1" tracing = "0.1" +url = "2.5.2" [dev-dependencies] +aws-credential-types = { path = "../aws-credential-types", features = ["test-util"] } aws-smithy-async = { path = "../../../rust-runtime/aws-smithy-async", features = ["test-util"] } aws-smithy-http = { path = "../../../rust-runtime/aws-smithy-http", features = ["rt-tokio"] } aws-smithy-runtime-api = { path = "../../../rust-runtime/aws-smithy-runtime-api", features = ["test-util"] } diff --git a/aws/rust-runtime/aws-inlineable/external-types.toml b/aws/rust-runtime/aws-inlineable/external-types.toml index 0e15e561d4..e2ff3ff6c1 100644 --- a/aws/rust-runtime/aws-inlineable/external-types.toml +++ b/aws/rust-runtime/aws-inlineable/external-types.toml @@ -1,5 +1,6 @@ allowed_external_types = [ - "aws_credential_types::provider::credentials::ProvideCredentials", + "aws_types::*", + "aws_credential_types::*", "aws_smithy_http::*", "aws_smithy_runtime_api::*", diff --git a/aws/rust-runtime/aws-inlineable/src/lib.rs b/aws/rust-runtime/aws-inlineable/src/lib.rs index 8fdbaf88b8..3a07c16b56 100644 --- a/aws/rust-runtime/aws-inlineable/src/lib.rs +++ b/aws/rust-runtime/aws-inlineable/src/lib.rs @@ -65,3 +65,5 @@ pub mod s3_expires_interceptor; /// allow docs to work #[derive(Debug)] pub struct Client; + +pub mod rds_auth_token; diff --git a/aws/rust-runtime/aws-inlineable/src/rds_auth_token.rs b/aws/rust-runtime/aws-inlineable/src/rds_auth_token.rs new file mode 100644 index 0000000000..8a85aea547 --- /dev/null +++ b/aws/rust-runtime/aws-inlineable/src/rds_auth_token.rs @@ -0,0 +1,314 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Code related to creating signed URLs for logging in to RDS. +//! +//! For more information, see + +use aws_credential_types::provider::{ProvideCredentials, SharedCredentialsProvider}; +use aws_sigv4::http_request; +use aws_sigv4::http_request::{SignableBody, SignableRequest, SigningSettings}; +use aws_sigv4::sign::v4; +use aws_smithy_runtime_api::box_error::BoxError; +use aws_smithy_runtime_api::client::identity::Identity; +use aws_types::region::Region; +use std::fmt; +use std::fmt::Debug; +use std::time::Duration; + +const ACTION: &str = "connect"; +const SERVICE: &str = "rds-db"; + +/// A signer that generates an auth token for a database. +/// +/// ## Example +/// +/// ```ignore +/// use crate::auth_token::{AuthTokenGenerator, Config}; +/// +/// #[tokio::main] +/// async fn main() { +/// let cfg = aws_config::load_defaults(BehaviorVersion::latest()).await; +/// let generator = AuthTokenGenerator::new( +/// Config::builder() +/// .hostname("zhessler-test-db.cp7a4mblr2ig.us-east-1.rds.amazonaws.com") +/// .port(5432) +/// .username("zhessler") +/// .build() +/// .expect("cfg is valid"), +/// ); +/// let token = generator.auth_token(&cfg).await.unwrap(); +/// println!("{token}"); +/// } +/// ``` +#[derive(Debug)] +pub struct AuthTokenGenerator { + config: Config, +} + +/// An auth token usable as a password for an RDS database. +/// +/// This struct can be converted into a `&str` using the `Deref` trait or by calling `to_string()`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AuthToken { + inner: String, +} + +impl AuthToken { + /// Return the auth token as a `&str`. + #[must_use] + pub fn as_str(&self) -> &str { + &self.inner + } +} + +impl fmt::Display for AuthToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.inner) + } +} + +impl AuthTokenGenerator { + /// Given a `Config`, create a new RDS database login URL signer. + pub fn new(config: Config) -> Self { + Self { config } + } + + /// Return a signed URL usable as an auth token. + pub async fn auth_token( + &self, + config: &aws_types::sdk_config::SdkConfig, + ) -> Result { + let credentials = self + .config + .credentials() + .or(config.credentials_provider()) + .ok_or("credentials are required to create a signed URL for RDS")? + .provide_credentials() + .await?; + let identity: Identity = credentials.into(); + let region = self + .config + .region() + .or(config.region()) + .cloned() + .unwrap_or_else(|| Region::new("us-east-1")); + let time = config.time_source().ok_or("a time source is required")?; + + let mut signing_settings = SigningSettings::default(); + signing_settings.expires_in = Some(Duration::from_secs( + self.config.expires_in().unwrap_or(900).min(900), + )); + signing_settings.signature_location = http_request::SignatureLocation::QueryParams; + + let signing_params = v4::SigningParams::builder() + .identity(&identity) + .region(region.as_ref()) + .name(SERVICE) + .time(time.now()) + .settings(signing_settings) + .build()?; + + let url = format!( + "https://{}:{}/?Action={}&DBUser={}", + self.config.hostname(), + self.config.port(), + ACTION, + self.config.username() + ); + let signable_request = + SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::empty()) + .expect("signable request"); + + let (signing_instructions, _signature) = + http_request::sign(signable_request, &signing_params.into())?.into_parts(); + + let mut url = url::Url::parse(&url).unwrap(); + for (name, value) in signing_instructions.params() { + url.query_pairs_mut().append_pair(name, value); + } + let inner = url.to_string().split_off("https://".len()); + + Ok(AuthToken { inner }) + } +} + +/// Configuration for an RDS auth URL signer. +#[derive(Debug, Clone)] +pub struct Config { + /// The AWS credentials to sign requests with. + /// + /// Uses the default credential provider chain if not specified. + credentials: Option, + + /// The hostname of the database to connect to. + hostname: String, + + /// The port number the database is listening on. + port: u64, + + /// The region the database is located in. Uses the region inferred from the runtime if omitted. + region: Option, + + /// The username to login as. + username: String, + + /// The number of seconds the signed URL should be valid for. + /// + /// Maxes at 900 seconds. + expires_in: Option, +} + +impl Config { + /// Create a new `SignerConfigBuilder`. + pub fn builder() -> ConfigBuilder { + ConfigBuilder::default() + } + + /// The AWS credentials to sign requests with. + pub fn credentials(&self) -> Option { + self.credentials.clone() + } + + /// The hostname of the database to connect to. + pub fn hostname(&self) -> &str { + &self.hostname + } + + /// The port number the database is listening on. + pub fn port(&self) -> u64 { + self.port + } + + /// The region to sign requests with. + pub fn region(&self) -> Option<&Region> { + self.region.as_ref() + } + + /// The DB username to login as. + pub fn username(&self) -> &str { + &self.username + } + + /// The number of seconds the signed URL should be valid for. + /// + /// Maxes out at 900 seconds. + pub fn expires_in(&self) -> Option { + self.expires_in + } +} + +/// A builder for [`Config`]s. +#[derive(Debug, Default)] +pub struct ConfigBuilder { + /// The AWS credentials to create the auth token with. + /// + /// Uses the default credential provider chain if not specified. + credentials: Option, + + /// The hostname of the database to connect to. + hostname: Option, + + /// The port number the database is listening on. + port: Option, + + /// The region the database is located in. Uses the region inferred from the runtime if omitted. + region: Option, + + /// The database username to login as. + username: Option, + + /// The number of seconds the auth token should be valid for. + expires_in: Option, +} + +impl ConfigBuilder { + /// The AWS credentials to create the auth token with. + /// + /// Uses the default credential provider chain if not specified. + pub fn credentials(mut self, credentials: impl ProvideCredentials + 'static) -> Self { + self.credentials = Some(SharedCredentialsProvider::new(credentials)); + self + } + + /// The hostname of the database to connect to. + pub fn hostname(mut self, hostname: impl Into) -> Self { + self.hostname = Some(hostname.into()); + self + } + + /// The port number the database is listening on. + pub fn port(mut self, port: u64) -> Self { + self.port = Some(port); + self + } + + /// The region the database is located in. Uses the region inferred from the runtime if omitted. + pub fn region(mut self, region: Region) -> Self { + self.region = Some(region); + self + } + + /// The database username to login as. + pub fn username(mut self, username: impl Into) -> Self { + self.username = Some(username.into()); + self + } + + /// The number of seconds the signed URL should be valid for. + /// + /// Maxes out at 900 seconds. + pub fn expires_in(mut self, expires_in: u64) -> Self { + self.expires_in = Some(expires_in); + self + } + + /// Consume this builder, returning an error if required fields are missing. + /// Otherwise, return a new `SignerConfig`. + pub fn build(self) -> Result { + Ok(Config { + credentials: self.credentials, + hostname: self.hostname.ok_or("A hostname is required")?, + port: self.port.ok_or("a port is required")?, + region: self.region, + username: self.username.ok_or("a username is required")?, + expires_in: self.expires_in, + }) + } +} + +#[cfg(test)] +mod test { + use super::{AuthTokenGenerator, Config}; + use aws_credential_types::provider::SharedCredentialsProvider; + use aws_credential_types::Credentials; + use aws_smithy_async::test_util::ManualTimeSource; + use aws_types::region::Region; + use aws_types::SdkConfig; + use std::time::{Duration, UNIX_EPOCH}; + + #[tokio::test] + async fn signing_works() { + let time_source = ManualTimeSource::new(UNIX_EPOCH + Duration::from_secs(1724709600)); + let sdk_config = SdkConfig::builder() + .credentials_provider(SharedCredentialsProvider::new(Credentials::new( + "akid", "secret", None, None, "test", + ))) + .time_source(time_source) + .build(); + let signer = AuthTokenGenerator::new( + Config::builder() + .hostname("prod-instance.us-east-1.rds.amazonaws.com") + .port(3306) + .region(Region::new("us-east-1")) + .username("peccy") + .build() + .unwrap(), + ); + + let signed_url = signer.auth_token(&sdk_config).await.unwrap(); + assert_eq!(signed_url.as_str(), "prod-instance.us-east-1.rds.amazonaws.com:3306/?Action=connect&DBUser=peccy&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=akid%2F20240826%2Fus-east-1%2Frds-db%2Faws4_request&X-Amz-Date=20240826T220000Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=dd0cba843009474347af724090233265628ace491ea17ce3eb3da098b983ad89"); + } +} diff --git a/aws/rust-runtime/aws-sigv4/Cargo.toml b/aws/rust-runtime/aws-sigv4/Cargo.toml index 49af10cc21..0d247d5436 100644 --- a/aws/rust-runtime/aws-sigv4/Cargo.toml +++ b/aws/rust-runtime/aws-sigv4/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-sigv4" -version = "1.2.4" +version = "1.2.5" authors = ["AWS Rust SDK Team ", "David Barsky "] description = "SigV4 signer for HTTP requests and Event Stream messages." edition = "2021" diff --git a/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs b/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs index dc8308dfda..414570791d 100644 --- a/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs +++ b/aws/rust-runtime/aws-sigv4/src/http_request/sign.rs @@ -93,6 +93,13 @@ pub enum SignableBody<'a> { StreamingUnsignedPayloadTrailer, } +impl SignableBody<'_> { + /// Create a new empty signable body + pub fn empty() -> SignableBody<'static> { + SignableBody::Bytes(&[]) + } +} + /// Instructions for applying a signature to an HTTP request. #[derive(Debug)] pub struct SigningInstructions { diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt index 31c9588db9..afa1afe607 100644 --- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/AwsCodegenDecorator.kt @@ -19,6 +19,7 @@ import software.amazon.smithy.rustsdk.customize.ec2.Ec2Decorator import software.amazon.smithy.rustsdk.customize.glacier.GlacierDecorator import software.amazon.smithy.rustsdk.customize.lambda.LambdaDecorator import software.amazon.smithy.rustsdk.customize.onlyApplyTo +import software.amazon.smithy.rustsdk.customize.rds.RdsDecorator import software.amazon.smithy.rustsdk.customize.route53.Route53Decorator import software.amazon.smithy.rustsdk.customize.s3.S3Decorator import software.amazon.smithy.rustsdk.customize.s3.S3ExpiresDecorator @@ -77,6 +78,7 @@ val DECORATORS: List = Ec2Decorator().onlyApplyTo("com.amazonaws.ec2#AmazonEC2"), GlacierDecorator().onlyApplyTo("com.amazonaws.glacier#Glacier"), LambdaDecorator().onlyApplyTo("com.amazonaws.lambda#AWSGirApiService"), + RdsDecorator().onlyApplyTo("com.amazonaws.rds#AmazonRDSv19"), Route53Decorator().onlyApplyTo("com.amazonaws.route53#AWSDnsV20130401"), "com.amazonaws.s3#AmazonS3".applyDecorators( S3Decorator(), diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/rds/RdsDecorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/rds/RdsDecorator.kt new file mode 100644 index 0000000000..f5bf031a67 --- /dev/null +++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/rds/RdsDecorator.kt @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.rustsdk.customize.rds + +import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext +import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator +import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency +import software.amazon.smithy.rust.codegen.core.rustlang.Visibility +import software.amazon.smithy.rust.codegen.core.rustlang.rust +import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType +import software.amazon.smithy.rust.codegen.core.smithy.RustCrate +import software.amazon.smithy.rustsdk.AwsCargoDependency +import software.amazon.smithy.rustsdk.InlineAwsDependency + +class RdsDecorator : ClientCodegenDecorator { + override val name: String = "RDS" + override val order: Byte = 0 + + override fun extras( + codegenContext: ClientCodegenContext, + rustCrate: RustCrate, + ) { + val rc = codegenContext.runtimeConfig + + rustCrate.lib { + // We should have a better way of including an inline dependency. + rust( + "// include #T;", + RuntimeType.forInlineDependency( + InlineAwsDependency.forRustFileAs( + "rds_auth_token", + "auth_token", + Visibility.PUBLIC, + AwsCargoDependency.awsSigv4(rc), + CargoDependency.smithyRuntimeApiClient(rc), + CargoDependency.smithyAsync(rc).toDevDependency().withFeature("test-util"), + CargoDependency.Url, + ), + ), + ) + } + } +} diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index 1b609232bf..c84b962516 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -230,7 +230,7 @@ dependencies = [ "aws-runtime", "aws-sigv4", "aws-smithy-async 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "aws-smithy-checksums 0.60.12 (registry+https://github.com/rust-lang/crates.io-index)", + "aws-smithy-checksums 0.60.12", "aws-smithy-eventstream 0.60.5 (registry+https://github.com/rust-lang/crates.io-index)", "aws-smithy-http 0.60.11 (registry+https://github.com/rust-lang/crates.io-index)", "aws-smithy-json 0.60.7 (registry+https://github.com/rust-lang/crates.io-index)", @@ -317,11 +317,12 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" version = "0.60.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" dependencies = [ - "aws-smithy-http 0.60.11", - "aws-smithy-types 1.2.7", + "aws-smithy-http 0.60.11 (registry+https://github.com/rust-lang/crates.io-index)", + "aws-smithy-types 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "bytes", - "bytes-utils", "crc32c", "crc32fast", "hex", @@ -329,23 +330,19 @@ dependencies = [ "http-body 0.4.6", "md-5", "pin-project-lite", - "pretty_assertions", "sha1", "sha2", - "tokio", "tracing", - "tracing-test", ] [[package]] name = "aws-smithy-checksums" -version = "0.60.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" +version = "0.60.13" dependencies = [ - "aws-smithy-http 0.60.11 (registry+https://github.com/rust-lang/crates.io-index)", - "aws-smithy-types 1.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "aws-smithy-http 0.60.11", + "aws-smithy-types 1.2.7", "bytes", + "bytes-utils", "crc32c", "crc32fast", "hex", @@ -353,9 +350,12 @@ dependencies = [ "http-body 0.4.6", "md-5", "pin-project-lite", + "pretty_assertions", "sha1", "sha2", + "tokio", "tracing", + "tracing-test", ] [[package]] diff --git a/rust-runtime/aws-smithy-checksums/Cargo.toml b/rust-runtime/aws-smithy-checksums/Cargo.toml index 5c7a6b8a01..0888246992 100644 --- a/rust-runtime/aws-smithy-checksums/Cargo.toml +++ b/rust-runtime/aws-smithy-checksums/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-checksums" -version = "0.60.12" +version = "0.60.13" authors = [ "AWS Rust SDK Team ", "Zelda Hessler ",