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