Skip to content

Commit 9f15b5b

Browse files
committed
apiclient: separate HTTP and HTTPS resolvers behind TLS feature flag
Split HttpUri into HttpUri (http://) and HttpsUri (https://), with HttpsUri gated behind the tls feature flag alongside other TLS-dependent resolvers (S3, SSM, Secrets Manager). Signed-off-by: Kyle Sessions <kssessio@amazon.com>
1 parent 215adad commit 9f15b5b

File tree

5 files changed

+240
-65
lines changed

5 files changed

+240
-65
lines changed

sources/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ aws-lc-rs = "=1.13.0"
121121
aws-sdk-cloudformation = "1"
122122
aws-sdk-ec2 = "1"
123123
aws-sdk-eks = "1"
124-
# Pin S3 <=1.85.0 and checksums <=0.63.1 to avoid crc-fast (no LTO support until v2.0)
124+
# Pin aws-sdk-s3 <=1.85.0 and aws-smithy-checksums <=0.63.1 to avoid crc-fast (no LTO support until v2.0)
125125
aws-sdk-s3 = "=1.85.0"
126126
aws-sdk-secretsmanager = "1"
127127
aws-sdk-ssm = "1"

sources/api/apiclient/src/apply.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ where
7171
{
7272
let settings = SettingsInput::new(input_source.as_ref());
7373
let resolver = select_resolver(&settings).context(error::ResolverFailureSnafu)?;
74-
resolver.resolve().await.context(error::ResolverFailureSnafu)
74+
resolver
75+
.resolve()
76+
.await
77+
.context(error::ResolverFailureSnafu)
7578
}
7679

7780
/// Takes a string of TOML or JSON settings data and reserializes
@@ -211,7 +214,7 @@ mod resolver_selection_tests {
211214
#[test_case("base64:SGVsbG8=", TypeId::of::<crate::uri_resolver::Base64Uri>(); "base64")]
212215
#[test_case("file:///tmp/folder", TypeId::of::<crate::uri_resolver::FileUri>(); "file")]
213216
#[test_case("http://amazon.com", TypeId::of::<crate::uri_resolver::HttpUri>(); "http")]
214-
#[test_case("https://amazon.com", TypeId::of::<crate::uri_resolver::HttpUri>(); "https")]
217+
#[test_case("https://amazon.com", TypeId::of::<crate::tls_resolvers::HttpsUri>(); "https")]
215218
#[test_case("s3://mybucket/path", TypeId::of::<crate::uri_resolver::S3Uri>(); "s3")]
216219
#[test_case("secretsmanager://sec", TypeId::of::<crate::uri_resolver::SecretsManagerUri>(); "secrets")]
217220
#[test_case("ssm://param", TypeId::of::<crate::uri_resolver::SsmUri>(); "ssmUri")]

sources/api/apiclient/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ pub mod network;
2828
pub mod reboot;
2929
pub mod report;
3030
pub mod set;
31-
pub mod update;
3231
#[cfg(feature = "tls")]
33-
pub mod cloud_resolvers;
32+
pub mod tls_resolvers;
33+
pub mod update;
3434
pub mod uri_resolver;
3535

3636
mod error {

sources/api/apiclient/src/cloud_resolvers.rs renamed to sources/api/apiclient/src/tls_resolvers.rs

Lines changed: 148 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
//! AWS cloud-based URI resolvers (S3, Secrets Manager, SSM Parameter Store).
1+
//! TLS-dependent URI resolvers (HTTPS, S3, Secrets Manager, SSM Parameter Store).
22
//!
33
//! This module is only compiled when the `tls` feature is enabled.
44
55
use crate::uri_resolver::SettingsInput;
66
use crate::uri_resolver::{ResolverResult, UriResolver, MAX_SIZE_BYTES};
77
use async_trait::async_trait;
8+
use reqwest::Url;
89
use snafu::{ensure, OptionExt, ResultExt, Snafu};
910
use std::convert::TryFrom;
1011

@@ -13,6 +14,68 @@ use aws_sdk_s3;
1314
use aws_sdk_secretsmanager;
1415
use aws_sdk_ssm;
1516

17+
pub struct HttpsUri {
18+
url: Url,
19+
}
20+
21+
impl TryFrom<&SettingsInput> for HttpsUri {
22+
type Error = crate::uri_resolver::ResolverError;
23+
fn try_from(input: &SettingsInput) -> ResolverResult<Self> {
24+
use tls_resolver_error::*;
25+
let url = input.parsed_url.clone().context(InvalidHttpsUriSnafu {
26+
input_source: input.input.clone(),
27+
})?;
28+
ensure!(
29+
url.scheme() == "https",
30+
InvalidHttpsUriSnafu {
31+
input_source: url.to_string()
32+
}
33+
);
34+
Ok(HttpsUri { url })
35+
}
36+
}
37+
38+
#[async_trait]
39+
impl UriResolver for HttpsUri {
40+
async fn resolve(&self) -> ResolverResult<String> {
41+
use crate::uri_resolver::resolver_error::*;
42+
let uri_str = self.url.to_string();
43+
let resp = reqwest::get(self.url.clone())
44+
.await
45+
.context(HttpRequestSnafu { uri: &uri_str })?;
46+
ensure!(
47+
resp.status().is_success(),
48+
HttpStatusSnafu {
49+
uri: &uri_str,
50+
status: resp.status(),
51+
}
52+
);
53+
if let Some(content_length) = resp.content_length() {
54+
ensure!(
55+
content_length < MAX_SIZE_BYTES,
56+
HttpObjectTooLargeSnafu {
57+
size: content_length,
58+
max_size: MAX_SIZE_BYTES,
59+
uri: &uri_str,
60+
}
61+
);
62+
}
63+
let bytes = resp
64+
.bytes()
65+
.await
66+
.context(HttpBodySnafu { uri: &uri_str })?;
67+
ensure!(
68+
bytes.len() as u64 <= MAX_SIZE_BYTES,
69+
HttpObjectTooLargeSnafu {
70+
size: bytes.len() as u64,
71+
max_size: MAX_SIZE_BYTES,
72+
uri: &uri_str,
73+
}
74+
);
75+
String::from_utf8(bytes.to_vec()).context(Utf8DecodeSnafu { uri: uri_str })
76+
}
77+
}
78+
1679
struct Arn {
1780
service: String,
1881
region: String,
@@ -53,7 +116,7 @@ pub struct S3Uri {
53116
impl TryFrom<&SettingsInput> for S3Uri {
54117
type Error = crate::uri_resolver::ResolverError;
55118
fn try_from(input: &SettingsInput) -> ResolverResult<Self> {
56-
use cloud_error::*;
119+
use tls_resolver_error::*;
57120
const PREFIX: &str = "s3://";
58121
let uri_str = input.input.as_str();
59122
let remainder = uri_str.strip_prefix(PREFIX).context(S3UriSchemeSnafu {
@@ -76,7 +139,7 @@ impl TryFrom<&SettingsInput> for S3Uri {
76139
#[async_trait]
77140
impl UriResolver for S3Uri {
78141
async fn resolve(&self) -> ResolverResult<String> {
79-
use cloud_error::*;
142+
use tls_resolver_error::*;
80143
let cfg = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
81144
let client = aws_sdk_s3::Client::new(&cfg);
82145

@@ -91,10 +154,12 @@ impl UriResolver for S3Uri {
91154
key: self.key.clone(),
92155
})?;
93156

94-
let size = head_resp.content_length.context(S3MissingContentLengthSnafu {
95-
bucket: self.bucket.clone(),
96-
key: self.key.clone(),
97-
})? as u64;
157+
let size = head_resp
158+
.content_length
159+
.context(S3MissingContentLengthSnafu {
160+
bucket: self.bucket.clone(),
161+
key: self.key.clone(),
162+
})? as u64;
98163

99164
ensure!(
100165
size < MAX_SIZE_BYTES,
@@ -122,9 +187,11 @@ impl UriResolver for S3Uri {
122187
key: self.key.clone(),
123188
})?;
124189

125-
String::from_utf8(bytes.to_vec()).context(crate::uri_resolver::resolver_error::Utf8DecodeSnafu {
126-
uri: format!("s3://{}/{}", self.bucket, self.key),
127-
})
190+
String::from_utf8(bytes.to_vec()).context(
191+
crate::uri_resolver::resolver_error::Utf8DecodeSnafu {
192+
uri: format!("s3://{}/{}", self.bucket, self.key),
193+
},
194+
)
128195
}
129196
}
130197

@@ -137,7 +204,7 @@ impl TryFrom<&SettingsInput> for SecretsManagerArn {
137204
type Error = crate::uri_resolver::ResolverError;
138205
fn try_from(input: &SettingsInput) -> ResolverResult<Self> {
139206
use crate::uri_resolver::resolver_error::InvalidArnFormatSnafu;
140-
use cloud_error::SecretsManagerArnSnafu;
207+
use tls_resolver_error::SecretsManagerArnSnafu;
141208
let arn = Arn::parse(input.input.as_str())?;
142209
ensure!(
143210
arn.parts == 7,
@@ -162,9 +229,11 @@ impl TryFrom<&SettingsInput> for SecretsManagerArn {
162229
#[async_trait]
163230
impl UriResolver for SecretsManagerArn {
164231
async fn resolve(&self) -> ResolverResult<String> {
165-
use cloud_error::*;
232+
use tls_resolver_error::*;
166233
let cfg = aws_config::defaults(aws_config::BehaviorVersion::latest())
167-
.region(aws_sdk_secretsmanager::config::Region::new(self.region.clone()))
234+
.region(aws_sdk_secretsmanager::config::Region::new(
235+
self.region.clone(),
236+
))
168237
.load()
169238
.await;
170239
let client = aws_sdk_secretsmanager::Client::new(&cfg);
@@ -196,12 +265,14 @@ pub struct SecretsManagerUri {
196265
impl TryFrom<&SettingsInput> for SecretsManagerUri {
197266
type Error = crate::uri_resolver::ResolverError;
198267
fn try_from(input: &SettingsInput) -> ResolverResult<Self> {
199-
use cloud_error::*;
268+
use tls_resolver_error::*;
200269
const PREFIX: &str = "secretsmanager://";
201270
let uri_str = input.input.as_str();
202-
let remainder = uri_str.strip_prefix(PREFIX).context(SecretsManagerUriSnafu {
203-
input_source: input.input.clone(),
204-
})?;
271+
let remainder = uri_str
272+
.strip_prefix(PREFIX)
273+
.context(SecretsManagerUriSnafu {
274+
input_source: input.input.clone(),
275+
})?;
205276
ensure!(
206277
!remainder.is_empty(),
207278
SecretsManagerUriSnafu {
@@ -217,7 +288,7 @@ impl TryFrom<&SettingsInput> for SecretsManagerUri {
217288
#[async_trait]
218289
impl UriResolver for SecretsManagerUri {
219290
async fn resolve(&self) -> ResolverResult<String> {
220-
use cloud_error::*;
291+
use tls_resolver_error::*;
221292
let cfg = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
222293
let client = aws_sdk_secretsmanager::Client::new(&cfg);
223294
let resp = client
@@ -246,7 +317,7 @@ impl TryFrom<&SettingsInput> for SsmArn {
246317
type Error = crate::uri_resolver::ResolverError;
247318
fn try_from(input: &SettingsInput) -> ResolverResult<Self> {
248319
use crate::uri_resolver::resolver_error::InvalidArnFormatSnafu;
249-
use cloud_error::SsmArnSnafu;
320+
use tls_resolver_error::SsmArnSnafu;
250321
let arn = Arn::parse(input.input.as_str())?;
251322
ensure!(
252323
arn.parts == 6,
@@ -271,7 +342,7 @@ impl TryFrom<&SettingsInput> for SsmArn {
271342
#[async_trait]
272343
impl UriResolver for SsmArn {
273344
async fn resolve(&self) -> ResolverResult<String> {
274-
use cloud_error::*;
345+
use tls_resolver_error::*;
275346
let cfg = aws_config::defaults(aws_config::BehaviorVersion::latest())
276347
.region(aws_sdk_ssm::config::Region::new(self.region.clone()))
277348
.load()
@@ -307,7 +378,7 @@ pub struct SsmUri {
307378
impl TryFrom<&SettingsInput> for SsmUri {
308379
type Error = crate::uri_resolver::ResolverError;
309380
fn try_from(input: &SettingsInput) -> ResolverResult<Self> {
310-
use cloud_error::*;
381+
use tls_resolver_error::*;
311382
const PREFIX: &str = "ssm://";
312383
let uri_str = input.input.as_str();
313384
let remainder = uri_str.strip_prefix(PREFIX).context(SsmUriSnafu {
@@ -328,7 +399,7 @@ impl TryFrom<&SettingsInput> for SsmUri {
328399
#[async_trait]
329400
impl UriResolver for SsmUri {
330401
async fn resolve(&self) -> ResolverResult<String> {
331-
use cloud_error::*;
402+
use tls_resolver_error::*;
332403
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
333404
let client = aws_sdk_ssm::Client::new(&config);
334405
let resp = client
@@ -352,7 +423,10 @@ impl UriResolver for SsmUri {
352423

353424
#[derive(Debug, Snafu)]
354425
#[snafu(module)]
355-
pub enum CloudError {
426+
pub enum TlsResolverError {
427+
#[snafu(display("Given invalid https:// URI '{}'", input_source))]
428+
InvalidHttpsUri { input_source: String },
429+
356430
#[snafu(display("Failed to HEAD S3 object s3://{bucket}/{key}: {source}"))]
357431
S3Head {
358432
source: aws_sdk_s3::error::SdkError<
@@ -380,7 +454,11 @@ pub enum CloudError {
380454
source: aws_sdk_s3::primitives::ByteStreamError,
381455
},
382456

383-
#[snafu(display("Failed to fetch secret '{}' from Secrets Manager: {}", secret_id, source))]
457+
#[snafu(display(
458+
"Failed to fetch secret '{}' from Secrets Manager: {}",
459+
secret_id,
460+
source
461+
))]
384462
SecretsManagerGet {
385463
secret_id: String,
386464
source: aws_sdk_secretsmanager::error::SdkError<
@@ -391,11 +469,19 @@ pub enum CloudError {
391469
#[snafu(display("Failed to fetch parameter '{}' from SSM: {}", parameter_name, source))]
392470
SsmGetParameter {
393471
parameter_name: String,
394-
source: aws_sdk_ssm::error::SdkError<aws_sdk_ssm::operation::get_parameter::GetParameterError>,
472+
source:
473+
aws_sdk_ssm::error::SdkError<aws_sdk_ssm::operation::get_parameter::GetParameterError>,
395474
},
396475

397-
#[snafu(display("S3 object s3://{bucket}/{key} is too large ({size} bytes, maximum is {max_size} bytes)"))]
398-
S3ObjectTooLarge { size: u64, max_size: u64, bucket: String, key: String },
476+
#[snafu(display(
477+
"S3 object s3://{bucket}/{key} is too large ({size} bytes, maximum is {max_size} bytes)"
478+
))]
479+
S3ObjectTooLarge {
480+
size: u64,
481+
max_size: u64,
482+
bucket: String,
483+
key: String,
484+
},
399485

400486
#[snafu(display("Invalid S3 URI scheme for '{}', expected s3://", input_source))]
401487
S3UriScheme { input_source: String },
@@ -409,16 +495,25 @@ pub enum CloudError {
409495
#[snafu(display("No Content-Length for S3 object {bucket}/{key}"))]
410496
S3MissingContentLength { bucket: String, key: String },
411497

412-
#[snafu(display("Invalid Secrets Manager URI scheme for '{}', expected secretsmanager://", input_source))]
498+
#[snafu(display(
499+
"Invalid Secrets Manager URI scheme for '{}', expected secretsmanager://",
500+
input_source
501+
))]
413502
SecretsManagerUri { input_source: String },
414503

415504
#[snafu(display("Secrets Manager secret '{}' did not return a string value", secret_id))]
416505
SecretsManagerStringMissing { secret_id: String },
417506

418-
#[snafu(display("Invalid Secrets Manager ARN scheme for '{}', expected arn:aws:secretsmanager:…", input_source))]
507+
#[snafu(display(
508+
"Invalid Secrets Manager ARN scheme for '{}', expected arn:aws:secretsmanager:…",
509+
input_source
510+
))]
419511
SecretsManagerArn { input_source: String },
420512

421-
#[snafu(display("Invalid SSM ARN scheme for '{}', expected arn:aws:ssm:…", input_source))]
513+
#[snafu(display(
514+
"Invalid SSM ARN scheme for '{}', expected arn:aws:ssm:…",
515+
input_source
516+
))]
422517
SsmArn { input_source: String },
423518

424519
#[snafu(display("SSM ARN parameter '{}' did not return a string value", parameter_name))]
@@ -430,3 +525,26 @@ pub enum CloudError {
430525
#[snafu(display("SSM parameter '{}' did not return a string value", parameter_name))]
431526
SsmParameterMissing { parameter_name: String },
432527
}
528+
529+
#[cfg(test)]
530+
mod tests {
531+
use super::*;
532+
use test_case::test_case;
533+
534+
#[test_case("https://example.com/foo", "https://example.com/foo"; "https_ok")]
535+
fn parse_https(input: &str, expected: &str) {
536+
let settings = SettingsInput::new(input);
537+
let uri = HttpsUri::try_from(&settings).expect("should parse HTTPS URI");
538+
assert_eq!(uri.url.as_str(), expected);
539+
}
540+
541+
#[test_case("http://example.com"; "http_rejected")]
542+
#[test_case("ftp://example.com"; "unsupported_scheme")]
543+
fn parse_https_fail(input: &str) {
544+
let settings = SettingsInput::new(input);
545+
assert!(
546+
HttpsUri::try_from(&settings).is_err(),
547+
"should reject non-HTTPS URI"
548+
);
549+
}
550+
}

0 commit comments

Comments
 (0)