aws_sdk_rds/
auth_token.rs

1// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
2/*
3 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7//! Code related to creating signed URLs for logging in to RDS.
8//!
9//! For more information, see <https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.html>
10
11use aws_credential_types::provider::{ProvideCredentials, SharedCredentialsProvider};
12use aws_sigv4::http_request;
13use aws_sigv4::http_request::{SignableBody, SignableRequest, SigningSettings};
14use aws_sigv4::sign::v4;
15use aws_smithy_runtime_api::box_error::BoxError;
16use aws_smithy_runtime_api::client::identity::Identity;
17use aws_types::region::Region;
18use std::fmt;
19use std::fmt::Debug;
20use std::time::Duration;
21
22const ACTION: &str = "connect";
23const SERVICE: &str = "rds-db";
24
25/// A signer that generates an auth token for a database.
26///
27/// ## Example
28///
29/// ```ignore
30/// use crate::auth_token::{AuthTokenGenerator, Config};
31///
32/// #[tokio::main]
33/// async fn main() {
34///    let cfg = aws_config::load_defaults(BehaviorVersion::latest()).await;
35///    let generator = AuthTokenGenerator::new(
36///        Config::builder()
37///            .hostname("zhessler-test-db.cp7a4mblr2ig.us-east-1.rds.amazonaws.com")
38///            .port(5432)
39///            .username("zhessler")
40///            .build()
41///            .expect("cfg is valid"),
42///    );
43///    let token = generator.auth_token(&cfg).await.unwrap();
44///    println!("{token}");
45/// }
46/// ```
47#[derive(Debug)]
48pub struct AuthTokenGenerator {
49    config: Config,
50}
51
52/// An auth token usable as a password for an RDS database.
53///
54/// This struct can be converted into a `&str` using the `Deref` trait or by calling `to_string()`.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct AuthToken {
57    inner: String,
58}
59
60impl AuthToken {
61    /// Return the auth token as a `&str`.
62    #[must_use]
63    pub fn as_str(&self) -> &str {
64        &self.inner
65    }
66}
67
68impl fmt::Display for AuthToken {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        write!(f, "{}", self.inner)
71    }
72}
73
74impl AuthTokenGenerator {
75    /// Given a `Config`, create a new RDS database login URL signer.
76    pub fn new(config: Config) -> Self {
77        Self { config }
78    }
79
80    /// Return a signed URL usable as an auth token.
81    pub async fn auth_token(&self, config: &aws_types::sdk_config::SdkConfig) -> Result<AuthToken, BoxError> {
82        let credentials = self
83            .config
84            .credentials()
85            .or(config.credentials_provider())
86            .ok_or("credentials are required to create a signed URL for RDS")?
87            .provide_credentials()
88            .await?;
89        let identity: Identity = credentials.into();
90        let region = self
91            .config
92            .region()
93            .or(config.region())
94            .cloned()
95            .unwrap_or_else(|| Region::new("us-east-1"));
96        let time = config.time_source().ok_or("a time source is required")?;
97
98        let mut signing_settings = SigningSettings::default();
99        signing_settings.expires_in = Some(Duration::from_secs(self.config.expires_in().unwrap_or(900).min(900)));
100        signing_settings.signature_location = http_request::SignatureLocation::QueryParams;
101
102        let signing_params = v4::SigningParams::builder()
103            .identity(&identity)
104            .region(region.as_ref())
105            .name(SERVICE)
106            .time(time.now())
107            .settings(signing_settings)
108            .build()?;
109
110        let url = format!(
111            "https://{}:{}/?Action={}&DBUser={}",
112            self.config.hostname(),
113            self.config.port(),
114            ACTION,
115            self.config.username()
116        );
117        let signable_request = SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::empty()).expect("signable request");
118
119        let (signing_instructions, _signature) = http_request::sign(signable_request, &signing_params.into())?.into_parts();
120
121        let mut url = url::Url::parse(&url).unwrap();
122        for (name, value) in signing_instructions.params() {
123            url.query_pairs_mut().append_pair(name, value);
124        }
125        let inner = url.to_string().split_off("https://".len());
126
127        Ok(AuthToken { inner })
128    }
129}
130
131/// Configuration for an RDS auth URL signer.
132#[derive(Debug, Clone)]
133pub struct Config {
134    /// The AWS credentials to sign requests with.
135    ///
136    /// Uses the default credential provider chain if not specified.
137    credentials: Option<SharedCredentialsProvider>,
138
139    /// The hostname of the database to connect to.
140    hostname: String,
141
142    /// The port number the database is listening on.
143    port: u64,
144
145    /// The region the database is located in. Uses the region inferred from the runtime if omitted.
146    region: Option<Region>,
147
148    /// The username to login as.
149    username: String,
150
151    /// The number of seconds the signed URL should be valid for.
152    ///
153    /// Maxes at 900 seconds.
154    expires_in: Option<u64>,
155}
156
157impl Config {
158    /// Create a new `SignerConfigBuilder`.
159    pub fn builder() -> ConfigBuilder {
160        ConfigBuilder::default()
161    }
162
163    /// The AWS credentials to sign requests with.
164    pub fn credentials(&self) -> Option<SharedCredentialsProvider> {
165        self.credentials.clone()
166    }
167
168    /// The hostname of the database to connect to.
169    pub fn hostname(&self) -> &str {
170        &self.hostname
171    }
172
173    /// The port number the database is listening on.
174    pub fn port(&self) -> u64 {
175        self.port
176    }
177
178    /// The region to sign requests with.
179    pub fn region(&self) -> Option<&Region> {
180        self.region.as_ref()
181    }
182
183    /// The DB username to login as.
184    pub fn username(&self) -> &str {
185        &self.username
186    }
187
188    /// The number of seconds the signed URL should be valid for.
189    ///
190    /// Maxes out at 900 seconds.
191    pub fn expires_in(&self) -> Option<u64> {
192        self.expires_in
193    }
194}
195
196/// A builder for [`Config`]s.
197#[derive(Debug, Default)]
198pub struct ConfigBuilder {
199    /// The AWS credentials to create the auth token with.
200    ///
201    /// Uses the default credential provider chain if not specified.
202    credentials: Option<SharedCredentialsProvider>,
203
204    /// The hostname of the database to connect to.
205    hostname: Option<String>,
206
207    /// The port number the database is listening on.
208    port: Option<u64>,
209
210    /// The region the database is located in. Uses the region inferred from the runtime if omitted.
211    region: Option<Region>,
212
213    /// The database username to login as.
214    username: Option<String>,
215
216    /// The number of seconds the auth token should be valid for.
217    expires_in: Option<u64>,
218}
219
220impl ConfigBuilder {
221    /// The AWS credentials to create the auth token with.
222    ///
223    /// Uses the default credential provider chain if not specified.
224    pub fn credentials(mut self, credentials: impl ProvideCredentials + 'static) -> Self {
225        self.credentials = Some(SharedCredentialsProvider::new(credentials));
226        self
227    }
228
229    /// The hostname of the database to connect to.
230    pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
231        self.hostname = Some(hostname.into());
232        self
233    }
234
235    /// The port number the database is listening on.
236    pub fn port(mut self, port: u64) -> Self {
237        self.port = Some(port);
238        self
239    }
240
241    /// The region the database is located in. Uses the region inferred from the runtime if omitted.
242    pub fn region(mut self, region: Region) -> Self {
243        self.region = Some(region);
244        self
245    }
246
247    /// The database username to login as.
248    pub fn username(mut self, username: impl Into<String>) -> Self {
249        self.username = Some(username.into());
250        self
251    }
252
253    /// The number of seconds the signed URL should be valid for.
254    ///
255    /// Maxes out at 900 seconds.
256    pub fn expires_in(mut self, expires_in: u64) -> Self {
257        self.expires_in = Some(expires_in);
258        self
259    }
260
261    /// Consume this builder, returning an error if required fields are missing.
262    /// Otherwise, return a new `SignerConfig`.
263    pub fn build(self) -> Result<Config, BoxError> {
264        Ok(Config {
265            credentials: self.credentials,
266            hostname: self.hostname.ok_or("A hostname is required")?,
267            port: self.port.ok_or("a port is required")?,
268            region: self.region,
269            username: self.username.ok_or("a username is required")?,
270            expires_in: self.expires_in,
271        })
272    }
273}
274
275#[cfg(test)]
276mod test {
277    use super::{AuthTokenGenerator, Config};
278    use aws_credential_types::provider::SharedCredentialsProvider;
279    use aws_credential_types::Credentials;
280    use aws_smithy_async::test_util::ManualTimeSource;
281    use aws_types::region::Region;
282    use aws_types::SdkConfig;
283    use std::time::{Duration, UNIX_EPOCH};
284
285    #[tokio::test]
286    async fn signing_works() {
287        let time_source = ManualTimeSource::new(UNIX_EPOCH + Duration::from_secs(1724709600));
288        let sdk_config = SdkConfig::builder()
289            .credentials_provider(SharedCredentialsProvider::new(Credentials::new("akid", "secret", None, None, "test")))
290            .time_source(time_source)
291            .build();
292        let signer = AuthTokenGenerator::new(
293            Config::builder()
294                .hostname("prod-instance.us-east-1.rds.amazonaws.com")
295                .port(3306)
296                .region(Region::new("us-east-1"))
297                .username("peccy")
298                .build()
299                .unwrap(),
300        );
301
302        let signed_url = signer.auth_token(&sdk_config).await.unwrap();
303        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");
304    }
305}