aws_sdk_dsql/
auth_token.rs1use 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#[derive(Debug)]
45pub struct AuthTokenGenerator {
46 config: Config,
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct AuthToken {
55 inner: String,
56}
57
58impl AuthToken {
59 #[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 pub fn new(config: Config) -> Self {
75 Self { config }
76 }
77
78 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 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#[derive(Debug, Clone)]
129pub struct Config {
130 credentials: Option<SharedCredentialsProvider>,
134
135 hostname: String,
137
138 region: Option<Region>,
140
141 expires_in: Option<u64>,
143}
144
145impl Config {
146 pub fn builder() -> ConfigBuilder {
148 ConfigBuilder::default()
149 }
150
151 pub fn credentials(&self) -> Option<SharedCredentialsProvider> {
153 self.credentials.clone()
154 }
155
156 pub fn hostname(&self) -> &str {
158 &self.hostname
159 }
160
161 pub fn region(&self) -> Option<&Region> {
163 self.region.as_ref()
164 }
165
166 pub fn expires_in(&self) -> Option<u64> {
168 self.expires_in
169 }
170}
171
172#[derive(Debug, Default)]
174pub struct ConfigBuilder {
175 credentials: Option<SharedCredentialsProvider>,
179
180 hostname: Option<String>,
182
183 region: Option<Region>,
185
186 expires_in: Option<u64>,
188}
189
190impl ConfigBuilder {
191 pub fn credentials(mut self, credentials: impl ProvideCredentials + 'static) -> Self {
195 self.credentials = Some(SharedCredentialsProvider::new(credentials));
196 self
197 }
198
199 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
201 self.hostname = Some(hostname.into());
202 self
203 }
204
205 pub fn region(mut self, region: Region) -> Self {
207 self.region = Some(region);
208 self
209 }
210
211 pub fn expires_in(mut self, expires_in: u64) -> Self {
215 self.expires_in = Some(expires_in);
216 self
217 }
218
219 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}