1use axum::body::Body;
2use axum::extract::{ConnectInfo, Extension, Query};
3use axum::http::{Request, StatusCode};
4use axum::response::Response;
5use bytes::Bytes;
6use std::collections::HashMap;
7use std::net::SocketAddr;
8use std::sync::Arc;
9
10use crate::auth::{
11 is_root_bypass, ConditionContext, CredentialResolver, IamMode, IamPolicyEvaluator,
12 InternalCaller, Principal, PrincipalType, ResourcePolicyProvider,
13};
14use crate::protocol::{self, AwsProtocol};
15use crate::registry::ServiceRegistry;
16use crate::service::{AwsRequest, ResponseBody};
17
18#[derive(Clone, Copy, Debug)]
21struct PinnedService(&'static str);
22
23pub async fn dispatch_to_service(
35 service: &'static str,
36 registry: Arc<ServiceRegistry>,
37 config: Arc<DispatchConfig>,
38 mut request: Request<Body>,
39) -> Response<Body> {
40 let remote_addr = request
41 .extensions()
42 .get::<ConnectInfo<SocketAddr>>()
43 .map(|c| c.0)
44 .unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 0)));
45 let query = match Query::<HashMap<String, String>>::try_from_uri(request.uri()) {
46 Ok(q) => q,
47 Err(e) => {
48 return build_error_response(
49 StatusCode::BAD_REQUEST,
50 "InvalidArgument",
51 &format!("Invalid query string: {e}"),
52 &uuid::Uuid::new_v4().to_string(),
53 AwsProtocol::Rest,
54 )
55 }
56 };
57 request.extensions_mut().insert(PinnedService(service));
58 dispatch(
59 ConnectInfo(remote_addr),
60 Extension(registry),
61 Extension(config),
62 query,
63 request,
64 )
65 .await
66}
67
68const REQUEST_COMPRESSION_SERVICES: &[&str] = &["monitoring"];
73
74fn decode_request_compression(
77 headers: &http::HeaderMap,
78 rpc_v2_cbor: Option<&protocol::DetectedRequest>,
79 body: Bytes,
80) -> Result<Bytes, (String, AwsProtocol)> {
81 let gzipped = headers
82 .get_all(http::header::CONTENT_ENCODING)
83 .iter()
84 .filter_map(|v| v.to_str().ok())
85 .flat_map(|v| v.split(','))
86 .any(|enc| enc.trim().eq_ignore_ascii_case("gzip"));
87 if !gzipped || body.is_empty() {
88 return Ok(body);
89 }
90 let target = headers
91 .get("x-amz-target")
92 .and_then(|v| v.to_str().ok())
93 .and_then(protocol::parse_amz_target);
94 let (service, protocol) = if let Some(d) = rpc_v2_cbor {
95 (Some(d.service.clone()), AwsProtocol::RpcV2Cbor)
96 } else if let Some(d) = target {
97 (Some(d.service), AwsProtocol::Json)
98 } else {
99 (
100 protocol::extract_service_from_auth(headers),
101 AwsProtocol::Query,
102 )
103 };
104 if !service.is_some_and(|s| REQUEST_COMPRESSION_SERVICES.contains(&s.as_str())) {
105 return Ok(body);
106 }
107 use std::io::Read;
108 let limit = max_request_body_bytes() as u64;
109 let mut out = Vec::new();
110 let read = flate2::read::MultiGzDecoder::new(body.as_ref())
111 .take(limit + 1)
112 .read_to_end(&mut out);
113 match read {
114 Ok(_) if out.len() as u64 > limit => {
115 Err(("Decompressed request body too large".to_string(), protocol))
116 }
117 Ok(_) => Ok(Bytes::from(out)),
118 Err(e) => Err((
119 format!("Unable to decompress gzip request body: {e}"),
120 protocol,
121 )),
122 }
123}
124
125const AWS_JSON_1_0: &str = "application/x-amz-json-1.0";
127const AWS_JSON_1_1: &str = "application/x-amz-json-1.1";
129
130pub async fn dispatch(
132 ConnectInfo(remote_addr): ConnectInfo<SocketAddr>,
133 Extension(registry): Extension<Arc<ServiceRegistry>>,
134 Extension(config): Extension<Arc<DispatchConfig>>,
135 Query(query_params): Query<HashMap<String, String>>,
136 request: Request<Body>,
137) -> Response<Body> {
138 let json_1_0 = request
139 .headers()
140 .get(http::header::CONTENT_TYPE)
141 .and_then(|v| v.to_str().ok())
142 .is_some_and(|ct| ct.trim().eq_ignore_ascii_case(AWS_JSON_1_0));
143 let mut response = dispatch_inner(remote_addr, registry, config, query_params, request).await;
144 if json_1_0 {
145 answer_in_json_1_0(&mut response);
146 }
147 response
148}
149
150fn answer_in_json_1_0(response: &mut Response<Body>) {
155 let is_1_1 = response
156 .headers()
157 .get(http::header::CONTENT_TYPE)
158 .and_then(|v| v.to_str().ok())
159 .is_some_and(|ct| ct.eq_ignore_ascii_case(AWS_JSON_1_1));
160 if is_1_1 {
161 response.headers_mut().insert(
162 http::header::CONTENT_TYPE,
163 http::HeaderValue::from_static(AWS_JSON_1_0),
164 );
165 }
166}
167
168async fn dispatch_inner(
169 remote_addr: SocketAddr,
170 registry: Arc<ServiceRegistry>,
171 config: Arc<DispatchConfig>,
172 query_params: HashMap<String, String>,
173 request: Request<Body>,
174) -> Response<Body> {
175 let remote_addr = Some(remote_addr);
176 let request_id = uuid::Uuid::new_v4().to_string();
177
178 let (mut parts, body) = request.into_parts();
179 let pinned = parts.extensions.get::<PinnedService>().map(|p| p.0);
182
183 let rpc_v2_cbor = if pinned.is_none() {
193 protocol::detect_rpc_v2_cbor(&parts.headers, parts.uri.path())
194 } else {
195 None
196 };
197 let stream_route = streaming_route(
198 &parts.method,
199 parts.uri.path(),
200 &parts.headers,
201 &query_params,
202 );
203 let action_detection_params;
208 let detect_params = if pinned.is_none()
209 && query_params.contains_key("Action")
210 && unsigned_action_is_a_parameter(
211 &parts.method,
212 &parts.uri,
213 &parts.headers,
214 &query_params,
215 ®istry,
216 &config,
217 ) {
218 let mut params = query_params.clone();
219 params.remove("Action");
220 action_detection_params = params;
221 &action_detection_params
222 } else {
223 &query_params
224 };
225 let header_only = protocol::detect_service_headers_only(&parts.headers, detect_params);
226 let early_region = request_region(&parts.headers, &query_params, &config.region);
227 let early = EarlyRequest {
228 method: &parts.method,
229 wire_path: parts.uri.path(),
230 headers: &parts.headers,
231 query_params: &query_params,
232 anonymous: presents_no_credential(&parts.headers, &query_params)
233 && parts.extensions.get::<InternalCaller>().is_none(),
234 region: &early_region,
235 request_id: &request_id,
236 };
237 let unrouted = (pinned.is_none()
250 && rpc_v2_cbor.is_none()
251 && header_only.is_none()
252 && !parts.headers.contains_key("x-amz-target")
253 && !protocol::is_form_content_type(&parts.headers))
254 .then(|| {
255 unrouted_target(
256 &parts.method,
257 &parts.uri,
258 &parts.headers,
259 &query_params,
260 ®istry,
261 &config,
262 )
263 })
264 .flatten();
265 let unrouted_detected = unrouted.map(unrouted_detection);
266 let head_detected = header_only
267 .as_ref()
268 .filter(|_| {
269 pinned.is_none()
270 && rpc_v2_cbor.is_none()
271 && protocol::headers_name_final_service(&parts.headers, &query_params)
272 })
273 .or(unrouted_detected.as_ref());
274 if pinned.is_none()
278 && head_detected
279 .or(header_only.as_ref())
280 .is_some_and(|d| d.service == "s3")
281 {
282 if let Some((message, fields)) =
283 s3_sigv2_query_auth_error(&parts.headers, &query_params, chrono::Utc::now())
284 {
285 return build_error_response_with_fields(
286 StatusCode::FORBIDDEN,
287 "AccessDenied",
288 &message,
289 &request_id,
290 AwsProtocol::Rest,
291 &fields,
292 );
293 }
294 }
295 if let Some(detected) = head_detected {
296 if let Some(service) = registry.get(&detected.service) {
297 if let Err(err) = with_request_head(service.as_ref(), &early, |head| {
298 service.refuse_before_body(head)
299 }) {
300 return service_error_response(
301 &err,
302 detected,
303 &request_id,
304 ErrorEnvelope::for_request(detected, &parts.headers),
305 );
306 }
307 }
308 }
309 let stream_dispatch = match (&stream_route, &header_only) {
310 _ if pinned.is_some() => None,
313 _ if rpc_v2_cbor.is_some() => None,
314 (Some(sr), Some(detected))
317 if sr.0 == detected.service
318 && registry
319 .get(&detected.service)
320 .is_some_and(|service| service_streams_body(service.as_ref(), &early)) =>
321 {
322 Some(detected.clone())
323 }
324 (Some((service, _)), None) if *service == "ecr" => Some(protocol::DetectedRequest {
330 service: "ecr".to_string(),
331 action: String::new(),
332 protocol: AwsProtocol::Rest,
333 }),
334 (None, header_only)
338 if early.anonymous
339 && match header_only {
340 Some(detected) => detected.service == "s3",
341 None => unrouted == Some("s3"),
342 }
343 && registry
344 .get("s3")
345 .is_some_and(|s3| service_streams_body(s3.as_ref(), &early)) =>
346 {
347 Some(unrouted_detection("s3"))
348 }
349 _ => None,
350 };
351
352 let (body_bytes, body_stream) = if stream_dispatch.is_some() {
353 (Bytes::new(), Some(body))
354 } else {
355 let max_body_bytes = max_request_body_bytes();
360 match axum::body::to_bytes(body, max_body_bytes).await {
361 Ok(b) => (b, None),
362 Err(_) => {
363 return build_error_response(
364 StatusCode::PAYLOAD_TOO_LARGE,
365 "RequestEntityTooLarge",
366 "Request body too large",
367 &request_id,
368 AwsProtocol::Query,
369 );
370 }
371 }
372 };
373
374 let wire_body = body_bytes.clone();
379 let body_bytes = if pinned.is_none() && stream_dispatch.is_none() {
380 match decode_request_compression(&parts.headers, rpc_v2_cbor.as_ref(), body_bytes) {
381 Ok(b) => b,
382 Err((message, protocol)) => {
383 return build_error_response(
384 StatusCode::BAD_REQUEST,
385 "SerializationException",
386 &message,
387 &request_id,
388 protocol,
389 );
390 }
391 }
392 } else {
393 body_bytes
394 };
395
396 let detected = if let Some(service) = pinned {
398 protocol::DetectedRequest {
399 service: service.to_string(),
400 action: String::new(),
401 protocol: AwsProtocol::Rest,
402 }
403 } else if let Some(d) = rpc_v2_cbor {
404 d
405 } else if let Some(d) = stream_dispatch {
406 d
407 } else {
408 match protocol::detect_service(&parts.headers, detect_params, &body_bytes) {
409 Some(d) => d,
410 None => {
411 if let Some(target) = parts
417 .headers
418 .get("x-amz-target")
419 .and_then(|v| v.to_str().ok())
420 {
421 return build_error_response(
422 StatusCode::BAD_REQUEST,
423 "UnknownOperationException",
424 &format!("The operation {target} is not recognized."),
425 &request_id,
426 AwsProtocol::Json,
427 );
428 }
429 match unrouted.or_else(|| {
430 unrouted_target(
431 &parts.method,
432 &parts.uri,
433 &parts.headers,
434 &query_params,
435 ®istry,
436 &config,
437 )
438 }) {
439 Some(service) => unrouted_detection(service),
440 None => {
441 return build_error_response(
442 StatusCode::BAD_REQUEST,
443 "MissingAction",
444 "Could not determine target service or action from request",
445 &request_id,
446 AwsProtocol::Query,
447 );
448 }
449 }
450 }
451 }
452 };
453
454 let detected = if detected.service == "bedrock" {
458 match bedrock_agent_service_for(&parts.method, parts.uri.path()) {
459 Some(service) => protocol::DetectedRequest {
460 service: service.to_string(),
461 ..detected
462 },
463 None => detected,
464 }
465 } else {
466 detected
467 };
468
469 let detected = if detected.service == "rds" && user_agent_indicates_docdb(&parts.headers) {
478 protocol::DetectedRequest {
479 service: "docdb".to_string(),
480 ..detected
481 }
482 } else {
483 detected
484 };
485
486 let detected = if detected.service == "rds" && user_agent_indicates_neptune(&parts.headers) {
495 protocol::DetectedRequest {
496 service: "neptune".to_string(),
497 ..detected
498 }
499 } else {
500 detected
501 };
502
503 if detected.service == "s3" && pinned.is_none() {
507 if let Some((message, fields)) =
508 s3_sigv2_query_auth_error(&parts.headers, &query_params, chrono::Utc::now())
509 {
510 return build_error_response_with_fields(
511 StatusCode::FORBIDDEN,
512 "AccessDenied",
513 &message,
514 &request_id,
515 AwsProtocol::Rest,
516 &fields,
517 );
518 }
519 }
520
521 let service = match registry.get(&detected.service) {
523 Some(s) => s,
524 None => {
525 return build_error_response(
526 detected.protocol.error_status(),
527 "UnknownService",
528 &format!("Service '{}' is not available", detected.service),
529 &request_id,
530 ErrorEnvelope::for_request(&detected, &parts.headers),
531 );
532 }
533 };
534
535 let auth_header = parts
537 .headers
538 .get("authorization")
539 .and_then(|v| v.to_str().ok())
540 .unwrap_or("");
541 let header_info = fakecloud_aws::sigv4::parse_sigv4(auth_header);
542 let presigned_info = if header_info.is_none() {
543 fakecloud_aws::sigv4::parse_sigv4_presigned(&query_params).map(|p| p.as_info())
545 } else {
546 None
547 };
548 let sigv4_info = header_info.or(presigned_info);
549 let form_sigv2_key = (sigv4_info.is_none()
557 && matches!(
558 detected.protocol,
559 AwsProtocol::Query | AwsProtocol::Ec2Query
560 )
561 && protocol::is_form_content_type(&parts.headers))
562 .then(|| sigv2_query_access_key(&protocol::parse_query_body(&body_bytes)))
563 .flatten();
564 let access_key_id = sigv4_info
565 .as_ref()
566 .map(|info| info.access_key.clone())
567 .or_else(|| sigv2_presigned_access_key(&query_params))
568 .or_else(|| sigv2_query_access_key(&query_params))
569 .or_else(|| form_sigv2_key.clone());
570
571 let host_info = protocol::parse_routing_host_from_headers(&parts.headers);
577
578 let region = early_region;
579
580 let caller_akid = access_key_id.as_deref().unwrap_or("");
586 let resolved = if !caller_akid.is_empty() && !is_root_bypass(caller_akid) {
587 config
588 .credential_resolver
589 .as_ref()
590 .and_then(|r| r.resolve(caller_akid))
591 } else {
592 None
593 };
594 let caller_principal = resolved.as_ref().map(|r| r.principal.clone());
595 let caller_session_policies = resolved
596 .as_ref()
597 .map(|r| r.session_policies.clone())
598 .unwrap_or_default();
599
600 let is_fully_anonymous =
616 presents_no_credential(&parts.headers, &query_params) && form_sigv2_key.is_none();
617 if config.verify_sigv4
618 && !is_fully_anonymous
619 && !is_root_bypass(caller_akid)
620 && config.credential_resolver.is_some()
621 {
622 let amz_date = parts
623 .headers
624 .get("x-amz-date")
625 .and_then(|v| v.to_str().ok());
626 let parsed = fakecloud_aws::sigv4::parse_sigv4_header(auth_header, amz_date)
627 .or_else(|| fakecloud_aws::sigv4::parse_sigv4_presigned(&query_params));
628 let parsed = match parsed {
629 Some(p) => p,
630 None => {
631 return build_error_response(
632 StatusCode::FORBIDDEN,
633 "IncompleteSignature",
634 "Request is missing or has a malformed AWS Signature",
635 &request_id,
636 ErrorEnvelope::for_request(&detected, &parts.headers),
637 );
638 }
639 };
640 let resolved_for_verify = match resolved.as_ref() {
641 Some(r) => r,
642 None => {
643 return unresolved_credential_response(
644 &config,
645 caller_akid,
646 &request_id,
647 ErrorEnvelope::for_request(&detected, &parts.headers),
648 );
649 }
650 };
651 let headers_vec = fakecloud_aws::sigv4::headers_from_http(&parts.headers);
652 let raw_query_for_verify = parts.uri.query().unwrap_or("").to_string();
653 let verify_req = fakecloud_aws::sigv4::VerifyRequest {
654 method: parts.method.as_str(),
655 path: parts.uri.path(),
656 query: &raw_query_for_verify,
657 headers: &headers_vec,
658 body: &wire_body,
659 };
660 match fakecloud_aws::sigv4::verify(
661 &parsed,
662 &verify_req,
663 &resolved_for_verify.secret_access_key,
664 chrono::Utc::now(),
665 ) {
666 Ok(()) => {
667 if !parsed.is_presigned && detected.service != "s3" {
683 if let Some(signed_hash) = parts
684 .headers
685 .get("x-amz-content-sha256")
686 .and_then(|v| v.to_str().ok())
687 .filter(|h| is_hex_sha256(h))
688 {
689 if sha256_hex_lower(&wire_body) != signed_hash {
690 return build_error_response(
691 StatusCode::FORBIDDEN,
692 "SignatureDoesNotMatch",
693 "The request signature we calculated does not match the signature you provided",
694 &request_id,
695 ErrorEnvelope::for_request(&detected, &parts.headers),
696 );
697 }
698 }
699 }
700 }
701 Err(fakecloud_aws::sigv4::SigV4Error::RequestTimeTooSkewed { .. }) => {
702 return build_error_response(
703 StatusCode::FORBIDDEN,
704 "RequestTimeTooSkewed",
705 "The difference between the request time and the current time is too large",
706 &request_id,
707 ErrorEnvelope::for_request(&detected, &parts.headers),
708 );
709 }
710 Err(fakecloud_aws::sigv4::SigV4Error::InvalidDate(msg)) => {
711 return build_error_response(
712 StatusCode::FORBIDDEN,
713 "IncompleteSignature",
714 &format!("Invalid x-amz-date: {msg}"),
715 &request_id,
716 ErrorEnvelope::for_request(&detected, &parts.headers),
717 );
718 }
719 Err(fakecloud_aws::sigv4::SigV4Error::Malformed(msg)) => {
720 return build_error_response(
721 StatusCode::FORBIDDEN,
722 "IncompleteSignature",
723 &format!("Malformed SigV4 signature: {msg}"),
724 &request_id,
725 ErrorEnvelope::for_request(&detected, &parts.headers),
726 );
727 }
728 Err(fakecloud_aws::sigv4::SigV4Error::SignatureMismatch) => {
729 return build_error_response(
730 StatusCode::FORBIDDEN,
731 "SignatureDoesNotMatch",
732 "The request signature we calculated does not match the signature you provided",
733 &request_id,
734 ErrorEnvelope::for_request(&detected, &parts.headers),
735 );
736 }
737 Err(fakecloud_aws::sigv4::SigV4Error::PresignedUrlExpired {
738 signed,
739 server,
740 expires_secs,
741 }) => {
742 let fields = if detected.service == "s3" {
745 vec![
746 ("X-Amz-Expires".to_string(), expires_secs.to_string()),
747 (
748 "Expires".to_string(),
749 s3_error_time(signed + chrono::Duration::seconds(expires_secs)),
750 ),
751 ("ServerTime".to_string(), s3_error_time(server)),
752 ]
753 } else {
754 Vec::new()
755 };
756 return build_error_response_with_fields(
757 StatusCode::FORBIDDEN,
758 "AccessDenied",
759 "Request has expired",
760 &request_id,
761 ErrorEnvelope::for_request(&detected, &parts.headers),
762 &fields,
763 );
764 }
765 Err(fakecloud_aws::sigv4::SigV4Error::InvalidPresignExpires(_)) => {
766 return build_error_response(
767 StatusCode::BAD_REQUEST,
768 "AuthorizationQueryParametersError",
769 "X-Amz-Expires must be a number between 1 and 604800 seconds",
770 &request_id,
771 ErrorEnvelope::for_request(&detected, &parts.headers),
772 );
773 }
774 }
775 }
776
777 if detected.service == "s3" && query_params.contains_key("X-Amz-Credential") {
782 hoist_presigned_query_headers(&mut parts.headers, &query_params);
783 }
784
785 let wire_path = parts.uri.path();
790 let path = if detected.service == "s3" {
791 s3_routing_path(
792 wire_path,
793 host_info.as_ref().and_then(|h| h.bucket.as_deref()),
794 )
795 } else {
796 wire_path.to_string()
797 };
798 let raw_query = parts.uri.query().unwrap_or("").to_string();
799 let path_segments = crate::path::split_path_segments(&path);
803
804 let body_bytes = if detected.protocol == AwsProtocol::RpcV2Cbor {
808 match crate::cbor::decode_to_json(&body_bytes) {
809 Ok(json) => Bytes::from(json.to_string()),
810 Err(e) => {
811 return build_error_response(
812 StatusCode::BAD_REQUEST,
813 "SerializationException",
814 &format!("Unable to decode CBOR request body: {e}"),
815 &request_id,
816 AwsProtocol::RpcV2Cbor,
817 );
818 }
819 }
820 } else {
821 body_bytes
822 };
823
824 if detected.protocol == AwsProtocol::Json
826 && !body_bytes.is_empty()
827 && serde_json::from_slice::<serde_json::Value>(&body_bytes).is_err()
828 {
829 return build_error_response(
830 StatusCode::BAD_REQUEST,
831 "SerializationException",
832 "Start of structure or map found where not expected",
833 &request_id,
834 AwsProtocol::Json,
835 );
836 }
837
838 let mut all_params = query_params;
841 if matches!(
842 detected.protocol,
843 AwsProtocol::Query | AwsProtocol::Ec2Query
844 ) {
845 let body_params = protocol::parse_query_body(&body_bytes);
846 for (k, v) in body_params {
847 all_params.entry(k).or_insert(v);
848 }
849 }
850
851 if matches!(
856 detected.protocol,
857 AwsProtocol::Json | AwsProtocol::RpcV2Cbor
858 ) && detected.service == "monitoring"
859 {
860 let body_params = protocol::flatten_json_to_query(&body_bytes);
861 for (k, v) in body_params {
862 all_params.entry(k).or_insert(v);
863 }
864 }
865
866 let internal_caller = if is_fully_anonymous && access_key_id.is_none() {
872 parts.extensions.get::<InternalCaller>().cloned()
873 } else {
874 None
875 };
876 let caller_principal =
877 caller_principal.or_else(|| internal_caller.as_ref().map(|c| c.principal()));
878
879 let error_envelope = ErrorEnvelope::for_request(&detected, &parts.headers);
880 let mut aws_request = AwsRequest {
881 service: detected.service.clone(),
882 action: detected.action.clone(),
883 region,
884 account_id: match internal_caller.as_ref() {
888 Some(caller) => {
889 internal_caller_account(caller, &detected.service, &path_segments, &config)
890 }
891 None => caller_principal
892 .as_ref()
893 .map(|p| p.account_id.clone())
894 .unwrap_or_else(|| config.account_id.clone()),
895 },
896 request_id: request_id.clone(),
897 headers: parts.headers,
898 query_params: all_params,
899 body: body_bytes,
900 body_stream: parking_lot::Mutex::new(body_stream),
901 path_segments,
902 raw_path: path,
903 raw_query,
904 method: parts.method,
905 is_query_protocol: matches!(
906 detected.protocol,
907 AwsProtocol::Query | AwsProtocol::Ec2Query
908 ),
909 access_key_id,
910 principal: caller_principal,
911 };
912
913 tracing::info!(
914 service = %aws_request.service,
915 action = %aws_request.action,
916 request_id = %aws_request.request_id,
917 "handling request"
918 );
919
920 if let Err(err) = service.prepare_request(&mut aws_request) {
923 return service_error_response(&err, &detected, &request_id, error_envelope);
924 }
925
926 let enforcement = IamEnforcement {
927 config: config.clone(),
928 service: service.clone(),
929 detected: detected.clone(),
930 request_id: request_id.clone(),
931 remote_addr,
932 resolved,
933 caller_session_policies,
934 internal_caller,
935 };
936 let anonymous = aws_request.access_key_id.is_none()
957 && aws_request.principal.is_none()
958 && enforcement.internal_caller.is_none();
959 let anonymous_access = (anonymous
964 && is_fully_anonymous
965 && config.iam_mode.is_enabled()
966 && names_api_operation(
967 &detected,
968 host_info.as_ref(),
969 &aws_request.method,
970 &aws_request.headers,
971 || service.is_control_plane_request(&aws_request),
972 ))
973 .then(|| service.anonymous_access(&aws_request));
974 if anonymous_access == Some(crate::anonymous::AnonymousAccess::Refused) {
975 tracing::warn!(
976 target: "fakecloud::iam::audit",
977 service = %detected.service,
978 action = %aws_request.action,
979 mode = %config.iam_mode,
980 request_id = %request_id,
981 "unsigned request is missing authentication; denying under strict, allowing under soft"
982 );
983 if config.iam_mode.is_strict() {
984 return missing_authentication_response(&detected, &request_id, &aws_request.headers);
985 }
986 }
987 let unauthenticated_operation =
988 anonymous_access == Some(crate::anonymous::AnonymousAccess::Unauthenticated);
989 let authorization_required =
990 !unauthenticated_operation && service.iam_authorization_required(&aws_request);
991 let authorizes = enforcement.is_active(&aws_request) && authorization_required;
992 if authorizes {
993 if let Some(denied) = enforcement.unauthenticated(&aws_request) {
994 return denied;
995 }
996 }
997 let refuse_anonymous_first =
998 authorizes && anonymous && !service.validates_anonymous_before_authorization();
999 if !refuse_anonymous_first {
1000 if let Err(err) = service.validate_before_authorization(&aws_request) {
1001 return service_error_response(&err, &detected, &request_id, error_envelope);
1002 }
1003 }
1004 if authorizes {
1005 let deferred = service.iam_authorized_by_handler(&aws_request);
1009 let iam_actions = if deferred {
1010 Vec::new()
1011 } else {
1012 service.iam_actions_for(&aws_request)
1013 };
1014 if let Some(denied) = enforcement.enforce(&aws_request, &iam_actions, deferred) {
1015 return denied;
1016 }
1017 }
1018 if refuse_anonymous_first {
1021 if let Err(err) = service.validate_before_authorization(&aws_request) {
1022 return service_error_response(&err, &detected, &request_id, error_envelope);
1023 }
1024 }
1025 let authorizer = enforcement.is_active(&aws_request).then(|| {
1028 Arc::new(DispatchAuthorizer {
1029 request: authorization_copy(&aws_request),
1030 enforcement,
1031 }) as Arc<dyn crate::auth::RequestAuthorizer>
1032 });
1033 let service = service.clone();
1034 finish_dispatch(
1035 service,
1036 aws_request,
1037 authorizer,
1038 detected,
1039 request_id,
1040 error_envelope,
1041 )
1042 .await
1043}
1044
1045#[derive(Clone)]
1049struct IamEnforcement {
1050 config: Arc<DispatchConfig>,
1051 service: Arc<dyn crate::service::AwsService>,
1052 detected: protocol::DetectedRequest,
1053 request_id: String,
1054 remote_addr: Option<SocketAddr>,
1055 resolved: Option<crate::auth::ResolvedCredential>,
1056 caller_session_policies: Vec<String>,
1057 internal_caller: Option<InternalCaller>,
1058}
1059
1060struct DispatchAuthorizer {
1065 enforcement: IamEnforcement,
1066 request: AwsRequest,
1067}
1068
1069impl crate::auth::RequestAuthorizer for DispatchAuthorizer {
1070 fn service_name(&self) -> &str {
1071 &self.enforcement.detected.service
1072 }
1073
1074 fn authorize(&self, action: &crate::auth::IamAction) -> bool {
1075 self.enforcement
1076 .enforce(&self.request, std::slice::from_ref(action), false)
1077 .is_none()
1078 }
1079
1080 fn authorize_for(&self, request: &AwsRequest, action: &crate::auth::IamAction) -> bool {
1081 let resolved = request
1084 .access_key_id
1085 .as_deref()
1086 .filter(|key| !is_root_bypass(key))
1087 .and_then(|key| {
1088 self.enforcement
1089 .config
1090 .credential_resolver
1091 .as_ref()
1092 .and_then(|r| r.resolve(key))
1093 });
1094 let mut request = authorization_copy(request);
1095 request.principal = resolved.as_ref().map(|r| r.principal.clone());
1096 let enforcement = IamEnforcement {
1097 caller_session_policies: resolved
1098 .as_ref()
1099 .map(|r| r.session_policies.clone())
1100 .unwrap_or_default(),
1101 resolved,
1102 internal_caller: None,
1103 ..self.enforcement.clone()
1104 };
1105 enforcement
1106 .enforce(&request, std::slice::from_ref(action), false)
1107 .is_none()
1108 }
1109}
1110
1111fn authorization_copy(request: &AwsRequest) -> AwsRequest {
1115 AwsRequest {
1116 service: request.service.clone(),
1117 action: request.action.clone(),
1118 region: request.region.clone(),
1119 account_id: request.account_id.clone(),
1120 request_id: request.request_id.clone(),
1121 headers: request.headers.clone(),
1122 query_params: request.query_params.clone(),
1123 body: request.body.clone(),
1124 body_stream: parking_lot::Mutex::new(None),
1125 path_segments: request.path_segments.clone(),
1126 raw_path: request.raw_path.clone(),
1127 raw_query: request.raw_query.clone(),
1128 method: request.method.clone(),
1129 is_query_protocol: request.is_query_protocol,
1130 access_key_id: request.access_key_id.clone(),
1131 principal: request.principal.clone(),
1132 }
1133}
1134
1135impl IamEnforcement {
1136 fn is_active(&self, aws_request: &AwsRequest) -> bool {
1140 self.config.iam_mode.is_enabled()
1141 && self.config.policy_evaluator.is_some()
1142 && self.service.iam_enforceable()
1143 && !is_root_bypass(aws_request.access_key_id.as_deref().unwrap_or(""))
1144 }
1145
1146 fn unauthenticated(&self, aws_request: &AwsRequest) -> Option<Response<Body>> {
1152 if !self.config.iam_mode.is_strict()
1153 || self.internal_caller.is_some()
1154 || aws_request.principal.is_some()
1155 {
1156 return None;
1157 }
1158 let akid = aws_request.access_key_id.as_deref()?;
1159 Some(self.unresolved_credential(aws_request, akid))
1160 }
1161
1162 fn unresolved_credential(&self, aws_request: &AwsRequest, akid: &str) -> Response<Body> {
1164 tracing::warn!(
1165 target: "fakecloud::iam::audit",
1166 service = %self.detected.service,
1167 action = %aws_request.action,
1168 mode = %self.config.iam_mode,
1169 request_id = %self.request_id,
1170 "request credential does not resolve to an identity; denying under strict, allowing under soft"
1171 );
1172 unresolved_credential_response(
1173 &self.config,
1174 akid,
1175 &self.request_id,
1176 ErrorEnvelope::for_request(&self.detected, &aws_request.headers),
1177 )
1178 }
1179
1180 fn enforce(
1185 &self,
1186 aws_request: &AwsRequest,
1187 iam_actions: &[crate::auth::IamAction],
1188 deferred: bool,
1189 ) -> Option<Response<Body>> {
1190 let Self {
1191 config,
1192 service,
1193 detected,
1194 request_id,
1195 resolved,
1196 caller_session_policies,
1197 internal_caller,
1198 ..
1199 } = self;
1200 let remote_addr = self.remote_addr;
1201 let error_envelope = ErrorEnvelope::for_request(detected, &aws_request.headers);
1202 if self.is_active(aws_request) {
1210 if let Some(evaluator) = config.policy_evaluator.as_ref() {
1211 if let Some(caller) = internal_caller.as_ref() {
1212 if let Some(denied) = authorize_internal_caller(
1213 caller,
1214 service.as_ref(),
1215 aws_request,
1216 evaluator.as_ref(),
1217 config,
1218 detected,
1219 request_id,
1220 iam_actions,
1221 deferred,
1222 ) {
1223 return Some(denied);
1224 }
1225 } else if let Some(principal) = aws_request.principal.as_ref() {
1226 if principal.is_root() {
1227 if let Some(denied) = authorize_root(
1228 principal,
1229 service.as_ref(),
1230 aws_request,
1231 evaluator.as_ref(),
1232 config,
1233 detected,
1234 request_id,
1235 remote_addr,
1236 resolved.as_ref(),
1237 iam_actions,
1238 deferred,
1239 ) {
1240 return Some(denied);
1241 }
1242 } else {
1243 if deferred || !iam_actions.is_empty() {
1246 for iam_action in iam_actions {
1247 let mut condition_context = principal_condition_context(
1248 principal,
1249 resolved.as_ref(),
1250 service.as_ref(),
1251 aws_request,
1252 iam_action,
1253 detected,
1254 remote_addr,
1255 );
1256 let service_resource = !iam_action.is_pass_role();
1257
1258 let in_caller_account =
1270 service.iam_resource_in_caller_account(aws_request);
1271 let resource_policy_json = config
1272 .resource_policy_provider
1273 .as_ref()
1274 .filter(|_| service_resource && !in_caller_account)
1275 .filter(|p| {
1276 p.resource_policy_applies(&detected.service, iam_action)
1277 })
1278 .and_then(|p| {
1279 p.resource_policy(&detected.service, &iam_action.resource)
1280 });
1281 let resource_account_id = config
1291 .resource_policy_provider
1292 .as_ref()
1293 .filter(|_| service_resource && !in_caller_account)
1294 .and_then(|p| {
1295 p.resource_owner_account(
1296 &detected.service,
1297 &iam_action.resource,
1298 )
1299 })
1300 .or_else(|| parse_account_from_arn(&iam_action.resource))
1301 .unwrap_or_else(|| principal.account_id.clone());
1302 let scps = config
1309 .scp_resolver
1310 .as_ref()
1311 .and_then(|r| r.scps_for(principal));
1312 add_global_request_keys(
1318 &mut condition_context,
1319 principal,
1320 &resource_account_id,
1321 config.scp_resolver.as_deref(),
1322 );
1323 let decision = evaluator.evaluate_with_resource_policy(
1324 principal,
1325 iam_action,
1326 &condition_context,
1327 resource_policy_json.as_deref(),
1328 &resource_account_id,
1329 caller_session_policies,
1330 scps.as_deref(),
1331 );
1332 let decision = if decision == crate::auth::IamDecision::ImplicitDeny
1337 && service_resource
1338 && !in_caller_account
1339 && grant_authorizes(
1340 config.resource_policy_provider.as_deref(),
1341 evaluator.as_ref(),
1342 &detected.service,
1343 aws_request,
1344 iam_action,
1345 principal,
1346 &condition_context,
1347 caller_session_policies,
1348 scps.as_deref(),
1349 ) {
1350 crate::auth::IamDecision::Allow
1351 } else {
1352 decision
1353 };
1354 if !decision.is_allow() {
1355 tracing::warn!(
1356 target: "fakecloud::iam::audit",
1357 service = %detected.service,
1358 action = %iam_action.action_string(),
1359 resource = %iam_action.resource,
1360 principal = %principal.arn,
1361 resource_policy_present = resource_policy_json.is_some(),
1362 decision = ?decision,
1363 mode = %config.iam_mode,
1364 request_id = %request_id,
1365 "IAM policy evaluation denied request"
1366 );
1367 if config.iam_mode.is_strict() {
1368 let context_summary = serde_json::json!({
1381 "aws:PrincipalArn": condition_context
1382 .aws_principal_arn
1383 .clone()
1384 .unwrap_or_else(|| principal.arn.clone()),
1385 "aws:PrincipalAccount": principal.account_id,
1386 "aws:RequestedRegion": condition_context
1387 .aws_requested_region
1388 .clone()
1389 .unwrap_or_default(),
1390 "aws:SecureTransport": condition_context
1391 .aws_secure_transport
1392 .unwrap_or(false),
1393 "aws:Action": iam_action.action_string(),
1394 "aws:Resource": iam_action.resource,
1395 "decision": format!("{:?}", decision),
1396 });
1397 let action_string = iam_action.action_string();
1398 let encoded = crate::auth_message::encode_deny(
1399 matches!(
1400 decision,
1401 crate::auth::IamDecision::ExplicitDeny
1402 ),
1403 Some(&action_string),
1404 Some(&principal.arn),
1405 Vec::new(),
1406 Some(context_summary),
1407 );
1408 let scp_reason = scps.as_deref().and_then(|scps| {
1411 let ceiling = evaluator.evaluate_ceiling(
1412 principal,
1413 iam_action,
1414 &condition_context,
1415 &[],
1416 Some(scps),
1417 false,
1418 );
1419 scp_cause(decision, ceiling)
1420 .and_then(|d| scp_denial_reason(d, &action_string))
1421 });
1422 return Some(access_denied_response(
1423 ¬_authorized_message(
1424 &principal.arn,
1425 &action_string,
1426 &iam_action.resource,
1427 scp_reason.as_deref(),
1428 Some(&encoded),
1429 ),
1430 request_id,
1431 detected,
1432 &aws_request.headers,
1433 ));
1434 }
1435 }
1438 }
1439 } else {
1440 tracing::warn!(
1452 target: "fakecloud::iam::audit",
1453 service = %detected.service,
1454 action = %aws_request.action,
1455 mode = %config.iam_mode,
1456 request_id = %request_id,
1457 "service is iam_enforceable but has no IamAction mapping for this action; denying under strict, allowing under soft"
1458 );
1459 if config.iam_mode.is_strict() {
1460 return Some(unmapped_action_denial(
1461 principal,
1462 aws_request,
1463 request_id,
1464 detected,
1465 ));
1466 }
1467 }
1470 }
1471 } else if let Some(akid) = aws_request
1472 .access_key_id
1473 .as_deref()
1474 .filter(|_| aws_request.principal.is_none())
1475 {
1476 tracing::warn!(
1487 target: "fakecloud::iam::audit",
1488 service = %detected.service,
1489 action = %aws_request.action,
1490 mode = %config.iam_mode,
1491 request_id = %request_id,
1492 "request credential does not resolve to an identity; denying under strict, allowing under soft"
1493 );
1494 if config.iam_mode.is_strict() {
1495 return Some(unresolved_credential_response(
1496 config,
1497 akid,
1498 request_id,
1499 error_envelope,
1500 ));
1501 }
1502 } else if aws_request.access_key_id.is_none() {
1503 if deferred || !iam_actions.is_empty() {
1515 for iam_action in iam_actions {
1516 let now = chrono::Utc::now();
1517 let mut condition_context = ConditionContext {
1518 aws_source_ip: remote_addr.map(|sa| sa.ip()),
1519 aws_current_time: Some(now),
1520 aws_epoch_time: Some(now.timestamp()),
1521 aws_secure_transport: Some(is_secure_transport(
1522 &aws_request.headers,
1523 )),
1524 aws_requested_region: Some(aws_request.region.clone()),
1525 ..Default::default()
1526 };
1527 condition_context.service_keys =
1528 service.iam_condition_keys_for(aws_request, iam_action);
1529 if let Some(owner) = config
1532 .resource_policy_provider
1533 .as_ref()
1534 .and_then(|p| {
1535 p.resource_owner_account(
1536 &detected.service,
1537 &iam_action.resource,
1538 )
1539 })
1540 .or_else(|| parse_account_from_arn(&iam_action.resource))
1541 {
1542 condition_context
1543 .service_keys
1544 .entry("aws:resourceaccount".to_string())
1545 .or_insert_with(|| vec![owner]);
1546 }
1547 let resource_policy_json =
1548 config.resource_policy_provider.as_ref().and_then(|p| {
1549 p.resource_policy(&detected.service, &iam_action.resource)
1550 });
1551 let policy_decision = evaluator.evaluate_anonymous(
1552 iam_action,
1553 &condition_context,
1554 resource_policy_json.as_deref(),
1555 );
1556 let policy_allows = policy_decision.is_allow();
1557 let policy_explicit_deny =
1562 matches!(policy_decision, crate::auth::IamDecision::ExplicitDeny);
1563 let acl_allows = !policy_explicit_deny
1564 && config.resource_policy_provider.as_ref().is_some_and(|p| {
1565 p.public_acl_allows(
1566 &detected.service,
1567 &iam_action.resource,
1568 iam_action.action,
1569 )
1570 });
1571 if !policy_allows && !acl_allows {
1572 tracing::warn!(
1573 target: "fakecloud::iam::audit",
1574 service = %detected.service,
1575 action = %iam_action.action_string(),
1576 resource = %iam_action.resource,
1577 resource_policy_present = resource_policy_json.is_some(),
1578 mode = %config.iam_mode,
1579 request_id = %request_id,
1580 "anonymous request denied: no public bucket policy or ACL grants the action"
1581 );
1582 if config.iam_mode.is_strict() {
1583 let message = anonymous_denied_message(
1584 &detected.service,
1585 iam_action,
1586 &aws_request.region,
1587 );
1588 if detected.service == "sqs" {
1591 return Some(access_denied_response(
1592 &message,
1593 request_id,
1594 detected,
1595 &aws_request.headers,
1596 ));
1597 }
1598 return Some(build_error_response(
1599 StatusCode::FORBIDDEN,
1600 "AccessDenied",
1601 &message,
1602 request_id,
1603 error_envelope,
1604 ));
1605 }
1606 }
1608 }
1609 } else {
1610 tracing::warn!(
1617 target: "fakecloud::iam::audit",
1618 service = %detected.service,
1619 action = %aws_request.action,
1620 mode = %config.iam_mode,
1621 request_id = %request_id,
1622 "anonymous request to iam_enforceable service has no IamAction mapping; denying under strict, allowing under soft"
1623 );
1624 if config.iam_mode.is_strict() {
1625 return Some(build_error_response(
1626 StatusCode::FORBIDDEN,
1627 "AccessDenied",
1628 "Access Denied",
1629 request_id,
1630 error_envelope,
1631 ));
1632 }
1633 }
1634 }
1635 }
1636 }
1637 None
1638 }
1639}
1640
1641async fn finish_dispatch(
1644 service: Arc<dyn crate::service::AwsService>,
1645 aws_request: AwsRequest,
1646 authorizer: Option<Arc<dyn crate::auth::RequestAuthorizer>>,
1647 detected: protocol::DetectedRequest,
1648 request_id: String,
1649 error_envelope: ErrorEnvelope,
1650) -> Response<Body> {
1651 let handled = match authorizer {
1652 Some(authorizer) => {
1653 crate::auth::with_request_authorizer(authorizer, service.handle(aws_request)).await
1654 }
1655 None => service.handle(aws_request).await,
1656 };
1657 match handled {
1658 Ok(resp) => {
1659 let resp = if detected.protocol == AwsProtocol::RpcV2Cbor {
1660 rpc_v2_cbor_response(resp)
1661 } else {
1662 resp
1663 };
1664 let mut builder = Response::builder()
1665 .status(resp.status)
1666 .header("x-amzn-requestid", &request_id)
1667 .header("x-amz-request-id", &request_id);
1668
1669 if !resp.content_type.is_empty() {
1670 builder = builder.header("content-type", &resp.content_type);
1671 }
1672
1673 let has_content_length = resp
1674 .headers
1675 .iter()
1676 .any(|(k, _)| k.as_str().eq_ignore_ascii_case("content-length"));
1677
1678 for (k, v) in &resp.headers {
1679 builder = builder.header(k, v);
1680 }
1681
1682 match resp.body {
1683 ResponseBody::Bytes(b) => builder.body(Body::from(b)).unwrap(),
1684 ResponseBody::File { file, size } => {
1685 let stream = tokio_util::io::ReaderStream::new(file);
1686 let body = Body::from_stream(stream);
1687 if !has_content_length {
1688 builder = builder.header("content-length", size.to_string());
1689 }
1690 builder.body(body).unwrap()
1691 }
1692 }
1693 }
1694 Err(err) => service_error_response(&err, &detected, &request_id, error_envelope),
1695 }
1696}
1697
1698fn service_error_response(
1700 err: &crate::service::AwsServiceError,
1701 detected: &protocol::DetectedRequest,
1702 request_id: &str,
1703 error_envelope: ErrorEnvelope,
1704) -> Response<Body> {
1705 tracing::warn!(
1706 service = %detected.service,
1707 action = %detected.action,
1708 error = %err,
1709 "request failed"
1710 );
1711 let error_headers = err.response_headers().to_vec();
1712 let mut resp = build_error_response_with_fields(
1713 err.status(),
1714 err.code(),
1715 &err.message(),
1716 request_id,
1717 error_envelope,
1718 err.extra_fields(),
1719 );
1720 for (k, v) in &error_headers {
1721 if let (Ok(name), Ok(val)) = (
1722 k.parse::<http::header::HeaderName>(),
1723 v.parse::<http::header::HeaderValue>(),
1724 ) {
1725 if name == http::header::VARY {
1731 let already = resp
1732 .headers()
1733 .get_all(&name)
1734 .iter()
1735 .any(|existing| existing == val);
1736 if !already {
1737 resp.headers_mut().append(name, val);
1738 }
1739 } else {
1740 resp.headers_mut().insert(name, val);
1741 }
1742 }
1743 }
1744 resp
1745}
1746
1747fn rpc_v2_cbor_response(mut resp: crate::service::AwsResponse) -> crate::service::AwsResponse {
1752 if resp.content_type != crate::cbor::CBOR_CONTENT_TYPE {
1753 if let ResponseBody::Bytes(bytes) = &resp.body {
1754 let json = if bytes.is_empty() {
1755 serde_json::Value::Object(serde_json::Map::new())
1756 } else {
1757 serde_json::from_slice(bytes).unwrap_or_else(|e| {
1758 tracing::warn!(error = %e, "non-JSON response body for an rpcv2Cbor request");
1759 serde_json::Value::Object(serde_json::Map::new())
1760 })
1761 };
1762 resp.body = ResponseBody::Bytes(Bytes::from(crate::cbor::encode(
1763 &crate::cbor::json_to_cbor(&json),
1764 )));
1765 resp.content_type = crate::cbor::CBOR_CONTENT_TYPE.to_string();
1766 }
1767 }
1768 resp.headers.insert(
1769 http::HeaderName::from_static(crate::cbor::SMITHY_PROTOCOL_HEADER),
1770 http::HeaderValue::from_static(crate::cbor::RPC_V2_CBOR),
1771 );
1772 resp
1773}
1774
1775#[derive(Clone)]
1777pub struct DispatchConfig {
1778 pub region: String,
1779 pub account_id: String,
1780 pub verify_sigv4: bool,
1784 pub iam_mode: IamMode,
1789 pub credential_resolver: Option<Arc<dyn CredentialResolver>>,
1793 pub policy_evaluator: Option<Arc<dyn IamPolicyEvaluator>>,
1797 pub resource_policy_provider: Option<Arc<dyn ResourcePolicyProvider>>,
1804 pub scp_resolver: Option<Arc<dyn crate::auth::ScpResolver>>,
1811}
1812
1813impl std::fmt::Debug for DispatchConfig {
1814 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1815 f.debug_struct("DispatchConfig")
1816 .field("region", &self.region)
1817 .field("account_id", &self.account_id)
1818 .field("verify_sigv4", &self.verify_sigv4)
1819 .field("iam_mode", &self.iam_mode)
1820 .field(
1821 "credential_resolver",
1822 &self
1823 .credential_resolver
1824 .as_ref()
1825 .map(|_| "<CredentialResolver>"),
1826 )
1827 .field(
1828 "policy_evaluator",
1829 &self
1830 .policy_evaluator
1831 .as_ref()
1832 .map(|_| "<IamPolicyEvaluator>"),
1833 )
1834 .field(
1835 "resource_policy_provider",
1836 &self
1837 .resource_policy_provider
1838 .as_ref()
1839 .map(|_| "<ResourcePolicyProvider>"),
1840 )
1841 .field(
1842 "scp_resolver",
1843 &self.scp_resolver.as_ref().map(|_| "<ScpResolver>"),
1844 )
1845 .finish()
1846 }
1847}
1848
1849impl DispatchConfig {
1850 pub fn new(region: impl Into<String>, account_id: impl Into<String>) -> Self {
1853 Self {
1854 region: region.into(),
1855 account_id: account_id.into(),
1856 verify_sigv4: false,
1857 iam_mode: IamMode::Off,
1858 credential_resolver: None,
1859 policy_evaluator: None,
1860 resource_policy_provider: None,
1861 scp_resolver: None,
1862 }
1863 }
1864}
1865
1866fn s3_routing_path(wire_path: &str, host_bucket: Option<&str>) -> String {
1882 let Some(bucket) = host_bucket else {
1883 return wire_path.to_string();
1884 };
1885 if wire_path == "/" || wire_path.is_empty() {
1886 format!("/{bucket}")
1887 } else {
1888 format!("/{bucket}{wire_path}")
1889 }
1890}
1891
1892fn streaming_route(
1908 method: &http::Method,
1909 path: &str,
1910 headers: &http::HeaderMap,
1911 query_params: &HashMap<String, String>,
1912) -> Option<(&'static str, &'static str)> {
1913 if (method == http::Method::PATCH || method == http::Method::PUT)
1915 && path.starts_with("/v2/")
1916 && path.contains("/blobs/uploads/")
1917 {
1918 return Some(("ecr", ""));
1919 }
1920
1921 if method == http::Method::PUT {
1926 let header_s3 = headers
1927 .get("authorization")
1928 .and_then(|v| v.to_str().ok())
1929 .and_then(fakecloud_aws::sigv4::parse_sigv4)
1930 .map(|info| info.service == "s3")
1931 .unwrap_or(false);
1932 let presigned_v4_s3 = query_params
1933 .get("X-Amz-Credential")
1934 .and_then(|c| c.split('/').nth(3).map(|s| s.to_string()))
1935 .map(|service| service == "s3")
1936 .unwrap_or(false);
1937 let presigned_v2 = query_params.contains_key("AWSAccessKeyId")
1938 && query_params.contains_key("Signature")
1939 && query_params.contains_key("Expires");
1940 if header_s3 || presigned_v4_s3 || presigned_v2 {
1941 return Some(("s3", ""));
1942 }
1943 }
1944
1945 None
1946}
1947
1948struct EarlyRequest<'a> {
1951 method: &'a http::Method,
1952 wire_path: &'a str,
1953 headers: &'a http::HeaderMap,
1954 query_params: &'a HashMap<String, String>,
1955 anonymous: bool,
1956 region: &'a str,
1957 request_id: &'a str,
1958}
1959
1960fn service_streams_body(service: &dyn crate::service::AwsService, early: &EarlyRequest) -> bool {
1967 with_request_head(service, early, |head| service.streams_request_body(head))
1968}
1969
1970fn with_request_head<R>(
1976 service: &dyn crate::service::AwsService,
1977 early: &EarlyRequest,
1978 f: impl FnOnce(&crate::service::RequestHead<'_>) -> R,
1979) -> R {
1980 let EarlyRequest {
1981 method,
1982 wire_path,
1983 headers,
1984 query_params,
1985 anonymous,
1986 region,
1987 request_id,
1988 } = *early;
1989 let is_s3 = service.service_name() == "s3";
1990 let path = if is_s3 {
1991 let host_bucket = protocol::parse_routing_host_from_headers(headers)
1992 .filter(|h| h.service == "s3")
1993 .and_then(|h| h.bucket);
1994 s3_routing_path(wire_path, host_bucket.as_deref())
1995 } else {
1996 wire_path.to_string()
1997 };
1998 let hoisted;
1999 let headers = if is_s3 && query_params.contains_key("X-Amz-Credential") {
2000 let mut with_query = headers.clone();
2001 hoist_presigned_query_headers(&mut with_query, query_params);
2002 hoisted = with_query;
2003 &hoisted
2004 } else {
2005 headers
2006 };
2007 f(&crate::service::RequestHead {
2008 method,
2009 raw_path: &path,
2010 headers,
2011 query_params,
2012 anonymous,
2013 region,
2014 request_id,
2015 })
2016}
2017
2018fn request_region(
2021 headers: &http::HeaderMap,
2022 query_params: &HashMap<String, String>,
2023 default_region: &str,
2024) -> String {
2025 let auth_header = headers
2026 .get("authorization")
2027 .and_then(|v| v.to_str().ok())
2028 .unwrap_or("");
2029 fakecloud_aws::sigv4::parse_sigv4(auth_header)
2030 .or_else(|| fakecloud_aws::sigv4::parse_sigv4_presigned(query_params).map(|p| p.as_info()))
2031 .map(|info| info.region)
2032 .or_else(|| protocol::parse_routing_host_from_headers(headers).map(|h| h.region))
2033 .or_else(|| extract_region_from_user_agent(headers))
2034 .unwrap_or_else(|| default_region.to_string())
2035}
2036
2037fn presents_no_credential(
2042 headers: &http::HeaderMap,
2043 query_params: &HashMap<String, String>,
2044) -> bool {
2045 headers
2046 .get("authorization")
2047 .and_then(|v| v.to_str().ok())
2048 .unwrap_or("")
2049 .is_empty()
2050 && !query_params.contains_key("X-Amz-Credential")
2051 && sigv2_presigned_access_key(query_params).is_none()
2052 && sigv2_query_access_key(query_params).is_none()
2053}
2054
2055const PRESIGN_AUTH_PARAMS: &[&str] = &[
2058 "x-amz-algorithm",
2059 "x-amz-credential",
2060 "x-amz-date",
2061 "x-amz-expires",
2062 "x-amz-signedheaders",
2063 "x-amz-signature",
2064 "x-amz-security-token",
2065];
2066
2067fn hoist_presigned_query_headers(
2073 headers: &mut http::HeaderMap,
2074 query_params: &HashMap<String, String>,
2075) {
2076 for (key, value) in query_params {
2077 let lower = key.to_ascii_lowercase();
2078 if !lower.starts_with("x-amz-") || PRESIGN_AUTH_PARAMS.contains(&lower.as_str()) {
2079 continue;
2080 }
2081 let Ok(name) = http::HeaderName::from_bytes(lower.as_bytes()) else {
2082 continue;
2083 };
2084 if headers.contains_key(&name) {
2085 continue;
2086 }
2087 let value = if lower.starts_with("x-amz-meta-") {
2088 crate::rfc2047::encode(value)
2089 } else {
2090 value.clone()
2091 };
2092 if let Ok(value) = http::HeaderValue::from_str(&value) {
2093 headers.insert(name, value);
2094 }
2095 }
2096}
2097
2098const DEFAULT_MAX_REQUEST_BODY_BYTES: usize = 1024 * 1024 * 1024;
2108
2109pub fn max_request_body_bytes() -> usize {
2114 static CACHED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2115 *CACHED.get_or_init(|| {
2116 std::env::var("FAKECLOUD_MAX_REQUEST_BODY_BYTES")
2117 .ok()
2118 .and_then(|s| s.parse::<usize>().ok())
2119 .filter(|&n| n > 0)
2120 .unwrap_or(DEFAULT_MAX_REQUEST_BODY_BYTES)
2121 })
2122}
2123
2124fn parse_account_from_arn(arn: &str) -> Option<String> {
2130 let mut parts = arn.splitn(6, ':');
2131 if parts.next()? != "arn" {
2132 return None;
2133 }
2134 let _partition = parts.next()?;
2135 let _service = parts.next()?;
2136 let _region = parts.next()?;
2137 let account = parts.next()?;
2138 parts.next()?;
2141 if account.is_empty() {
2142 None
2143 } else {
2144 Some(account.to_string())
2145 }
2146}
2147
2148fn user_agent_indicates_neptune(headers: &http::HeaderMap) -> bool {
2154 for name in ["user-agent", "x-amz-user-agent"] {
2155 if let Some(ua) = headers.get(name).and_then(|v| v.to_str().ok()) {
2156 for part in ua.split_whitespace() {
2157 if let Some(rest) = part.strip_prefix("api/neptune") {
2158 if rest.is_empty() || rest.starts_with('#') || rest.starts_with('/') {
2159 return true;
2160 }
2161 }
2162 }
2163 }
2164 }
2165 false
2166}
2167
2168fn user_agent_indicates_docdb(headers: &http::HeaderMap) -> bool {
2175 for name in ["user-agent", "x-amz-user-agent"] {
2176 if let Some(ua) = headers.get(name).and_then(|v| v.to_str().ok()) {
2177 for part in ua.split_whitespace() {
2178 if let Some(rest) = part.strip_prefix("api/docdb") {
2179 if rest.is_empty() || rest.starts_with('#') || rest.starts_with('/') {
2180 return true;
2181 }
2182 }
2183 }
2184 }
2185 }
2186 false
2187}
2188
2189fn extract_region_from_user_agent(headers: &http::HeaderMap) -> Option<String> {
2190 let ua = headers.get("user-agent")?.to_str().ok()?;
2191 for part in ua.split_whitespace() {
2192 if let Some(region) = part.strip_prefix("region/") {
2193 if !region.is_empty() {
2194 return Some(region.to_string());
2195 }
2196 }
2197 }
2198 None
2199}
2200
2201#[derive(Clone, Copy, Debug)]
2206struct ErrorEnvelope {
2207 protocol: AwsProtocol,
2208 rest_xml_namespace: Option<&'static str>,
2210 s3_control: bool,
2213 lambda_body: bool,
2215 query: fakecloud_aws::error::QueryErrorShape,
2217}
2218
2219impl ErrorEnvelope {
2220 fn for_request(detected: &protocol::DetectedRequest, headers: &http::HeaderMap) -> Self {
2222 let rest_xml_namespace = if detected.protocol == AwsProtocol::Rest {
2223 fakecloud_aws::error::rest_xml_error_namespace(&detected.service)
2224 } else {
2225 None
2226 };
2227 Self {
2228 protocol: detected.protocol,
2229 rest_xml_namespace,
2230 s3_control: detected.protocol == AwsProtocol::Rest
2231 && detected.service == "s3"
2232 && protocol::is_s3_control_host(headers),
2233 lambda_body: detected.protocol == AwsProtocol::RestJson
2234 && fakecloud_aws::error::uses_lambda_error_body(&detected.service),
2235 query: if detected.protocol == AwsProtocol::Query {
2236 fakecloud_aws::error::QueryErrorShape::for_service(&detected.service)
2237 } else {
2238 Default::default()
2239 },
2240 }
2241 }
2242}
2243
2244impl From<AwsProtocol> for ErrorEnvelope {
2245 fn from(protocol: AwsProtocol) -> Self {
2246 Self {
2247 protocol,
2248 rest_xml_namespace: None,
2249 s3_control: false,
2250 lambda_body: false,
2251 query: Default::default(),
2252 }
2253 }
2254}
2255
2256fn build_error_response(
2257 status: StatusCode,
2258 code: &str,
2259 message: &str,
2260 request_id: &str,
2261 envelope: impl Into<ErrorEnvelope>,
2262) -> Response<Body> {
2263 build_error_response_with_fields(status, code, message, request_id, envelope, &[])
2264}
2265
2266fn build_error_response_with_fields(
2267 status: StatusCode,
2268 code: &str,
2269 message: &str,
2270 request_id: &str,
2271 envelope: impl Into<ErrorEnvelope>,
2272 extra_fields: &[(String, String)],
2273) -> Response<Body> {
2274 let envelope = envelope.into();
2275 let (status, content_type, body) = match (envelope.protocol, envelope.rest_xml_namespace) {
2276 (AwsProtocol::Query, _) => fakecloud_aws::error::query_error_response(
2279 status,
2280 code,
2281 message,
2282 request_id,
2283 envelope.query,
2284 ),
2285 (AwsProtocol::Ec2Query, _) => {
2290 fakecloud_aws::ec2query::ec2_error_response(status, code, message, request_id)
2291 }
2292 (AwsProtocol::Rest, Some(namespace)) => fakecloud_aws::error::rest_xml_error_response(
2295 status, code, message, request_id, namespace,
2296 ),
2297 (AwsProtocol::Rest, None) if envelope.s3_control => {
2298 fakecloud_aws::error::s3_control_xml_error_response(
2299 status,
2300 code,
2301 message,
2302 request_id,
2303 extra_fields,
2304 )
2305 }
2306 (AwsProtocol::Rest, None) => fakecloud_aws::error::s3_xml_error_response_with_fields(
2307 status,
2308 code,
2309 message,
2310 request_id,
2311 extra_fields,
2312 ),
2313 (AwsProtocol::RestJson, _) if envelope.lambda_body => {
2314 fakecloud_aws::error::lambda_json_error_response(status, code, message, extra_fields)
2315 }
2316 (AwsProtocol::Json | AwsProtocol::RestJson, _) => {
2317 fakecloud_aws::error::json_error_response_with_fields(
2318 status,
2319 code,
2320 message,
2321 extra_fields,
2322 )
2323 }
2324 (AwsProtocol::RpcV2Cbor, _) => (
2325 status,
2326 crate::cbor::CBOR_CONTENT_TYPE.to_string(),
2327 Bytes::from(crate::cbor::error_body(code, message, extra_fields)),
2328 ),
2329 };
2330
2331 let safe_code = sanitize_header_value(code);
2341 let safe_message = sanitize_header_value(message);
2342 let mut builder = Response::builder()
2343 .status(status)
2344 .header("content-type", content_type)
2345 .header("x-amzn-requestid", request_id)
2346 .header("x-amz-request-id", request_id);
2347 if let Ok(v) = http::HeaderValue::from_str(&safe_code) {
2348 builder = builder.header("x-amz-error-code", v);
2349 }
2350 if let Ok(v) = http::HeaderValue::from_str(&safe_message) {
2351 builder = builder.header("x-amz-error-message", v);
2352 }
2353 if envelope.protocol == AwsProtocol::RestJson {
2356 if let Ok(v) = http::HeaderValue::from_str(&safe_code) {
2357 builder = builder.header("x-amzn-errortype", v);
2358 }
2359 }
2360 if envelope.protocol == AwsProtocol::RpcV2Cbor {
2361 builder = builder.header(
2362 crate::cbor::SMITHY_PROTOCOL_HEADER,
2363 crate::cbor::RPC_V2_CBOR,
2364 );
2365 }
2366 builder.body(Body::from(body)).unwrap_or_else(|_| {
2367 Response::new(Body::empty())
2371 })
2372}
2373
2374fn sanitize_header_value(s: &str) -> String {
2379 const MAX_LEN: usize = 1024;
2380 let mut out = String::with_capacity(s.len().min(MAX_LEN));
2381 for ch in s.chars() {
2382 if out.len() >= MAX_LEN {
2383 break;
2384 }
2385 if ch.is_control() {
2388 if !out.ends_with(' ') {
2389 out.push(' ');
2390 }
2391 } else {
2392 out.push(ch);
2393 }
2394 }
2395 out.trim().to_string()
2396}
2397
2398fn sigv2_presigned_access_key(query_params: &HashMap<String, String>) -> Option<String> {
2418 if query_params.contains_key("Signature") && query_params.contains_key("Expires") {
2419 query_params.get("AWSAccessKeyId").cloned()
2420 } else {
2421 None
2422 }
2423}
2424
2425fn sigv2_query_access_key(params: &HashMap<String, String>) -> Option<String> {
2431 (params.get("SignatureVersion").map(String::as_str) == Some("2")
2432 && params.contains_key("Signature"))
2433 .then(|| params.get("AWSAccessKeyId").cloned())
2434 .flatten()
2435 .filter(|key| !key.is_empty())
2436}
2437
2438fn is_hex_sha256(s: &str) -> bool {
2444 s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
2445}
2446
2447fn sha256_hex_lower(bytes: &[u8]) -> String {
2450 use sha2::{Digest, Sha256};
2451 let digest = Sha256::digest(bytes);
2452 const HEX: &[u8] = b"0123456789abcdef";
2453 let mut out = String::with_capacity(64);
2454 for b in digest {
2455 out.push(HEX[(b >> 4) as usize] as char);
2456 out.push(HEX[(b & 0x0f) as usize] as char);
2457 }
2458 out
2459}
2460
2461#[allow(clippy::too_many_arguments)]
2467fn principal_condition_context(
2468 principal: &Principal,
2469 resolved: Option<&crate::auth::ResolvedCredential>,
2470 service: &dyn crate::service::AwsService,
2471 aws_request: &AwsRequest,
2472 iam_action: &crate::auth::IamAction,
2473 detected: &protocol::DetectedRequest,
2474 remote_addr: Option<SocketAddr>,
2475) -> ConditionContext {
2476 let mut ctx = build_condition_context(
2477 principal,
2478 remote_addr,
2479 &aws_request.region,
2480 is_secure_transport(&aws_request.headers),
2481 );
2482 if let Some(rc) = resolved {
2490 if let Some(role_arn) = rc.role_arn.as_ref() {
2493 ctx.aws_principal_arn = Some(role_arn.clone());
2494 }
2495 ctx.federated_parent_arn = rc.federated_parent_arn.clone();
2496 ctx.aws_mfa_present = Some(rc.mfa_present);
2497 ctx.aws_token_issue_time = rc.token_issued_at;
2498 ctx.aws_federated_provider = rc.federated_provider.clone();
2499 if rc.mfa_present {
2507 if let Some(issued) = rc.token_issued_at {
2508 let age = chrono::Utc::now()
2509 .signed_duration_since(issued)
2510 .num_seconds()
2511 .max(0);
2512 ctx.aws_mfa_age_seconds = Some(age);
2513 }
2514 }
2515 }
2516 ctx.service_keys = service.iam_condition_keys_for(aws_request, iam_action);
2517
2518 let service_resource = !iam_action.is_pass_role();
2524 match service_resource
2525 .then(|| service.resource_tags_for(&iam_action.resource))
2526 .flatten()
2527 {
2528 Some(tags) => ctx.resource_tags = Some(tags),
2529 None => tracing::debug!(
2530 target: "fakecloud::iam::audit",
2531 service = %detected.service,
2532 resource = %iam_action.resource,
2533 "service does not expose resource tags for ABAC; skipping aws:ResourceTag/* evaluation"
2534 ),
2535 }
2536 match service.request_tags_from(aws_request, iam_action.action) {
2538 Some(tags) => ctx.request_tags = Some(tags),
2539 None => tracing::debug!(
2540 target: "fakecloud::iam::audit",
2541 service = %detected.service,
2542 action = %iam_action.action_string(),
2543 "service does not expose request tags for ABAC; skipping aws:RequestTag/* / aws:TagKeys evaluation"
2544 ),
2545 }
2546 ctx.principal_tags = principal.tags.clone();
2548 ctx
2549}
2550
2551fn add_global_request_keys(
2554 ctx: &mut ConditionContext,
2555 principal: &Principal,
2556 resource_account_id: &str,
2557 scp_resolver: Option<&dyn crate::auth::ScpResolver>,
2558) {
2559 global_request_keys(ctx, &principal.account_id, resource_account_id, |account| {
2560 scp_resolver.and_then(|r| r.principal_org(account))
2561 });
2562}
2563
2564pub fn global_request_keys(
2573 ctx: &mut ConditionContext,
2574 principal_account: &str,
2575 resource_account_id: &str,
2576 org_of: impl Fn(&str) -> Option<(String, String)>,
2577) {
2578 let mut set = |key: &str, value: String| {
2579 ctx.service_keys
2580 .entry(key.to_string())
2581 .or_insert_with(|| vec![value]);
2582 };
2583 if !resource_account_id.is_empty() {
2584 set("aws:resourceaccount", resource_account_id.to_string());
2585 if let Some((org_id, path)) = org_of(resource_account_id) {
2586 set("aws:resourceorgid", org_id);
2587 set("aws:resourceorgpaths", path);
2588 }
2589 }
2590 if let Some((org_id, path)) = org_of(principal_account) {
2591 set("aws:principalorgid", org_id);
2592 set("aws:principalorgpaths", path);
2593 }
2594 set("aws:viaawsservice", "false".to_string());
2595}
2596
2597fn unresolved_credential_response(
2601 config: &DispatchConfig,
2602 access_key_id: &str,
2603 request_id: &str,
2604 envelope: ErrorEnvelope,
2605) -> Response<Body> {
2606 let expired = config
2607 .credential_resolver
2608 .as_ref()
2609 .is_some_and(|r| r.is_expired(access_key_id));
2610 let (code, message) = if expired {
2611 (
2612 "ExpiredToken",
2613 "The security token included in the request is expired",
2614 )
2615 } else {
2616 invalid_token_error(&envelope)
2617 };
2618 build_error_response(StatusCode::FORBIDDEN, code, message, request_id, envelope)
2619}
2620
2621fn invalid_token_error(envelope: &ErrorEnvelope) -> (&'static str, &'static str) {
2625 if envelope.lambda_body {
2626 (
2627 "UnrecognizedClientException",
2628 "The security token included in the request is invalid.",
2629 )
2630 } else {
2631 (
2632 "InvalidClientTokenId",
2633 "The security token included in the request is invalid",
2634 )
2635 }
2636}
2637
2638fn unrouted_target(
2659 method: &http::Method,
2660 uri: &http::Uri,
2661 headers: &http::HeaderMap,
2662 query_params: &HashMap<String, String>,
2663 registry: &ServiceRegistry,
2664 config: &DispatchConfig,
2665) -> Option<&'static str> {
2666 use crate::service::DataPlaneClaim;
2667 let path = uri.path();
2668 let claim = registry
2669 .get("apigateway")
2670 .and_then(|apigw| apigw.claim_unrouted_request(method, path, headers, &config.account_id));
2671 if claim == Some(DataPlaneClaim::Host)
2672 || (*method == http::Method::OPTIONS && is_execute_api_host(headers))
2673 {
2674 return Some("apigateway");
2675 }
2676 if *method == http::Method::OPTIONS {
2677 return Some("s3");
2678 }
2679 if path == "/v2" || path.starts_with("/v2/") {
2680 return Some("ecr");
2681 }
2682 if let Some(bucket) = anonymous_s3_bucket(uri, config) {
2683 tracing::debug!(bucket = %bucket, "routing unrouted request to S3 (existing bucket)");
2684 return Some("s3");
2685 }
2686 let fakecloud_endpoint = path.starts_with("/_");
2687 let s3_takes_it = match claim {
2688 Some(_) => is_unmistakable_s3_query_auth(headers, query_params),
2691 None => has_s3_query_auth(headers, query_params) || is_unsigned(headers, query_params),
2692 };
2693 if !fakecloud_endpoint && registry.get("s3").is_some() && s3_takes_it {
2694 return Some("s3");
2695 }
2696 if !fakecloud_endpoint || path.starts_with("/_aws/execute-api/") {
2697 return Some("apigateway");
2698 }
2699 None
2700}
2701
2702fn unsigned_action_is_a_parameter(
2712 method: &http::Method,
2713 uri: &http::Uri,
2714 headers: &http::HeaderMap,
2715 query_params: &HashMap<String, String>,
2716 registry: &ServiceRegistry,
2717 config: &DispatchConfig,
2718) -> bool {
2719 if !is_unsigned(headers, query_params) || headers.contains_key("x-amz-target") {
2720 return false;
2721 }
2722 let plain_host = protocol::parse_routing_host_from_headers(headers).is_none();
2723 (plain_host && anonymous_s3_bucket(uri, config).is_some())
2724 || registry.get("apigateway").is_some_and(|apigw| {
2725 apigw
2729 .claim_unrouted_request(method, uri.path(), headers, &config.account_id)
2730 .is_some()
2731 })
2732}
2733
2734fn unrouted_detection(service: &str) -> protocol::DetectedRequest {
2736 protocol::DetectedRequest {
2737 service: service.to_string(),
2738 action: String::new(),
2739 protocol: if service == "apigateway" {
2740 AwsProtocol::RestJson
2741 } else {
2742 AwsProtocol::Rest
2743 },
2744 }
2745}
2746
2747fn is_unsigned(headers: &http::HeaderMap, query_params: &HashMap<String, String>) -> bool {
2752 const QUERY_AUTH: &[&str] = &[
2753 "X-Amz-Credential",
2754 "X-Amz-Signature",
2755 "AWSAccessKeyId",
2756 "Signature",
2757 ];
2758 headers
2759 .get(http::header::AUTHORIZATION)
2760 .is_none_or(|v| v.as_bytes().is_empty())
2761 && !QUERY_AUTH.iter().any(|k| query_params.contains_key(*k))
2762}
2763
2764fn has_s3_query_auth(headers: &http::HeaderMap, query_params: &HashMap<String, String>) -> bool {
2769 if headers
2770 .get(http::header::AUTHORIZATION)
2771 .is_some_and(|v| !v.as_bytes().is_empty())
2772 {
2773 return false;
2774 }
2775 if query_params.contains_key("AWSAccessKeyId") || query_params.contains_key("Signature") {
2776 return true;
2777 }
2778 let sigv4 = query_params.contains_key("X-Amz-Credential")
2779 || query_params.contains_key("X-Amz-Signature")
2780 || query_params.contains_key("X-Amz-Algorithm");
2781 sigv4
2782 && query_params
2783 .get("X-Amz-Credential")
2784 .and_then(|c| c.split('/').nth(3))
2785 .is_none_or(|service| service == "s3")
2786}
2787
2788fn is_unmistakable_s3_query_auth(
2793 headers: &http::HeaderMap,
2794 query_params: &HashMap<String, String>,
2795) -> bool {
2796 if headers
2797 .get(http::header::AUTHORIZATION)
2798 .is_some_and(|v| !v.as_bytes().is_empty())
2799 {
2800 return false;
2801 }
2802 let sigv4_s3 = query_params
2803 .get("X-Amz-Credential")
2804 .and_then(|c| c.split('/').nth(3))
2805 .is_some_and(|service| service == "s3");
2806 let sigv2_parts = ["AWSAccessKeyId", "Signature", "Expires"]
2807 .iter()
2808 .filter(|k| query_params.contains_key(**k))
2809 .count();
2810 sigv4_s3 || sigv2_parts >= 2
2811}
2812
2813fn s3_sigv2_query_auth_error(
2829 headers: &http::HeaderMap,
2830 query_params: &HashMap<String, String>,
2831 now: chrono::DateTime<chrono::Utc>,
2832) -> Option<(String, Vec<(String, String)>)> {
2833 if headers.contains_key(http::header::AUTHORIZATION)
2834 || !(query_params.contains_key("AWSAccessKeyId") || query_params.contains_key("Signature"))
2835 {
2836 return None;
2837 }
2838 let (Some(_), Some(_), Some(expires)) = (
2839 query_params.get("AWSAccessKeyId"),
2840 query_params.get("Signature"),
2841 query_params.get("Expires"),
2842 ) else {
2843 return Some((
2844 "Query-string authentication requires the Signature, Expires and AWSAccessKeyId \
2845 parameters"
2846 .to_string(),
2847 Vec::new(),
2848 ));
2849 };
2850 let Ok(secs) = expires.parse::<i32>() else {
2851 return Some((
2852 format!("Invalid date (should be seconds since epoch): {expires}"),
2853 Vec::new(),
2854 ));
2855 };
2856 let expiry = chrono::DateTime::from_timestamp(i64::from(secs), 0)?;
2857 (expiry < now).then(|| {
2858 (
2859 "Request has expired".to_string(),
2860 vec![
2861 ("Expires".to_string(), s3_error_time(expiry)),
2862 ("ServerTime".to_string(), s3_error_time(now)),
2863 ],
2864 )
2865 })
2866}
2867
2868fn anonymous_denied_message(
2878 service: &str,
2879 action: &crate::auth::IamAction,
2880 region: &str,
2881) -> std::borrow::Cow<'static, str> {
2882 if service == "s3" && action.service == "s3" && action.action == "CreateBucket" {
2883 "Anonymous users cannot invoke this API. Please authenticate.".into()
2884 } else if service == "sqs" {
2885 if action.resource.starts_with("arn:") {
2886 let name = format!("sqs:{}", action.action.to_ascii_lowercase());
2887 format!(
2888 "User: anonymous is not authorized to perform: {name} on resource: {} \
2889 because no resource-based policy allows the {name} action",
2890 action.resource,
2891 )
2892 .into()
2893 } else {
2894 format!(
2895 "Access to the resource https://sqs.{region}.{}/ is denied.",
2896 fakecloud_aws::endpoint::dns_suffix_for_region(region)
2897 )
2898 .into()
2899 }
2900 } else {
2901 "Access Denied".into()
2902 }
2903}
2904
2905fn names_api_operation(
2917 detected: &protocol::DetectedRequest,
2918 host: Option<&protocol::RoutingHost>,
2919 method: &http::Method,
2920 headers: &http::HeaderMap,
2921 is_control_plane: impl FnOnce() -> bool,
2922) -> bool {
2923 if *method == http::Method::OPTIONS {
2924 return false;
2925 }
2926 match detected.protocol {
2927 AwsProtocol::Json | AwsProtocol::Query | AwsProtocol::Ec2Query | AwsProtocol::RpcV2Cbor => {
2928 !detected.action.is_empty()
2929 }
2930 AwsProtocol::Rest | AwsProtocol::RestJson => {
2931 protocol::is_rest_api_service(&detected.service)
2932 && host.is_some_and(|h| h.service == detected.service && h.bucket.is_none())
2933 && !is_execute_api_host(headers)
2934 && is_control_plane()
2935 }
2936 }
2937}
2938
2939fn missing_authentication_response(
2959 detected: &protocol::DetectedRequest,
2960 request_id: &str,
2961 headers: &http::HeaderMap,
2962) -> Response<Body> {
2963 const QUERY_MESSAGE: &str = "Request is missing Authentication Token";
2964 const JSON_MESSAGE: &str = "Missing Authentication Token";
2965 let envelope = ErrorEnvelope::for_request(detected, headers);
2966 let (status, code, message, envelope) = match (detected.service.as_str(), detected.protocol) {
2967 (_, AwsProtocol::Ec2Query) => (
2968 StatusCode::BAD_REQUEST,
2969 "MissingParameter",
2970 "The request must contain the parameter AWSAccessKeyId",
2971 envelope,
2972 ),
2973 ("dynamodb", AwsProtocol::Json | AwsProtocol::RpcV2Cbor) => (
2974 StatusCode::BAD_REQUEST,
2975 "com.amazon.coral.service#MissingAuthenticationTokenException",
2976 QUERY_MESSAGE,
2977 envelope,
2978 ),
2979 ("secretsmanager", AwsProtocol::Json | AwsProtocol::RpcV2Cbor) => (
2980 StatusCode::BAD_REQUEST,
2981 "UnrecognizedClientException",
2982 "The security token included in the request is invalid.",
2983 envelope,
2984 ),
2985 (_, AwsProtocol::Json | AwsProtocol::RpcV2Cbor) => (
2986 StatusCode::BAD_REQUEST,
2987 "MissingAuthenticationTokenException",
2988 JSON_MESSAGE,
2989 envelope,
2990 ),
2991 (_, AwsProtocol::Query | AwsProtocol::Rest) => (
2992 StatusCode::FORBIDDEN,
2993 "MissingAuthenticationToken",
2994 QUERY_MESSAGE,
2995 envelope,
2996 ),
2997 (_, AwsProtocol::RestJson) => (
3000 StatusCode::FORBIDDEN,
3001 "MissingAuthenticationTokenException",
3002 JSON_MESSAGE,
3003 ErrorEnvelope {
3004 lambda_body: true,
3005 ..envelope
3006 },
3007 ),
3008 };
3009 build_error_response(status, code, message, request_id, envelope)
3010}
3011
3012fn s3_error_time(t: chrono::DateTime<chrono::Utc>) -> String {
3014 t.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
3015}
3016
3017fn anonymous_s3_bucket(uri: &http::Uri, config: &DispatchConfig) -> Option<String> {
3018 let provider = config.resource_policy_provider.as_ref()?;
3019 let segment = uri.path().split('/').find(|s| !s.is_empty())?.to_string();
3020 let arn = fakecloud_aws::arn::Arn::s3(&segment).to_string();
3021 provider.resource_owner_account("s3", &arn).map(|_| segment)
3022}
3023
3024fn internal_caller_account(
3029 caller: &InternalCaller,
3030 service: &str,
3031 path_segments: &[String],
3032 config: &DispatchConfig,
3033) -> String {
3034 let bucket_owner = (service == "s3")
3035 .then(|| path_segments.first())
3036 .flatten()
3037 .and_then(|bucket| {
3038 let arn = fakecloud_aws::arn::Arn::s3(bucket).to_string();
3039 config
3040 .resource_policy_provider
3041 .as_ref()?
3042 .resource_owner_account("s3", &arn)
3043 });
3044 bucket_owner.unwrap_or_else(|| caller.acting_account().to_string())
3045}
3046
3047#[allow(clippy::too_many_arguments)]
3059fn authorize_internal_caller(
3060 caller: &InternalCaller,
3061 service: &dyn crate::service::AwsService,
3062 aws_request: &AwsRequest,
3063 evaluator: &dyn IamPolicyEvaluator,
3064 config: &DispatchConfig,
3065 detected: &protocol::DetectedRequest,
3066 request_id: &str,
3067 iam_actions: &[crate::auth::IamAction],
3068 deferred: bool,
3069) -> Option<Response<Body>> {
3070 let principal = caller.principal();
3071 let denied = || {
3072 config.iam_mode.is_strict().then(|| {
3073 build_error_response(
3074 StatusCode::FORBIDDEN,
3075 "AccessDenied",
3076 "Access Denied",
3077 request_id,
3078 ErrorEnvelope::for_request(detected, &aws_request.headers),
3079 )
3080 })
3081 };
3082 if iam_actions.is_empty() && !deferred {
3083 tracing::warn!(
3084 target: "fakecloud::iam::audit",
3085 service = %detected.service,
3086 action = %aws_request.action,
3087 principal = %principal.arn,
3088 mode = %config.iam_mode,
3089 request_id = %request_id,
3090 "service-principal request has no IamAction mapping; denying under strict, allowing under soft"
3091 );
3092 return denied();
3093 }
3094 for iam_action in iam_actions.iter().filter(|a| !a.is_pass_role()) {
3097 let now = chrono::Utc::now();
3098 let mut context = ConditionContext {
3099 aws_principal_arn: Some(principal.arn.clone()),
3100 aws_current_time: Some(now),
3101 aws_epoch_time: Some(now.timestamp()),
3102 aws_secure_transport: Some(is_secure_transport(&aws_request.headers)),
3103 aws_requested_region: Some(aws_request.region.clone()),
3104 ..Default::default()
3105 };
3106 context.service_keys = service.iam_condition_keys_for(aws_request, iam_action);
3107 context.service_keys.extend(caller.condition_keys());
3108 context
3111 .service_keys
3112 .entry("aws:resourceaccount".to_string())
3113 .or_insert_with(|| vec![aws_request.account_id.clone()]);
3114 let resource_policy_json = config
3115 .resource_policy_provider
3116 .as_ref()
3117 .and_then(|p| p.resource_policy(&detected.service, &iam_action.resource));
3118 let decision = evaluator.evaluate_resource_policy_only(
3119 &principal,
3120 iam_action,
3121 &context,
3122 resource_policy_json.as_deref(),
3123 );
3124 let explicit_deny = matches!(decision, crate::auth::IamDecision::ExplicitDeny);
3125 let acl_allows = !explicit_deny
3126 && config.resource_policy_provider.as_ref().is_some_and(|p| {
3127 p.public_acl_allows(&detected.service, &iam_action.resource, iam_action.action)
3128 });
3129 if !decision.is_allow() && !acl_allows {
3130 tracing::warn!(
3131 target: "fakecloud::iam::audit",
3132 service = %detected.service,
3133 action = %iam_action.action_string(),
3134 resource = %iam_action.resource,
3135 principal = %principal.arn,
3136 resource_policy_present = resource_policy_json.is_some(),
3137 decision = ?decision,
3138 mode = %config.iam_mode,
3139 request_id = %request_id,
3140 "service-principal request denied: the resource policy does not grant the action"
3141 );
3142 if let Some(resp) = denied() {
3143 return Some(resp);
3144 }
3145 }
3146 }
3147 None
3148}
3149
3150#[allow(clippy::too_many_arguments)]
3161fn grant_authorizes(
3162 provider: Option<&dyn ResourcePolicyProvider>,
3163 evaluator: &dyn IamPolicyEvaluator,
3164 service: &str,
3165 request: &AwsRequest,
3166 action: &crate::auth::IamAction,
3167 principal: &Principal,
3168 context: &ConditionContext,
3169 session_policies: &[String],
3170 scps: Option<&[String]>,
3171) -> bool {
3172 let Some(provider) = provider else {
3173 return false;
3174 };
3175 match provider.grant_authorization(service, request, action, principal) {
3176 Some(crate::auth::GrantAuthorization::Grantee) => evaluator
3177 .evaluate_ceiling(principal, action, context, &[], scps, false)
3178 .is_allow(),
3179 Some(crate::auth::GrantAuthorization::GranteeRole) => evaluator
3180 .evaluate_ceiling(principal, action, context, session_policies, scps, true)
3181 .is_allow(),
3182 Some(crate::auth::GrantAuthorization::Account) => evaluator
3183 .evaluate(principal, action, context, session_policies, scps)
3184 .is_allow(),
3185 None => false,
3186 }
3187}
3188
3189#[allow(clippy::too_many_arguments)]
3196fn authorize_root(
3197 principal: &Principal,
3198 service: &dyn crate::service::AwsService,
3199 aws_request: &AwsRequest,
3200 evaluator: &dyn IamPolicyEvaluator,
3201 config: &DispatchConfig,
3202 detected: &protocol::DetectedRequest,
3203 request_id: &str,
3204 remote_addr: Option<SocketAddr>,
3205 resolved: Option<&crate::auth::ResolvedCredential>,
3206 iam_actions: &[crate::auth::IamAction],
3207 deferred: bool,
3208) -> Option<Response<Body>> {
3209 authorize_root_scps(
3210 principal,
3211 service,
3212 aws_request,
3213 evaluator,
3214 config,
3215 detected,
3216 request_id,
3217 remote_addr,
3218 resolved,
3219 iam_actions,
3220 deferred,
3221 )
3222 .or_else(|| {
3223 authorize_root_cross_account(
3224 principal,
3225 service,
3226 aws_request,
3227 evaluator,
3228 config,
3229 detected,
3230 request_id,
3231 remote_addr,
3232 resolved,
3233 iam_actions,
3234 )
3235 })
3236}
3237
3238#[allow(clippy::too_many_arguments)]
3251fn authorize_root_scps(
3252 principal: &Principal,
3253 service: &dyn crate::service::AwsService,
3254 aws_request: &AwsRequest,
3255 evaluator: &dyn IamPolicyEvaluator,
3256 config: &DispatchConfig,
3257 detected: &protocol::DetectedRequest,
3258 request_id: &str,
3259 remote_addr: Option<SocketAddr>,
3260 resolved: Option<&crate::auth::ResolvedCredential>,
3261 iam_actions: &[crate::auth::IamAction],
3262 deferred: bool,
3263) -> Option<Response<Body>> {
3264 let resolver = config.scp_resolver.as_deref();
3265 let scps = resolver.and_then(|r| r.scps_for(principal));
3266 let session_policies = resolved.map_or(&[][..], |r| r.session_policies.as_slice());
3267 if scps.is_none() && session_policies.is_empty() {
3268 return None;
3269 }
3270 let in_caller_account = service.iam_resource_in_caller_account(aws_request);
3271 if iam_actions.is_empty() && !deferred {
3272 tracing::warn!(
3276 target: "fakecloud::iam::audit",
3277 service = %detected.service,
3278 action = %aws_request.action,
3279 principal = %principal.arn,
3280 mode = %config.iam_mode,
3281 request_id = %request_id,
3282 "root request under SCPs or session policies has no IamAction mapping; denying under strict, allowing under soft"
3283 );
3284 return config
3285 .iam_mode
3286 .is_strict()
3287 .then(|| unmapped_action_denial(principal, aws_request, request_id, detected));
3288 }
3289 for iam_action in iam_actions {
3290 let resource_account = config
3291 .resource_policy_provider
3292 .as_ref()
3293 .filter(|_| !iam_action.is_pass_role() && !in_caller_account)
3294 .and_then(|p| p.resource_owner_account(&detected.service, &iam_action.resource))
3295 .or_else(|| parse_account_from_arn(&iam_action.resource))
3296 .unwrap_or_else(|| principal.account_id.clone());
3297 let mut context = principal_condition_context(
3298 principal,
3299 resolved,
3300 service,
3301 aws_request,
3302 iam_action,
3303 detected,
3304 remote_addr,
3305 );
3306 add_global_request_keys(&mut context, principal, &resource_account, resolver);
3307 let decision = evaluator.evaluate_ceiling(
3308 principal,
3309 iam_action,
3310 &context,
3311 session_policies,
3312 scps.as_deref(),
3313 false,
3314 );
3315 if decision.is_allow() {
3316 continue;
3317 }
3318 tracing::warn!(
3319 target: "fakecloud::iam::audit",
3320 service = %detected.service,
3321 action = %iam_action.action_string(),
3322 resource = %iam_action.resource,
3323 principal = %principal.arn,
3324 decision = ?decision,
3325 mode = %config.iam_mode,
3326 request_id = %request_id,
3327 "root request denied by the account's service control policies or its session policies"
3328 );
3329 if config.iam_mode.is_strict() {
3330 let action = iam_action.action_string();
3331 let scp_reason = scps.as_deref().and_then(|scps| {
3333 let ceiling = evaluator.evaluate_ceiling(
3334 principal,
3335 iam_action,
3336 &context,
3337 &[],
3338 Some(scps),
3339 false,
3340 );
3341 scp_cause(decision, ceiling).and_then(|d| scp_denial_reason(d, &action))
3342 });
3343 return Some(access_denied_response(
3344 ¬_authorized_message(
3345 &principal.arn,
3346 &action,
3347 &iam_action.resource,
3348 scp_reason.as_deref(),
3349 None,
3350 ),
3351 request_id,
3352 detected,
3353 &aws_request.headers,
3354 ));
3355 }
3356 }
3357 None
3358}
3359
3360fn scp_denial_reason(decision: crate::auth::IamDecision, action: &str) -> Option<String> {
3365 match decision {
3366 crate::auth::IamDecision::Allow => None,
3367 crate::auth::IamDecision::ExplicitDeny => {
3368 Some("with an explicit deny in a service control policy".to_string())
3369 }
3370 crate::auth::IamDecision::ImplicitDeny => Some(format!(
3371 "because no service control policy allows the {action} action"
3372 )),
3373 }
3374}
3375
3376fn scp_cause(
3382 decision: crate::auth::IamDecision,
3383 ceiling: crate::auth::IamDecision,
3384) -> Option<crate::auth::IamDecision> {
3385 use crate::auth::IamDecision;
3386 match (decision, ceiling) {
3387 (_, IamDecision::ExplicitDeny) => Some(IamDecision::ExplicitDeny),
3388 (IamDecision::ImplicitDeny, IamDecision::ImplicitDeny) => Some(IamDecision::ImplicitDeny),
3389 _ => None,
3390 }
3391}
3392
3393fn not_authorized_message(
3398 principal_arn: &str,
3399 action: &str,
3400 resource: &str,
3401 reason: Option<&str>,
3402 encoded: Option<&str>,
3403) -> String {
3404 let mut message = format!(
3405 "User: {principal_arn} is not authorized to perform: {action} on resource: {resource}"
3406 );
3407 if let Some(reason) = reason {
3408 message.push(' ');
3409 message.push_str(reason);
3410 }
3411 if let Some(encoded) = encoded {
3412 message.push_str(" Encoded authorization failure message: ");
3413 message.push_str(encoded);
3414 }
3415 message
3416}
3417
3418fn access_denied_code(service: &str, protocol: AwsProtocol) -> &'static str {
3424 match (service, protocol) {
3425 ("sns", _) => "AuthorizationError",
3426 ("s3", _) | (_, AwsProtocol::Query) => "AccessDenied",
3427 _ => "AccessDeniedException",
3428 }
3429}
3430
3431const QUERY_COMPATIBLE_JSON_SERVICES: &[&str] = &["sqs"];
3435
3436fn access_denied_response(
3442 message: &str,
3443 request_id: &str,
3444 detected: &protocol::DetectedRequest,
3445 headers: &http::HeaderMap,
3446) -> Response<Body> {
3447 let mut resp = build_error_response(
3448 StatusCode::FORBIDDEN,
3449 access_denied_code(&detected.service, detected.protocol),
3450 message,
3451 request_id,
3452 ErrorEnvelope::for_request(detected, headers),
3453 );
3454 if detected.protocol == AwsProtocol::Json
3455 && QUERY_COMPATIBLE_JSON_SERVICES.contains(&detected.service.as_str())
3456 {
3457 resp.headers_mut().insert(
3458 "x-amzn-query-error",
3459 http::HeaderValue::from_static("AccessDenied;Sender"),
3460 );
3461 }
3462 resp
3463}
3464
3465fn unmapped_action_denial(
3468 principal: &Principal,
3469 aws_request: &AwsRequest,
3470 request_id: &str,
3471 detected: &protocol::DetectedRequest,
3472) -> Response<Body> {
3473 access_denied_response(
3474 &format!(
3475 "User: {} is not authorized to perform: {}: no IAM action mapping exists for this operation, so it cannot be authorized under strict IAM enforcement",
3476 principal.arn, aws_request.action,
3477 ),
3478 request_id,
3479 detected,
3480 &aws_request.headers,
3481 )
3482}
3483
3484#[allow(clippy::too_many_arguments)]
3500fn authorize_root_cross_account(
3501 principal: &Principal,
3502 service: &dyn crate::service::AwsService,
3503 aws_request: &AwsRequest,
3504 evaluator: &dyn IamPolicyEvaluator,
3505 config: &DispatchConfig,
3506 detected: &protocol::DetectedRequest,
3507 request_id: &str,
3508 remote_addr: Option<SocketAddr>,
3509 resolved: Option<&crate::auth::ResolvedCredential>,
3510 iam_actions: &[crate::auth::IamAction],
3511) -> Option<Response<Body>> {
3512 if service.iam_resource_in_caller_account(aws_request) {
3513 return None;
3514 }
3515 let provider = config.resource_policy_provider.as_ref()?;
3516 for iam_action in iam_actions {
3517 let Some(owner) = provider.resource_owner_account(&detected.service, &iam_action.resource)
3518 else {
3519 continue;
3520 };
3521 if owner == principal.account_id {
3522 continue;
3523 }
3524 let mut context = principal_condition_context(
3525 principal,
3526 resolved,
3527 service,
3528 aws_request,
3529 iam_action,
3530 detected,
3531 remote_addr,
3532 );
3533 add_global_request_keys(
3534 &mut context,
3535 principal,
3536 &owner,
3537 config.scp_resolver.as_deref(),
3538 );
3539 let resource_policy_json = provider
3540 .resource_policy_applies(&detected.service, iam_action)
3541 .then(|| provider.resource_policy(&detected.service, &iam_action.resource))
3542 .flatten();
3543 let decision = evaluator.evaluate_resource_policy_only(
3544 principal,
3545 iam_action,
3546 &context,
3547 resource_policy_json.as_deref(),
3548 );
3549 let explicit_deny = matches!(decision, crate::auth::IamDecision::ExplicitDeny);
3550 let acl_allows = !explicit_deny
3551 && provider.public_acl_allows(
3552 &detected.service,
3553 &iam_action.resource,
3554 iam_action.action,
3555 );
3556 let grant_allows = decision == crate::auth::IamDecision::ImplicitDeny
3560 && provider
3561 .grant_authorization(&detected.service, aws_request, iam_action, principal)
3562 .is_some();
3563 if decision.is_allow() || acl_allows || grant_allows {
3564 continue;
3565 }
3566 tracing::warn!(
3567 target: "fakecloud::iam::audit",
3568 service = %detected.service,
3569 action = %iam_action.action_string(),
3570 resource = %iam_action.resource,
3571 principal = %principal.arn,
3572 resource_account = %owner,
3573 resource_policy_present = resource_policy_json.is_some(),
3574 decision = ?decision,
3575 mode = %config.iam_mode,
3576 request_id = %request_id,
3577 "cross-account root request denied: the resource policy does not grant the action"
3578 );
3579 if config.iam_mode.is_strict() {
3580 return Some(access_denied_response(
3581 &format!(
3582 "User: {} is not authorized to perform: {} on resource: {} because no resource-based policy allows the {} action",
3583 principal.arn,
3584 iam_action.action_string(),
3585 iam_action.resource,
3586 iam_action.action_string(),
3587 ),
3588 request_id,
3589 detected,
3590 &aws_request.headers,
3591 ));
3592 }
3593 }
3594 None
3595}
3596
3597fn build_condition_context(
3598 principal: &Principal,
3599 remote_addr: Option<SocketAddr>,
3600 region: &str,
3601 secure_transport: bool,
3602) -> ConditionContext {
3603 let now = chrono::Utc::now();
3604 ConditionContext {
3605 aws_username: aws_username_from_principal(principal),
3606 aws_userid: Some(principal.user_id.clone()),
3607 aws_principal_arn: Some(principal.arn.clone()),
3608 aws_principal_account: Some(principal.account_id.clone()),
3609 aws_principal_type: Some(principal_type_label(principal.principal_type).to_string()),
3610 aws_source_ip: remote_addr.map(|sa| sa.ip()),
3611 aws_current_time: Some(now),
3612 aws_epoch_time: Some(now.timestamp()),
3613 aws_secure_transport: Some(secure_transport),
3614 aws_requested_region: Some(region.to_string()),
3615 aws_mfa_present: None,
3621 aws_mfa_age_seconds: None,
3622 aws_called_via: Vec::new(),
3623 aws_source_vpce: None,
3624 aws_source_vpc: None,
3625 aws_vpc_source_ip: None,
3626 aws_federated_provider: None,
3627 aws_token_issue_time: None,
3628 service_keys: Default::default(),
3629 resource_tags: None,
3630 request_tags: None,
3631 principal_tags: None,
3632 federated_parent_arn: None,
3633 }
3634}
3635
3636fn aws_username_from_principal(principal: &Principal) -> Option<String> {
3640 if principal.principal_type != PrincipalType::User {
3641 return None;
3642 }
3643 let after = principal.arn.rsplit_once(":user/").map(|(_, s)| s)?;
3644 Some(after.rsplit('/').next().unwrap_or(after).to_string())
3646}
3647
3648pub fn principal_type_label(t: PrincipalType) -> &'static str {
3651 match t {
3652 PrincipalType::User => "User",
3653 PrincipalType::AssumedRole => "AssumedRole",
3654 PrincipalType::FederatedUser => "FederatedUser",
3655 PrincipalType::Root => "Account",
3656 PrincipalType::Unknown => "Unknown",
3657 PrincipalType::Service => "Service",
3658 }
3659}
3660
3661fn is_secure_transport(headers: &http::HeaderMap) -> bool {
3667 headers
3668 .get("x-forwarded-proto")
3669 .and_then(|v| v.to_str().ok())
3670 .map(|s| s.eq_ignore_ascii_case("https"))
3671 .unwrap_or(false)
3672}
3673
3674trait ProtocolExt {
3675 fn error_status(&self) -> StatusCode;
3676}
3677
3678impl ProtocolExt for AwsProtocol {
3679 fn error_status(&self) -> StatusCode {
3680 StatusCode::BAD_REQUEST
3681 }
3682}
3683
3684fn names_bedrock_session(label: &str) -> bool {
3687 let decoded = label
3688 .to_ascii_lowercase()
3689 .replace("%3a", ":")
3690 .replace("%2f", "/");
3691 decoded.starts_with("arn:") && decoded.contains(":bedrock:") && decoded.contains(":session/")
3692}
3693
3694fn bedrock_agent_service_for(method: &http::Method, path: &str) -> Option<&'static str> {
3700 let first_seg = path.split('/').nth(1);
3701 if !matches!(
3702 first_seg,
3703 Some(
3704 "agents"
3705 | "knowledgebases"
3706 | "flows"
3707 | "prompts"
3708 | "tags"
3709 | "retrieveAndGenerate"
3710 | "retrieveAndGenerateStream"
3711 | "optimize-prompt"
3712 | "sessions"
3713 | "invocations"
3714 | "generate-query"
3715 | "rerank"
3716 )
3717 ) {
3718 return None;
3719 }
3720 let segs: Vec<&str> = path.split('/').collect();
3721 let is_runtime = matches!(
3722 segs.as_slice(),
3723 ["", "agents", _, "agentAliases", _, ..] | ["", "flows", _, "executions"] | ["", "flows", _, "aliases", _, "executions", ..] | ["", "knowledgebases", _, "retrieve"] | ["", "retrieveAndGenerate"]
3728 | ["", "retrieveAndGenerateStream"]
3729 | ["", "optimize-prompt"]
3730 | ["", "sessions", ..]
3731 | ["", "invocations", ..]
3732 | ["", "generate-query"]
3733 | ["", "rerank"]
3734 ) || (
3735 *method == http::Method::POST && matches!(segs.as_slice(), ["", "flows", _, "aliases", _])
3738 ) || (
3739 *method == http::Method::POST
3742 && matches!(segs.as_slice(), ["", "agents", id] if !id.is_empty())
3743 ) || path
3744 .strip_prefix("/tags/")
3748 .is_some_and(names_bedrock_session);
3749 Some(if is_runtime {
3750 "bedrock-agent-runtime"
3751 } else {
3752 "bedrock-agent"
3753 })
3754}
3755
3756#[cfg(test)]
3757mod tests {
3758
3759 fn gzip(data: &[u8]) -> Bytes {
3760 use std::io::Write;
3761 let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
3762 enc.write_all(data).unwrap();
3763 Bytes::from(enc.finish().unwrap())
3764 }
3765
3766 fn gzip_headers(extra: &[(&'static str, &str)]) -> http::HeaderMap {
3767 let mut h = http::HeaderMap::new();
3768 h.insert("content-encoding", "gzip".parse().unwrap());
3769 for (k, v) in extra {
3770 h.insert(*k, v.parse().unwrap());
3771 }
3772 h
3773 }
3774
3775 #[test]
3776 fn request_compression_decodes_gzip_for_cloudwatch() {
3777 let body = br#"{"Namespace":"App"}"#;
3778 let h = gzip_headers(&[(
3780 "x-amz-target",
3781 "GraniteServiceVersion20100801.PutMetricData",
3782 )]);
3783 assert_eq!(
3784 decode_request_compression(&h, None, gzip(body)).unwrap(),
3785 Bytes::from_static(body)
3786 );
3787 let h = gzip_headers(&[(
3789 "authorization",
3790 "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/monitoring/aws4_request, SignedHeaders=host, Signature=0",
3791 )]);
3792 let form = b"Action=PutMetricData&Namespace=App";
3793 assert_eq!(
3794 decode_request_compression(&h, None, gzip(form)).unwrap(),
3795 Bytes::from_static(form)
3796 );
3797 let detected = protocol::DetectedRequest {
3799 service: "monitoring".to_string(),
3800 action: "PutMetricData".to_string(),
3801 protocol: AwsProtocol::RpcV2Cbor,
3802 };
3803 let h = gzip_headers(&[]);
3804 assert_eq!(
3805 decode_request_compression(&h, Some(&detected), gzip(&[0xa0])).unwrap(),
3806 Bytes::from_static(&[0xa0])
3807 );
3808 let err = decode_request_compression(&h, Some(&detected), Bytes::from_static(b"nope"))
3810 .unwrap_err();
3811 assert_eq!(err.1, AwsProtocol::RpcV2Cbor);
3812 }
3813
3814 #[test]
3815 fn request_compression_leaves_other_services_alone() {
3816 let h = gzip_headers(&[(
3818 "authorization",
3819 "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/s3/aws4_request, SignedHeaders=host, Signature=0",
3820 )]);
3821 let body = gzip(b"object bytes");
3822 assert_eq!(
3823 decode_request_compression(&h, None, body.clone()).unwrap(),
3824 body
3825 );
3826 let h = http::HeaderMap::new();
3828 assert_eq!(
3829 decode_request_compression(&h, None, Bytes::from_static(b"x")).unwrap(),
3830 Bytes::from_static(b"x")
3831 );
3832 }
3833
3834 #[test]
3835 fn request_compression_services_match_the_models() {
3836 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../aws-models");
3839 let mut with_trait: Vec<String> = std::fs::read_dir(&dir)
3840 .unwrap_or_else(|e| panic!("read {}: {e}", dir.display()))
3841 .filter_map(|e| e.ok())
3842 .filter(|e| {
3843 std::fs::read_to_string(e.path())
3844 .is_ok_and(|s| s.contains("\"smithy.api#requestCompression\""))
3845 })
3846 .map(|e| e.file_name().to_string_lossy().into_owned())
3847 .collect();
3848 with_trait.sort();
3849 assert_eq!(
3850 with_trait,
3851 vec!["cloudwatch.json".to_string()],
3852 "update REQUEST_COMPRESSION_SERVICES for new @requestCompression models"
3853 );
3854 assert_eq!(REQUEST_COMPRESSION_SERVICES, &["monitoring"]);
3855 }
3856 #[test]
3857 fn query_error_namespaces_match_the_models() {
3858 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../aws-models");
3862 let mut from_models = Vec::new();
3863 for entry in std::fs::read_dir(&dir).unwrap() {
3864 let path = entry.unwrap().path();
3865 let text = std::fs::read_to_string(&path).unwrap();
3866 if !text.contains("\"aws.protocols#awsQuery\"") {
3867 continue;
3868 }
3869 let model: serde_json::Value = serde_json::from_str(&text).unwrap();
3870 let shapes = model["shapes"].as_object().unwrap();
3871 let service = shapes.values().find(|s| s["type"] == "service").unwrap();
3872 let traits = &service["traits"];
3873 if traits.get("aws.protocols#awsQuery").is_none() {
3874 continue;
3875 }
3876 let stem = path.file_stem().unwrap().to_str().unwrap().to_string();
3877 let name = if stem == "docdb" || stem == "neptune" {
3878 stem
3879 } else {
3880 traits["aws.auth#sigv4"]["name"]
3881 .as_str()
3882 .unwrap()
3883 .to_string()
3884 };
3885 let ns = traits["smithy.api#xmlNamespace"]["uri"]
3886 .as_str()
3887 .unwrap()
3888 .to_string();
3889 from_models.push((name, ns));
3890 }
3891 assert!(from_models.len() >= 10, "{from_models:?}");
3892 for (name, ns) in from_models {
3893 assert_eq!(
3894 fakecloud_aws::error::QueryErrorShape::for_service(&name).namespace,
3895 Some(ns.as_str()),
3896 "{name}: update QUERY_XML_NAMESPACES"
3897 );
3898 }
3899 }
3900
3901 #[tokio::test]
3902 async fn query_errors_carry_the_service_namespace() {
3903 let detected = protocol::DetectedRequest {
3904 service: "sqs".to_string(),
3905 action: "ListQueues".to_string(),
3906 protocol: AwsProtocol::Query,
3907 };
3908 let resp = build_error_response(
3909 StatusCode::BAD_REQUEST,
3910 "InvalidParameterValue",
3911 "bad",
3912 "rid",
3913 ErrorEnvelope::for_request(&detected, &http::HeaderMap::new()),
3914 );
3915 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
3916 .await
3917 .unwrap();
3918 let body = String::from_utf8(body.to_vec()).unwrap();
3919 assert!(
3920 body.contains(r#"<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">"#),
3921 "{body}"
3922 );
3923 assert!(body.contains("<Detail/>"), "{body}");
3924 }
3925
3926 #[test]
3927 fn bedrock_agent_paths_split_between_runtime_and_control_plane() {
3928 use http::Method;
3929 let runtime = Some("bedrock-agent-runtime");
3930 let agent = Some("bedrock-agent");
3931 for (method, path, want) in [
3932 (Method::POST, "/flows/F/aliases/A", runtime),
3933 (Method::GET, "/flows/F/aliases/A", agent),
3934 (Method::PUT, "/flows/F/aliases/A", agent),
3935 (Method::DELETE, "/flows/F/aliases/A", agent),
3936 (Method::POST, "/flows/F/aliases/A/executions", runtime),
3937 (Method::GET, "/flows/F/aliases/A/executions/E", runtime),
3938 (
3939 Method::POST,
3940 "/flows/F/aliases/A/executions/E/stop",
3941 runtime,
3942 ),
3943 (
3944 Method::GET,
3945 "/flows/F/aliases/A/executions/E/events",
3946 runtime,
3947 ),
3948 (
3949 Method::GET,
3950 "/flows/F/aliases/A/executions/E/flowsnapshot",
3951 runtime,
3952 ),
3953 (Method::GET, "/flows/F/executions", runtime),
3954 (Method::GET, "/flows/F/aliases", agent),
3955 (Method::POST, "/flows/F/versions", agent),
3956 (Method::GET, "/flows/F", agent),
3957 (
3958 Method::POST,
3959 "/agents/X/agentAliases/Y/sessions/S/text",
3960 runtime,
3961 ),
3962 (Method::GET, "/agents/X", agent),
3963 (Method::POST, "/agents/session-1", runtime),
3964 (Method::POST, "/agents/X/", agent),
3965 (Method::GET, "/agents/X/", agent),
3966 (Method::PUT, "/agents/X/", agent),
3967 (
3968 Method::POST,
3969 "/tags/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Asession%2F0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b",
3970 runtime,
3971 ),
3972 (
3973 Method::GET,
3974 "/tags/arn:aws:bedrock:us-east-1:123456789012:session/0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b",
3975 runtime,
3976 ),
3977 (
3978 Method::DELETE,
3979 "/tags/arn%3aaws%3abedrock%3aus-east-1%3a123456789012%3asession%2fabc",
3980 runtime,
3981 ),
3982 (
3983 Method::POST,
3984 "/tags/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Aagent%2FAGENT12345",
3985 agent,
3986 ),
3987 (
3988 Method::GET,
3989 "/tags/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Aflow%2FFLOW123456",
3990 agent,
3991 ),
3992 (Method::POST, "/model/m/invoke", None),
3993 ] {
3994 assert_eq!(
3995 bedrock_agent_service_for(&method, path),
3996 want,
3997 "{method} {path}"
3998 );
3999 }
4000 }
4001
4002 use super::*;
4003
4004 #[test]
4005 fn default_max_request_body_bytes_is_one_gib() {
4006 assert_eq!(DEFAULT_MAX_REQUEST_BODY_BYTES, 1024 * 1024 * 1024);
4010 }
4011
4012 #[test]
4013 fn sigv2_presigned_access_key_extracted_with_signature_and_expires() {
4014 let mut q = HashMap::new();
4015 q.insert("AWSAccessKeyId".to_string(), "AKIAEXAMPLE".to_string());
4016 q.insert("Signature".to_string(), "abc%2Bdef".to_string());
4017 q.insert("Expires".to_string(), "1700000000".to_string());
4018 assert_eq!(
4019 sigv2_presigned_access_key(&q).as_deref(),
4020 Some("AKIAEXAMPLE")
4021 );
4022 }
4023
4024 #[test]
4025 fn sigv2_presigned_access_key_none_without_signature_or_expires() {
4026 let mut q = HashMap::new();
4029 q.insert("AWSAccessKeyId".to_string(), "AKIAEXAMPLE".to_string());
4030 assert_eq!(sigv2_presigned_access_key(&q), None);
4031
4032 q.insert("Expires".to_string(), "1700000000".to_string());
4033 assert_eq!(
4034 sigv2_presigned_access_key(&q),
4035 None,
4036 "missing Signature must not qualify"
4037 );
4038 }
4039
4040 #[test]
4041 fn sigv2_query_auth_names_its_key() {
4042 let q = |pairs: &[(&str, &str)]| -> HashMap<String, String> {
4043 pairs
4044 .iter()
4045 .map(|(k, v)| (k.to_string(), v.to_string()))
4046 .collect()
4047 };
4048 let signed = q(&[
4049 ("Action", "ListQueues"),
4050 ("AWSAccessKeyId", "AKIDSIGV2"),
4051 ("Signature", "abc"),
4052 ("SignatureVersion", "2"),
4053 ("SignatureMethod", "HmacSHA256"),
4054 ("Timestamp", "2026-11-13T00:00:00Z"),
4055 ]);
4056 assert_eq!(
4057 sigv2_query_access_key(&signed).as_deref(),
4058 Some("AKIDSIGV2")
4059 );
4060 assert!(!presents_no_credential(&http::HeaderMap::new(), &signed));
4061 assert_eq!(
4064 sigv2_query_access_key(&q(&[("AWSAccessKeyId", "A"), ("Signature", "s")])),
4065 None
4066 );
4067 assert_eq!(
4068 sigv2_query_access_key(&q(&[("AWSAccessKeyId", "A"), ("SignatureVersion", "2")])),
4069 None
4070 );
4071 }
4072
4073 #[test]
4074 fn sigv2_presigned_access_key_none_for_unsigned_request() {
4075 assert_eq!(sigv2_presigned_access_key(&HashMap::new()), None);
4076 }
4077
4078 #[test]
4079 fn is_hex_sha256_accepts_real_digest_rejects_markers() {
4080 assert!(is_hex_sha256(&sha256_hex_lower(b"hello")));
4082 assert!(is_hex_sha256(
4083 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
4084 ));
4085 assert!(!is_hex_sha256("UNSIGNED-PAYLOAD"));
4087 assert!(!is_hex_sha256("STREAMING-AWS4-HMAC-SHA256-PAYLOAD"));
4088 assert!(!is_hex_sha256("STREAMING-UNSIGNED-PAYLOAD-TRAILER"));
4089 assert!(!is_hex_sha256("abc123"));
4091 assert!(!is_hex_sha256(
4092 "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
4093 ));
4094 }
4095
4096 #[test]
4097 fn sha256_hex_lower_matches_known_vectors() {
4098 assert_eq!(
4100 sha256_hex_lower(b""),
4101 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
4102 );
4103 assert_eq!(
4104 sha256_hex_lower(b"abc"),
4105 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
4106 );
4107 assert_eq!(sha256_hex_lower(b"abc").len(), 64);
4108 }
4109
4110 #[test]
4111 fn global_request_keys_cover_resource_account_and_org() {
4112 struct Org;
4113 impl crate::auth::ScpResolver for Org {
4114 fn scps_for(&self, _: &Principal) -> Option<Vec<String>> {
4115 None
4116 }
4117 fn principal_org(&self, account: &str) -> Option<(String, String)> {
4118 (account == "111111111111")
4119 .then(|| ("o-abc".to_string(), "o-abc/r-1/ou-2/".to_string()))
4120 }
4121 }
4122 let principal = |account: &str| Principal {
4123 arn: format!("arn:aws:iam::{account}:user/u"),
4124 user_id: "AIDAU".into(),
4125 account_id: account.into(),
4126 principal_type: PrincipalType::User,
4127 source_identity: None,
4128 tags: None,
4129 };
4130 let mut ctx = ConditionContext::default();
4131 add_global_request_keys(
4132 &mut ctx,
4133 &principal("111111111111"),
4134 "222222222222",
4135 Some(&Org),
4136 );
4137 assert_eq!(
4138 ctx.lookup("aws:ResourceAccount"),
4139 Some(vec!["222222222222".into()])
4140 );
4141 assert_eq!(ctx.lookup("aws:PrincipalOrgID"), Some(vec!["o-abc".into()]));
4142 assert_eq!(
4143 ctx.lookup("aws:PrincipalOrgPaths"),
4144 Some(vec!["o-abc/r-1/ou-2/".into()])
4145 );
4146 let mut ctx = ConditionContext::default();
4148 add_global_request_keys(
4149 &mut ctx,
4150 &principal("333333333333"),
4151 "333333333333",
4152 Some(&Org),
4153 );
4154 assert_eq!(ctx.lookup("aws:PrincipalOrgID"), None);
4155 assert_eq!(
4156 ctx.lookup("aws:ResourceAccount"),
4157 Some(vec!["333333333333".into()])
4158 );
4159 }
4160
4161 #[test]
4162 fn unresolved_credential_distinguishes_expired_from_invalid() {
4163 struct Expired;
4164 impl CredentialResolver for Expired {
4165 fn resolve(&self, _: &str) -> Option<crate::auth::ResolvedCredential> {
4166 None
4167 }
4168 fn is_expired(&self, akid: &str) -> bool {
4169 akid == "ASIAEXPIRED"
4170 }
4171 }
4172 let mut cfg = DispatchConfig::new("us-east-1", "123456789012");
4173 cfg.credential_resolver = Some(Arc::new(Expired));
4174 let detected = protocol::DetectedRequest {
4175 service: "sts".into(),
4176 action: "GetCallerIdentity".into(),
4177 protocol: AwsProtocol::Query,
4178 };
4179 let code = |akid: &str| {
4180 let resp = unresolved_credential_response(
4181 &cfg,
4182 akid,
4183 "r",
4184 ErrorEnvelope::for_request(&detected, &http::HeaderMap::new()),
4185 );
4186 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
4187 resp.headers()["x-amz-error-code"]
4188 .to_str()
4189 .unwrap()
4190 .to_string()
4191 };
4192 assert_eq!(code("ASIAEXPIRED"), "ExpiredToken");
4193 assert_eq!(code("AKIAUNKNOWN"), "InvalidClientTokenId");
4194 }
4195
4196 #[test]
4197 fn dispatch_config_new_defaults_to_off() {
4198 let cfg = DispatchConfig::new("us-east-1", "123456789012");
4199 assert_eq!(cfg.region, "us-east-1");
4200 assert_eq!(cfg.account_id, "123456789012");
4201 assert!(!cfg.verify_sigv4);
4202 assert_eq!(cfg.iam_mode, IamMode::Off);
4203 }
4204
4205 struct IdentityOnly {
4209 identity: crate::auth::IamDecision,
4210 scp_allows: bool,
4211 session_allows: bool,
4212 boundary_allows: bool,
4213 }
4214
4215 impl IamPolicyEvaluator for IdentityOnly {
4216 fn evaluate(
4217 &self,
4218 _: &Principal,
4219 _: &crate::auth::IamAction,
4220 _: &ConditionContext,
4221 _: &[String],
4222 _: Option<&[String]>,
4223 ) -> crate::auth::IamDecision {
4224 self.identity
4225 }
4226
4227 fn evaluate_ceiling(
4228 &self,
4229 _: &Principal,
4230 _: &crate::auth::IamAction,
4231 _: &ConditionContext,
4232 session_policies: &[String],
4233 scps: Option<&[String]>,
4234 with_boundary: bool,
4235 ) -> crate::auth::IamDecision {
4236 let scp_ok = scps.is_none() || self.scp_allows;
4237 let session_ok = session_policies.is_empty() || self.session_allows;
4238 let boundary_ok = !with_boundary || self.boundary_allows;
4239 if scp_ok && session_ok && boundary_ok {
4240 crate::auth::IamDecision::Allow
4241 } else {
4242 crate::auth::IamDecision::ImplicitDeny
4243 }
4244 }
4245
4246 fn evaluate_with_resource_policy(
4247 &self,
4248 _: &Principal,
4249 _: &crate::auth::IamAction,
4250 _: &ConditionContext,
4251 _: Option<&str>,
4252 _: &str,
4253 _: &[String],
4254 _: Option<&[String]>,
4255 ) -> crate::auth::IamDecision {
4256 crate::auth::IamDecision::ImplicitDeny
4257 }
4258 }
4259
4260 struct Grants(Option<crate::auth::GrantAuthorization>);
4262
4263 impl ResourcePolicyProvider for Grants {
4264 fn resource_policy(&self, _: &str, _: &str) -> Option<String> {
4265 None
4266 }
4267
4268 fn grant_authorization(
4269 &self,
4270 _: &str,
4271 _: &AwsRequest,
4272 _: &crate::auth::IamAction,
4273 _: &Principal,
4274 ) -> Option<crate::auth::GrantAuthorization> {
4275 self.0
4276 }
4277 }
4278
4279 #[test]
4282 fn grants_authorize_their_grantee_and_delegate_to_its_account() {
4283 use crate::auth::{GrantAuthorization, IamDecision};
4284 let request = AwsRequest {
4285 service: "kms".into(),
4286 action: "Decrypt".into(),
4287 region: "us-east-1".into(),
4288 account_id: "222222222222".into(),
4289 request_id: "rid".into(),
4290 headers: http::HeaderMap::new(),
4291 query_params: HashMap::new(),
4292 body: Bytes::new(),
4293 body_stream: parking_lot::Mutex::new(None),
4294 path_segments: Vec::new(),
4295 raw_path: "/".into(),
4296 raw_query: String::new(),
4297 method: http::Method::POST,
4298 is_query_protocol: false,
4299 access_key_id: None,
4300 principal: None,
4301 };
4302 let action = crate::auth::IamAction {
4303 service: "kms",
4304 action: "Decrypt",
4305 resource: "arn:aws:kms:us-east-1:111111111111:key/k".into(),
4306 };
4307 let principal = Principal {
4308 arn: "arn:aws:iam::222222222222:user/u".into(),
4309 user_id: "AIDA".into(),
4310 account_id: "222222222222".into(),
4311 principal_type: PrincipalType::User,
4312 source_identity: None,
4313 tags: None,
4314 };
4315 let session = vec!["{}".to_string()];
4316 let scps = vec!["{}".to_string()];
4317 let check = |grant: Option<GrantAuthorization>,
4318 evaluator: IdentityOnly,
4319 provided: bool,
4320 session_policies: &[String],
4321 scps: Option<&[String]>| {
4322 let provider = Grants(grant);
4323 grant_authorizes(
4324 provided.then_some(&provider as &dyn ResourcePolicyProvider),
4325 &evaluator,
4326 "kms",
4327 &request,
4328 &action,
4329 &principal,
4330 &ConditionContext::default(),
4331 session_policies,
4332 scps,
4333 )
4334 };
4335 let open = |identity| IdentityOnly {
4336 identity,
4337 scp_allows: true,
4338 session_allows: true,
4339 boundary_allows: true,
4340 };
4341 let authorizes =
4342 |grant, identity, provided| check(grant, open(identity), provided, &[], None);
4343 let grantee = Some(GrantAuthorization::Grantee);
4344 let role = Some(GrantAuthorization::GranteeRole);
4345 let account = Some(GrantAuthorization::Account);
4346 assert!(authorizes(grantee, IamDecision::ImplicitDeny, true));
4347 assert!(authorizes(role, IamDecision::ImplicitDeny, true));
4348 assert!(authorizes(account, IamDecision::Allow, true));
4349 assert!(!authorizes(account, IamDecision::ImplicitDeny, true));
4350 assert!(!authorizes(None, IamDecision::Allow, true));
4351 assert!(!authorizes(grantee, IamDecision::Allow, false));
4352
4353 let scp_denies = || IdentityOnly {
4355 identity: IamDecision::ImplicitDeny,
4356 scp_allows: false,
4357 session_allows: true,
4358 boundary_allows: true,
4359 };
4360 for grant in [grantee, role] {
4361 assert!(!check(grant, scp_denies(), true, &[], Some(&scps)));
4362 }
4363 let session_denies = || IdentityOnly {
4366 identity: IamDecision::ImplicitDeny,
4367 scp_allows: true,
4368 session_allows: false,
4369 boundary_allows: true,
4370 };
4371 assert!(!check(role, session_denies(), true, &session, None));
4372 assert!(check(grantee, session_denies(), true, &session, None));
4373 let boundary_denies = || IdentityOnly {
4376 identity: IamDecision::ImplicitDeny,
4377 scp_allows: true,
4378 session_allows: true,
4379 boundary_allows: false,
4380 };
4381 assert!(!check(role, boundary_denies(), true, &[], None));
4382 assert!(check(grantee, boundary_denies(), true, &[], None));
4383 }
4384
4385 #[test]
4386 fn aws_username_strips_iam_path_for_users() {
4387 let p = Principal {
4388 arn: "arn:aws:iam::123456789012:user/engineering/alice".into(),
4389 user_id: "AIDAALICE".into(),
4390 account_id: "123456789012".into(),
4391 principal_type: PrincipalType::User,
4392 source_identity: None,
4393 tags: None,
4394 };
4395 assert_eq!(aws_username_from_principal(&p), Some("alice".into()));
4396 }
4397
4398 #[test]
4399 fn aws_username_unset_for_assumed_role() {
4400 let p = Principal {
4401 arn: "arn:aws:sts::123456789012:assumed-role/ops/session".into(),
4402 user_id: "AROAOPS:session".into(),
4403 account_id: "123456789012".into(),
4404 principal_type: PrincipalType::AssumedRole,
4405 source_identity: None,
4406 tags: None,
4407 };
4408 assert_eq!(aws_username_from_principal(&p), None);
4409 }
4410
4411 #[test]
4412 fn principal_type_label_matches_aws_casing() {
4413 assert_eq!(principal_type_label(PrincipalType::User), "User");
4414 assert_eq!(
4415 principal_type_label(PrincipalType::AssumedRole),
4416 "AssumedRole"
4417 );
4418 assert_eq!(principal_type_label(PrincipalType::Root), "Account");
4419 }
4420
4421 #[test]
4422 fn build_condition_context_populates_global_keys() {
4423 let p = Principal {
4424 arn: "arn:aws:iam::123456789012:user/alice".into(),
4425 user_id: "AIDAALICE".into(),
4426 account_id: "123456789012".into(),
4427 principal_type: PrincipalType::User,
4428 source_identity: None,
4429 tags: None,
4430 };
4431 let addr: SocketAddr = "10.0.0.1:54321".parse().unwrap();
4432 let ctx = build_condition_context(&p, Some(addr), "us-east-1", false);
4433 assert_eq!(ctx.aws_username.as_deref(), Some("alice"));
4434 assert_eq!(ctx.aws_userid.as_deref(), Some("AIDAALICE"));
4435 assert_eq!(
4436 ctx.aws_principal_arn.as_deref(),
4437 Some("arn:aws:iam::123456789012:user/alice")
4438 );
4439 assert_eq!(ctx.aws_principal_account.as_deref(), Some("123456789012"));
4440 assert_eq!(ctx.aws_principal_type.as_deref(), Some("User"));
4441 assert_eq!(
4442 ctx.aws_source_ip.map(|i| i.to_string()).as_deref(),
4443 Some("10.0.0.1")
4444 );
4445 assert_eq!(ctx.aws_requested_region.as_deref(), Some("us-east-1"));
4446 assert_eq!(ctx.aws_secure_transport, Some(false));
4447 assert!(ctx.aws_current_time.is_some());
4448 assert!(ctx.aws_epoch_time.is_some());
4449 }
4450
4451 #[test]
4452 fn is_secure_transport_reads_x_forwarded_proto() {
4453 let mut headers = http::HeaderMap::new();
4454 headers.insert("x-forwarded-proto", "https".parse().unwrap());
4455 assert!(is_secure_transport(&headers));
4456 headers.insert("x-forwarded-proto", "http".parse().unwrap());
4457 assert!(!is_secure_transport(&headers));
4458 let empty = http::HeaderMap::new();
4459 assert!(!is_secure_transport(&empty));
4460 }
4461
4462 #[test]
4463 fn parse_account_from_arn_extracts_standard_shapes() {
4464 assert_eq!(
4465 parse_account_from_arn("arn:aws:sqs:us-east-1:123456789012:queue"),
4466 Some("123456789012".to_string())
4467 );
4468 assert_eq!(
4469 parse_account_from_arn("arn:aws:iam::123456789012:user/alice"),
4470 Some("123456789012".to_string())
4471 );
4472 }
4473
4474 #[test]
4475 fn parse_account_from_arn_returns_none_for_s3_empty_account() {
4476 assert_eq!(parse_account_from_arn("arn:aws:s3:::my-bucket"), None);
4478 assert_eq!(
4479 parse_account_from_arn("arn:aws:s3:::my-bucket/path/to/key"),
4480 None
4481 );
4482 }
4483
4484 #[test]
4485 fn parse_account_from_arn_returns_none_for_malformed() {
4486 assert_eq!(parse_account_from_arn(""), None);
4487 assert_eq!(parse_account_from_arn("not-an-arn"), None);
4488 assert_eq!(parse_account_from_arn("arn:aws:sqs:us-east-1"), None);
4489 assert_eq!(parse_account_from_arn("arn:aws:sqs"), None);
4490 }
4491
4492 #[test]
4493 fn extract_region_from_user_agent_finds_region_segment() {
4494 let mut headers = http::HeaderMap::new();
4495 headers.insert(
4496 "user-agent",
4497 "aws-sdk-rust/1.0 os/linux region/eu-central-1"
4498 .parse()
4499 .unwrap(),
4500 );
4501 assert_eq!(
4502 extract_region_from_user_agent(&headers),
4503 Some("eu-central-1".to_string())
4504 );
4505 }
4506
4507 #[test]
4508 fn extract_region_from_user_agent_none_without_header() {
4509 let headers = http::HeaderMap::new();
4510 assert_eq!(extract_region_from_user_agent(&headers), None);
4511 }
4512
4513 #[test]
4514 fn extract_region_from_user_agent_ignores_empty_region() {
4515 let mut headers = http::HeaderMap::new();
4516 headers.insert("user-agent", "aws-sdk-java region/".parse().unwrap());
4517 assert_eq!(extract_region_from_user_agent(&headers), None);
4518 }
4519
4520 #[test]
4521 fn extract_region_from_user_agent_none_when_no_region_marker() {
4522 let mut headers = http::HeaderMap::new();
4523 headers.insert("user-agent", "curl/7.79.1".parse().unwrap());
4524 assert_eq!(extract_region_from_user_agent(&headers), None);
4525 }
4526
4527 #[test]
4528 fn aws_username_none_for_root() {
4529 let p = Principal {
4530 arn: "arn:aws:iam::123456789012:root".into(),
4531 user_id: "123456789012".into(),
4532 account_id: "123456789012".into(),
4533 principal_type: PrincipalType::Root,
4534 source_identity: None,
4535 tags: None,
4536 };
4537 assert_eq!(aws_username_from_principal(&p), None);
4538 }
4539
4540 #[test]
4541 fn aws_username_bare_no_path() {
4542 let p = Principal {
4543 arn: "arn:aws:iam::123456789012:user/bob".into(),
4544 user_id: "AIDABOB".into(),
4545 account_id: "123456789012".into(),
4546 principal_type: PrincipalType::User,
4547 source_identity: None,
4548 tags: None,
4549 };
4550 assert_eq!(aws_username_from_principal(&p), Some("bob".into()));
4551 }
4552
4553 #[test]
4554 fn principal_type_label_covers_federated_and_unknown() {
4555 assert_eq!(
4556 principal_type_label(PrincipalType::FederatedUser),
4557 "FederatedUser"
4558 );
4559 assert_eq!(principal_type_label(PrincipalType::Unknown), "Unknown");
4560 }
4561
4562 #[test]
4563 fn build_condition_context_marks_secure_when_flag_set() {
4564 let p = Principal {
4565 arn: "arn:aws:iam::123456789012:user/alice".into(),
4566 user_id: "AIDAALICE".into(),
4567 account_id: "123456789012".into(),
4568 principal_type: PrincipalType::User,
4569 source_identity: None,
4570 tags: None,
4571 };
4572 let ctx = build_condition_context(&p, None, "us-west-2", true);
4573 assert_eq!(ctx.aws_secure_transport, Some(true));
4574 assert!(ctx.aws_source_ip.is_none());
4575 assert_eq!(ctx.aws_requested_region.as_deref(), Some("us-west-2"));
4576 }
4577
4578 #[test]
4579 fn is_secure_transport_case_insensitive() {
4580 let mut headers = http::HeaderMap::new();
4581 headers.insert("x-forwarded-proto", "HTTPS".parse().unwrap());
4582 assert!(is_secure_transport(&headers));
4583 }
4584
4585 #[test]
4586 fn is_secure_transport_non_ascii_bytes_false() {
4587 let mut headers = http::HeaderMap::new();
4588 headers.insert(
4589 "x-forwarded-proto",
4590 http::HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap(),
4591 );
4592 assert!(!is_secure_transport(&headers));
4593 }
4594
4595 #[test]
4596 fn protocol_ext_error_status_is_bad_request() {
4597 assert_eq!(AwsProtocol::Query.error_status(), StatusCode::BAD_REQUEST);
4598 assert_eq!(AwsProtocol::Json.error_status(), StatusCode::BAD_REQUEST);
4599 assert_eq!(AwsProtocol::Rest.error_status(), StatusCode::BAD_REQUEST);
4600 assert_eq!(
4601 AwsProtocol::RestJson.error_status(),
4602 StatusCode::BAD_REQUEST
4603 );
4604 }
4605
4606 #[test]
4607 fn build_error_response_json_has_json_content_type() {
4608 let resp = build_error_response(
4609 StatusCode::BAD_REQUEST,
4610 "TestCode",
4611 "test msg",
4612 "req-1",
4613 AwsProtocol::Json,
4614 );
4615 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
4616 let ct = resp
4617 .headers()
4618 .get("content-type")
4619 .unwrap()
4620 .to_str()
4621 .unwrap();
4622 assert!(ct.contains("json"));
4623 let rid = resp
4624 .headers()
4625 .get("x-amzn-requestid")
4626 .unwrap()
4627 .to_str()
4628 .unwrap();
4629 assert_eq!(rid, "req-1");
4630 }
4631
4632 #[test]
4633 fn build_error_response_rest_returns_xml_content_type() {
4634 let resp = build_error_response(
4635 StatusCode::NOT_FOUND,
4636 "NoSuchBucket",
4637 "bucket missing",
4638 "req-2",
4639 AwsProtocol::Rest,
4640 );
4641 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
4642 let ct = resp
4643 .headers()
4644 .get("content-type")
4645 .unwrap()
4646 .to_str()
4647 .unwrap();
4648 assert!(ct.contains("xml"));
4649 }
4650
4651 #[tokio::test]
4655 async fn lambda_errors_carry_x_amzn_errortype_and_lambdas_body() {
4656 let detected = protocol::DetectedRequest {
4657 service: "lambda".to_string(),
4658 action: String::new(),
4659 protocol: AwsProtocol::RestJson,
4660 };
4661 let resp = build_error_response(
4662 StatusCode::NOT_FOUND,
4663 "ResourceNotFoundException",
4664 "Function not found: f",
4665 "req-3",
4666 ErrorEnvelope::for_request(&detected, &http::HeaderMap::new()),
4667 );
4668 assert_eq!(
4669 resp.headers()["x-amzn-errortype"],
4670 "ResourceNotFoundException"
4671 );
4672 assert_eq!(resp.headers()["content-type"], "application/json");
4673 let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
4674 .await
4675 .unwrap();
4676 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
4677 assert_eq!(
4678 body,
4679 serde_json::json!({"Type": "User", "Message": "Function not found: f"})
4680 );
4681
4682 let other = protocol::DetectedRequest {
4683 service: "apigateway".to_string(),
4684 action: String::new(),
4685 protocol: AwsProtocol::RestJson,
4686 };
4687 let resp = build_error_response(
4688 StatusCode::NOT_FOUND,
4689 "NotFoundException",
4690 "Invalid API identifier specified",
4691 "req-4",
4692 ErrorEnvelope::for_request(&other, &http::HeaderMap::new()),
4693 );
4694 assert_eq!(resp.headers()["x-amzn-errortype"], "NotFoundException");
4695 let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
4696 .await
4697 .unwrap();
4698 let body: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
4699 assert_eq!(body["__type"], "NotFoundException");
4700 }
4701
4702 #[test]
4703 fn lambda_refuses_an_unknown_access_key_as_unrecognized_client() {
4704 let lambda = protocol::DetectedRequest {
4705 service: "lambda".to_string(),
4706 action: String::new(),
4707 protocol: AwsProtocol::RestJson,
4708 };
4709 let envelope = ErrorEnvelope::for_request(&lambda, &http::HeaderMap::new());
4710 assert_eq!(
4711 invalid_token_error(&envelope),
4712 (
4713 "UnrecognizedClientException",
4714 "The security token included in the request is invalid."
4715 )
4716 );
4717 assert_eq!(
4718 invalid_token_error(&ErrorEnvelope::from(AwsProtocol::Query)).0,
4719 "InvalidClientTokenId"
4720 );
4721 }
4722
4723 fn rest_detected(service: &str) -> protocol::DetectedRequest {
4724 protocol::DetectedRequest {
4725 service: service.to_string(),
4726 action: String::new(),
4727 protocol: AwsProtocol::Rest,
4728 }
4729 }
4730
4731 async fn body_string(resp: Response<Body>) -> String {
4732 let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
4733 .await
4734 .unwrap();
4735 String::from_utf8(bytes.to_vec()).unwrap()
4736 }
4737
4738 #[tokio::test]
4739 async fn cloudfront_and_route53_errors_use_error_response_wrapper() {
4740 for (service, ns) in [
4741 (
4742 "cloudfront",
4743 "http://cloudfront.amazonaws.com/doc/2020-05-31/",
4744 ),
4745 ("route53", "https://route53.amazonaws.com/doc/2013-04-01/"),
4746 ] {
4747 let resp = build_error_response(
4748 StatusCode::NOT_FOUND,
4749 "NoSuchThing",
4750 "missing",
4751 "req-w",
4752 ErrorEnvelope::for_request(&rest_detected(service), &http::HeaderMap::new()),
4753 );
4754 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
4755 assert_eq!(
4756 resp.headers().get("x-amz-error-code").unwrap(),
4757 "NoSuchThing"
4758 );
4759 let body = body_string(resp).await;
4760 assert!(
4761 body.contains(&format!(
4762 "<ErrorResponse xmlns=\"{ns}\"><Error><Type>Sender</Type>\
4763 <Code>NoSuchThing</Code><Message>missing</Message></Error>\
4764 <RequestId>req-w</RequestId></ErrorResponse>"
4765 )),
4766 "{service}: {body}"
4767 );
4768 }
4769 }
4770
4771 #[tokio::test]
4772 async fn s3_errors_keep_bare_error_document() {
4773 let resp = build_error_response(
4774 StatusCode::NOT_FOUND,
4775 "NoSuchBucket",
4776 "missing",
4777 "req-s3",
4778 ErrorEnvelope::for_request(&rest_detected("s3"), &http::HeaderMap::new()),
4779 );
4780 let body = body_string(resp).await;
4781 assert!(!body.contains("<ErrorResponse"), "{body}");
4782 assert!(body.contains("<Error>"), "{body}");
4783 assert!(body.contains("<Code>NoSuchBucket</Code>"), "{body}");
4784 }
4785
4786 #[tokio::test]
4787 async fn s3_control_errors_use_error_response_wrapper() {
4788 let mut headers = http::HeaderMap::new();
4789 headers.insert(
4790 "host",
4791 "000000000000.s3-control.us-east-1.amazonaws.com"
4792 .parse()
4793 .unwrap(),
4794 );
4795 let resp = build_error_response(
4796 StatusCode::NOT_FOUND,
4797 "NoSuchAccessPoint",
4798 "missing",
4799 "req-ctl",
4800 ErrorEnvelope::for_request(&rest_detected("s3"), &headers),
4801 );
4802 assert_eq!(
4803 resp.headers().get("x-amz-error-code").unwrap(),
4804 "NoSuchAccessPoint"
4805 );
4806 let body = body_string(resp).await;
4807 assert!(
4808 body.contains(
4809 "<ErrorResponse><Error><Code>NoSuchAccessPoint</Code>\
4810 <Message>missing</Message></Error>\
4811 <RequestId>req-ctl</RequestId></ErrorResponse>"
4812 ),
4813 "{body}"
4814 );
4815 }
4816
4817 #[test]
4818 fn build_error_response_query_returns_xml() {
4819 let resp = build_error_response(
4820 StatusCode::BAD_REQUEST,
4821 "InvalidParameter",
4822 "bad param",
4823 "req-3",
4824 AwsProtocol::Query,
4825 );
4826 let ct = resp
4827 .headers()
4828 .get("content-type")
4829 .unwrap()
4830 .to_str()
4831 .unwrap();
4832 assert!(ct.contains("xml"));
4833 }
4834
4835 #[test]
4840 fn build_error_response_with_multiline_message_does_not_panic() {
4841 let resp = build_error_response(
4842 StatusCode::INTERNAL_SERVER_ERROR,
4843 "ServiceException",
4844 "Lambda execution failed: container failed to start: docker start failed: \
4845 Error: unable to start container \"abc\": \
4846 failed to create new hosts file:\nhost-gateway is empty\n",
4847 "req-multi",
4848 AwsProtocol::Json,
4849 );
4850 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
4851 let msg = resp
4852 .headers()
4853 .get("x-amz-error-message")
4854 .expect("x-amz-error-message must be set even when input contains newlines")
4855 .to_str()
4856 .unwrap();
4857 assert!(!msg.contains('\n'));
4858 assert!(!msg.contains('\r'));
4859 assert!(msg.contains("Lambda execution failed"));
4860 assert!(msg.contains("host-gateway is empty"));
4861 }
4862
4863 #[test]
4864 fn build_error_response_with_control_chars_strips_them() {
4865 let resp = build_error_response(
4866 StatusCode::BAD_REQUEST,
4867 "Code\twith\ttabs",
4868 "msg\x00with\x01nulls",
4869 "req-ctrl",
4870 AwsProtocol::Json,
4871 );
4872 let code = resp
4873 .headers()
4874 .get("x-amz-error-code")
4875 .unwrap()
4876 .to_str()
4877 .unwrap();
4878 let msg = resp
4879 .headers()
4880 .get("x-amz-error-message")
4881 .unwrap()
4882 .to_str()
4883 .unwrap();
4884 assert!(!code.contains('\t'));
4885 assert!(!msg.contains('\x00'));
4886 assert!(!msg.contains('\x01'));
4887 }
4888
4889 #[test]
4890 fn sanitize_header_value_truncates_long_input() {
4891 let huge = "x".repeat(5_000);
4892 let out = sanitize_header_value(&huge);
4893 assert!(out.len() <= 1024);
4894 }
4895
4896 #[test]
4897 fn sanitize_header_value_collapses_consecutive_control_runs() {
4898 let out = sanitize_header_value("a\n\n\n\rb");
4899 assert_eq!(out, "a b");
4900 }
4901
4902 #[test]
4903 fn anonymous_s3_probe_finds_a_bucket_on_a_china_server() {
4904 struct RecordingProvider(parking_lot::Mutex<Vec<String>>);
4908 impl crate::auth::ResourcePolicyProvider for RecordingProvider {
4909 fn resource_policy(&self, _service: &str, _resource_arn: &str) -> Option<String> {
4910 None
4911 }
4912 fn resource_owner_account(&self, _service: &str, resource_arn: &str) -> Option<String> {
4913 self.0.lock().push(resource_arn.to_string());
4914 resource_arn
4915 .strip_prefix("arn:aws:s3:::")
4916 .filter(|bucket| *bucket == "my-bucket")
4917 .map(|_| "000000000000".to_string())
4918 }
4919 }
4920 let provider = Arc::new(RecordingProvider(parking_lot::Mutex::new(Vec::new())));
4921 let mut cfg = DispatchConfig::new("cn-north-1", "000000000000");
4922 cfg.resource_policy_provider = Some(provider.clone());
4923 let uri: http::Uri = "/my-bucket/key.txt".parse().unwrap();
4924 assert_eq!(
4925 anonymous_s3_bucket(&uri, &cfg),
4926 Some("my-bucket".to_string())
4927 );
4928 assert_eq!(
4929 *provider.0.lock(),
4930 vec!["arn:aws:s3:::my-bucket".to_string()]
4931 );
4932 }
4933
4934 fn unsigned_routing_registry(with_s3: bool) -> ServiceRegistry {
4938 struct Named(&'static str);
4939 #[async_trait::async_trait]
4940 impl crate::service::AwsService for Named {
4941 fn service_name(&self) -> &str {
4942 self.0
4943 }
4944 async fn handle(
4945 &self,
4946 _req: AwsRequest,
4947 ) -> Result<crate::service::AwsResponse, crate::service::AwsServiceError> {
4948 unreachable!("routing only")
4949 }
4950 fn supported_actions(&self) -> &[&str] {
4951 &[]
4952 }
4953 fn claim_unrouted_request(
4954 &self,
4955 _method: &http::Method,
4956 raw_path: &str,
4957 headers: &http::HeaderMap,
4958 account_id: &str,
4959 ) -> Option<crate::service::DataPlaneClaim> {
4960 assert_eq!(account_id, "000000000000");
4961 if self.0 != "apigateway" {
4962 None
4963 } else if is_execute_api_host(headers) {
4964 Some(crate::service::DataPlaneClaim::Host)
4965 } else if raw_path.starts_with("/prod/") {
4966 Some(crate::service::DataPlaneClaim::Path)
4967 } else {
4968 None
4969 }
4970 }
4971 }
4972 let mut registry = ServiceRegistry::new();
4973 registry.register(Arc::new(Named("apigateway")));
4974 if with_s3 {
4975 registry.register(Arc::new(Named("s3")));
4976 }
4977 registry
4978 }
4979
4980 #[test]
4981 fn unsigned_path_style_requests_go_to_s3_unless_api_gateway_claims_them() {
4982 struct Buckets(&'static [&'static str]);
4984 impl crate::auth::ResourcePolicyProvider for Buckets {
4985 fn resource_policy(&self, _service: &str, _resource_arn: &str) -> Option<String> {
4986 None
4987 }
4988 fn resource_owner_account(&self, _service: &str, resource_arn: &str) -> Option<String> {
4989 resource_arn
4990 .strip_prefix("arn:aws:s3:::")
4991 .filter(|b| self.0.contains(b))
4992 .map(|_| "000000000000".to_string())
4993 }
4994 }
4995 let registry = unsigned_routing_registry(true);
4996 let mut cfg = DispatchConfig::new("us-east-1", "000000000000");
4997 cfg.resource_policy_provider = Some(Arc::new(Buckets(&[])));
4998 let target = |cfg: &DispatchConfig,
4999 method: http::Method,
5000 uri: &str,
5001 headers: &http::HeaderMap,
5002 query: &[(&str, &str)]| {
5003 let query: HashMap<String, String> = query
5004 .iter()
5005 .map(|(k, v)| (k.to_string(), v.to_string()))
5006 .collect();
5007 unrouted_target(
5008 &method,
5009 &uri.parse().unwrap(),
5010 headers,
5011 &query,
5012 ®istry,
5013 cfg,
5014 )
5015 };
5016 let is_s3 =
5017 |method: http::Method, uri: &str, headers: &http::HeaderMap, query: &[(&str, &str)]| {
5018 target(&cfg, method, uri, headers, query) == Some("s3")
5019 };
5020 let none = http::HeaderMap::new();
5021 assert!(is_s3(http::Method::GET, "/ghost-bucket/k.txt", &none, &[]));
5023 assert!(is_s3(http::Method::PUT, "/ghost-bucket/k.txt", &none, &[]));
5024 assert!(is_s3(http::Method::PUT, "/ghost-bucket", &none, &[]));
5025 assert!(is_s3(http::Method::GET, "/", &none, &[]));
5026 assert!(is_s3(
5028 http::Method::GET,
5029 "/ghost-bucket",
5030 &none,
5031 &[("list-type", "2")]
5032 ));
5033 assert!(!is_s3(http::Method::GET, "/prod/items", &none, &[]));
5035 assert!(!is_s3(http::Method::GET, "/_fakecloud/health", &none, &[]));
5037 let mut signed = http::HeaderMap::new();
5040 signed.insert(
5041 http::header::AUTHORIZATION,
5042 "AWS4-HMAC-SHA256 Credential=AKID/20240101/us-east-1/execute-api/aws4_request"
5043 .parse()
5044 .unwrap(),
5045 );
5046 assert!(!is_s3(http::Method::GET, "/ghost-bucket/k", &signed, &[]));
5047 assert!(!is_s3(
5048 http::Method::GET,
5049 "/ghost-bucket/k",
5050 &none,
5051 &[(
5052 "X-Amz-Credential",
5053 "AKID/20240101/us-east-1/execute-api/aws4_request"
5054 )]
5055 ));
5056 for (query, beats_stage) in [
5060 (&[("AWSAccessKeyId", "AKID"), ("Signature", "x")][..], true),
5061 (&[("Signature", "x"), ("Expires", "1")][..], true),
5062 (
5063 &[(
5064 "X-Amz-Credential",
5065 "AKID/20240101/us-east-1/s3/aws4_request",
5066 )][..],
5067 true,
5068 ),
5069 (&[("AWSAccessKeyId", "AKID")][..], false),
5070 (&[("Signature", "abc")][..], false),
5071 (&[("X-Amz-Algorithm", "AWS4-HMAC-SHA256")][..], false),
5072 (&[("X-Amz-Signature", "abc")][..], false),
5073 ] {
5074 assert!(is_s3(http::Method::GET, "/ghost-bucket/k", &none, query));
5075 assert_eq!(
5076 is_s3(http::Method::GET, "/prod/items", &none, query),
5077 beats_stage,
5078 "{query:?}"
5079 );
5080 }
5081 let exec = {
5082 let mut h = http::HeaderMap::new();
5083 h.insert(
5084 "host",
5085 "abc123.execute-api.us-east-1.amazonaws.com"
5086 .parse()
5087 .unwrap(),
5088 );
5089 h
5090 };
5091 assert_eq!(
5092 target(
5093 &cfg,
5094 http::Method::GET,
5095 "/ghost-bucket/k",
5096 &exec,
5097 &[("AWSAccessKeyId", "AKID")]
5098 ),
5099 Some("apigateway")
5100 );
5101
5102 let mut with_prod = DispatchConfig::new("us-east-1", "000000000000");
5105 with_prod.resource_policy_provider = Some(Arc::new(Buckets(&["prod"])));
5106 for method in [http::Method::GET, http::Method::PUT] {
5107 assert_eq!(
5108 target(&with_prod, method.clone(), "/prod/items", &none, &[]),
5109 Some("s3")
5110 );
5111 assert_eq!(
5112 target(&with_prod, method.clone(), "/prod/items", &exec, &[]),
5113 Some("apigateway")
5114 );
5115 assert_eq!(
5116 target(&cfg, method, "/prod/items", &none, &[]),
5117 Some("apigateway")
5118 );
5119 }
5120 assert_eq!(
5122 target(&cfg, http::Method::OPTIONS, "/prod/items", &exec, &[]),
5123 Some("apigateway")
5124 );
5125 assert_eq!(
5126 target(&cfg, http::Method::OPTIONS, "/prod/items", &none, &[]),
5127 Some("s3")
5128 );
5129 assert_eq!(
5130 target(&cfg, http::Method::GET, "/v2/", &none, &[]),
5131 Some("ecr")
5132 );
5133 assert_eq!(
5134 target(&cfg, http::Method::GET, "/_fakecloud/x", &none, &[]),
5135 None
5136 );
5137 assert_eq!(
5138 target(&cfg, http::Method::GET, "/_aws/execute-api/a/b", &none, &[]),
5139 Some("apigateway")
5140 );
5141
5142 assert!(is_unsigned(&none, &HashMap::new()));
5143 assert!(!is_unsigned(&signed, &HashMap::new()));
5144 assert!(!is_unsigned(
5146 &none,
5147 &[("AWSAccessKeyId".to_string(), "AKID".to_string())]
5148 .into_iter()
5149 .collect()
5150 ));
5151 let mut form = http::HeaderMap::new();
5153 form.insert(
5154 http::header::CONTENT_TYPE,
5155 "application/x-www-form-urlencoded; charset=utf-8"
5156 .parse()
5157 .unwrap(),
5158 );
5159 assert!(protocol::is_form_content_type(&form));
5160 assert!(!protocol::is_form_content_type(&none));
5161
5162 let registry = unsigned_routing_registry(false);
5164 assert_eq!(
5165 unrouted_target(
5166 &http::Method::GET,
5167 &"/ghost-bucket/k".parse().unwrap(),
5168 &none,
5169 &HashMap::new(),
5170 ®istry,
5171 &cfg,
5172 ),
5173 Some("apigateway")
5174 );
5175 }
5176
5177 #[test]
5178 fn malformed_sigv2_query_auth_is_rejected_as_recorded() {
5179 let q = |pairs: &[(&str, &str)]| -> HashMap<String, String> {
5180 pairs
5181 .iter()
5182 .map(|(k, v)| (k.to_string(), v.to_string()))
5183 .collect()
5184 };
5185 let none = http::HeaderMap::new();
5186 let now = chrono::DateTime::from_timestamp(1_800_000_000, 0).unwrap();
5187 let message = |headers: &http::HeaderMap, pairs: &[(&str, &str)]| {
5188 s3_sigv2_query_auth_error(headers, &q(pairs), now).map(|(m, _)| m)
5189 };
5190 let incomplete = "Query-string authentication requires the Signature, Expires and \
5191 AWSAccessKeyId parameters";
5192 for pairs in [
5193 &[("AWSAccessKeyId", "AKID"), ("Signature", "x")][..],
5194 &[("AWSAccessKeyId", "AKID"), ("Expires", "1700000000")][..],
5195 &[("Signature", "x")][..],
5196 ] {
5197 assert_eq!(message(&none, pairs).as_deref(), Some(incomplete));
5198 }
5199 let full = |expires: &str| {
5200 s3_sigv2_query_auth_error(
5201 &none,
5202 &q(&[
5203 ("AWSAccessKeyId", "AKID"),
5204 ("Signature", "x"),
5205 ("Expires", expires),
5206 ]),
5207 now,
5208 )
5209 };
5210 assert_eq!(full("2147483647"), None);
5213 for bad in [
5214 "2147483648",
5215 "4294967295",
5216 "4294967296",
5217 "9999999999",
5218 "abc",
5219 ] {
5220 assert_eq!(
5221 full(bad),
5222 Some((
5223 format!("Invalid date (should be seconds since epoch): {bad}"),
5224 Vec::new()
5225 )),
5226 "{bad}"
5227 );
5228 }
5229 assert_eq!(
5231 full("-1"),
5232 Some((
5233 "Request has expired".to_string(),
5234 vec![
5235 ("Expires".to_string(), "1969-12-31T23:59:59Z".to_string()),
5236 ("ServerTime".to_string(), "2027-01-15T08:00:00Z".to_string()),
5237 ]
5238 ))
5239 );
5240 assert_eq!(
5241 full("1700000000").map(|(m, _)| m).as_deref(),
5242 Some("Request has expired")
5243 );
5244 assert_eq!(message(&none, &[("Expires", "x")]), None);
5246 let mut signed = http::HeaderMap::new();
5247 signed.insert(
5248 http::header::AUTHORIZATION,
5249 "AWS4-HMAC-SHA256 x".parse().unwrap(),
5250 );
5251 assert_eq!(message(&signed, &[("Signature", "x")]), None);
5252
5253 let action = |service: &'static str, action: &'static str| crate::auth::IamAction {
5254 service,
5255 action,
5256 resource: "arn:aws:s3:::b".to_string(),
5257 };
5258 assert_eq!(
5259 anonymous_denied_message("s3", &action("s3", "CreateBucket"), "us-east-1"),
5260 "Anonymous users cannot invoke this API. Please authenticate."
5261 );
5262 assert_eq!(
5263 anonymous_denied_message("s3", &action("s3", "GetObject"), "us-east-1"),
5264 "Access Denied"
5265 );
5266 }
5267
5268 #[test]
5269 fn sqs_anonymous_denial_messages() {
5270 let action = |action: &'static str, resource: &str| crate::auth::IamAction {
5272 service: "sqs",
5273 action,
5274 resource: resource.to_string(),
5275 };
5276 assert_eq!(
5277 anonymous_denied_message("sqs", &action("ListQueues", "*"), "us-east-1"),
5278 "Access to the resource https://sqs.us-east-1.amazonaws.com/ is denied."
5279 );
5280 assert_eq!(
5281 anonymous_denied_message("sqs", &action("ListQueues", "*"), "cn-north-1"),
5282 "Access to the resource https://sqs.cn-north-1.amazonaws.com.cn/ is denied."
5283 );
5284 for op in ["ReceiveMessage", "SendMessage", "GetQueueAttributes"] {
5285 let lower = op.to_ascii_lowercase();
5286 assert_eq!(
5287 anonymous_denied_message(
5288 "sqs",
5289 &action(op, "arn:aws:sqs:us-east-1:123456789012:jobs"),
5290 "us-east-1"
5291 ),
5292 format!(
5293 "User: anonymous is not authorized to perform: sqs:{lower} on resource: \
5294 arn:aws:sqs:us-east-1:123456789012:jobs because no resource-based policy \
5295 allows the sqs:{lower} action"
5296 )
5297 );
5298 }
5299 }
5300
5301 async fn missing_auth(
5302 service: &str,
5303 action: &str,
5304 protocol: AwsProtocol,
5305 ) -> (StatusCode, http::HeaderMap, String) {
5306 let detected = protocol::DetectedRequest {
5307 service: service.to_string(),
5308 action: action.to_string(),
5309 protocol,
5310 };
5311 let resp = missing_authentication_response(&detected, "rid", &http::HeaderMap::new());
5312 let status = resp.status();
5313 let headers = resp.headers().clone();
5314 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
5315 .await
5316 .unwrap();
5317 (status, headers, String::from_utf8(body.to_vec()).unwrap())
5318 }
5319
5320 #[tokio::test]
5321 async fn missing_authentication_query_shape() {
5322 for (service, ns) in [
5324 ("sts", "https://sts.amazonaws.com/doc/2011-06-15/"),
5325 ("iam", "https://iam.amazonaws.com/doc/2010-05-08/"),
5326 ("sns", "http://sns.amazonaws.com/doc/2010-03-31/"),
5327 ] {
5328 let (status, _, body) = missing_auth(service, "ListUsers", AwsProtocol::Query).await;
5329 assert_eq!(status, StatusCode::FORBIDDEN);
5330 assert!(
5331 body.contains(&format!(r#"<ErrorResponse xmlns="{ns}">"#)),
5332 "{body}"
5333 );
5334 assert!(body.contains("<Type>Sender</Type>"), "{body}");
5335 assert!(
5336 body.contains("<Code>MissingAuthenticationToken</Code>"),
5337 "{body}"
5338 );
5339 assert!(
5340 body.contains("<Message>Request is missing Authentication Token</Message>"),
5341 "{body}"
5342 );
5343 }
5344 }
5345
5346 #[tokio::test]
5347 async fn missing_authentication_ec2_shape() {
5348 let (status, _, body) = missing_auth("ec2", "DescribeRegions", AwsProtocol::Ec2Query).await;
5349 assert_eq!(status, StatusCode::BAD_REQUEST);
5350 assert!(body.contains("<Response><Errors><Error>"), "{body}");
5351 assert!(body.contains("<Code>MissingParameter</Code>"), "{body}");
5352 assert!(
5353 body.contains("The request must contain the parameter AWSAccessKeyId"),
5354 "{body}"
5355 );
5356 }
5357
5358 #[tokio::test]
5359 async fn missing_authentication_json_shapes() {
5360 let json = |body: &str| serde_json::from_str::<serde_json::Value>(body).unwrap();
5361 for service in ["kms", "ssm", "logs"] {
5362 let (status, _, body) = missing_auth(service, "List", AwsProtocol::Json).await;
5363 assert_eq!(status, StatusCode::BAD_REQUEST);
5364 assert_eq!(
5365 json(&body),
5366 serde_json::json!({
5367 "__type": "MissingAuthenticationTokenException",
5368 "message": "Missing Authentication Token"
5369 })
5370 );
5371 }
5372 let (status, _, body) = missing_auth("dynamodb", "ListTables", AwsProtocol::Json).await;
5373 assert_eq!(status, StatusCode::BAD_REQUEST);
5374 assert_eq!(
5375 json(&body),
5376 serde_json::json!({
5377 "__type": "com.amazon.coral.service#MissingAuthenticationTokenException",
5378 "message": "Request is missing Authentication Token"
5379 })
5380 );
5381 let (status, _, body) =
5382 missing_auth("secretsmanager", "ListSecrets", AwsProtocol::Json).await;
5383 assert_eq!(status, StatusCode::BAD_REQUEST);
5384 assert_eq!(
5385 json(&body),
5386 serde_json::json!({
5387 "__type": "UnrecognizedClientException",
5388 "message": "The security token included in the request is invalid."
5389 })
5390 );
5391 }
5392
5393 #[tokio::test]
5394 async fn missing_authentication_rest_shapes() {
5395 for service in ["lambda", "apigateway", "scheduler"] {
5396 let (status, headers, body) = missing_auth(service, "", AwsProtocol::RestJson).await;
5397 assert_eq!(status, StatusCode::FORBIDDEN);
5398 assert_eq!(
5399 serde_json::from_str::<serde_json::Value>(&body).unwrap(),
5400 serde_json::json!({"message": "Missing Authentication Token"})
5401 );
5402 assert_eq!(
5403 headers["x-amzn-errortype"],
5404 "MissingAuthenticationTokenException"
5405 );
5406 }
5407 let (status, _, body) = missing_auth("route53", "", AwsProtocol::Rest).await;
5408 assert_eq!(status, StatusCode::FORBIDDEN);
5409 assert!(
5410 body.contains(
5411 r#"<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">"#
5412 ),
5413 "{body}"
5414 );
5415 assert!(
5416 body.contains("<Code>MissingAuthenticationToken</Code>"),
5417 "{body}"
5418 );
5419 assert!(
5420 body.contains("<Message>Request is missing Authentication Token</Message>"),
5421 "{body}"
5422 );
5423 }
5424
5425 #[test]
5426 fn only_api_operations_are_judged_for_missing_credentials() {
5427 let detected = |service: &str, action: &str, protocol| protocol::DetectedRequest {
5428 service: service.to_string(),
5429 action: action.to_string(),
5430 protocol,
5431 };
5432 let host = |name: &str| protocol::parse_routing_host(name);
5433 let headers = |name: &str| {
5434 let mut h = http::HeaderMap::new();
5435 h.insert(http::header::HOST, name.parse().unwrap());
5436 h
5437 };
5438 let get = http::Method::GET;
5439 let judged = |d: protocol::DetectedRequest,
5440 hostname: Option<&str>,
5441 method: &http::Method,
5442 control_plane: bool| {
5443 let hdrs = hostname.map(headers).unwrap_or_default();
5444 names_api_operation(&d, hostname.and_then(host).as_ref(), method, &hdrs, || {
5445 control_plane
5446 })
5447 };
5448 assert!(judged(
5449 detected("kms", "ListKeys", AwsProtocol::Json),
5450 None,
5451 &get,
5452 true
5453 ));
5454 assert!(judged(
5455 detected("sts", "GetCallerIdentity", AwsProtocol::Query),
5456 None,
5457 &get,
5458 true
5459 ));
5460 let lambda_host = Some("lambda.us-east-1.amazonaws.com");
5463 let lambda = || detected("lambda", "", AwsProtocol::RestJson);
5464 assert!(judged(lambda(), lambda_host, &get, true));
5465 assert!(!judged(lambda(), lambda_host, &get, false));
5466 assert!(!judged(lambda(), lambda_host, &http::Method::OPTIONS, true));
5468 assert!(judged(
5469 detected("route53", "", AwsProtocol::Rest),
5470 Some("route53.amazonaws.com"),
5471 &get,
5472 true
5473 ));
5474 assert!(!judged(
5477 detected("apigateway", "", AwsProtocol::RestJson),
5478 None,
5479 &get,
5480 true
5481 ));
5482 assert!(!judged(
5484 detected("ecr", "", AwsProtocol::Rest),
5485 Some("ecr.us-east-1.amazonaws.com"),
5486 &get,
5487 true
5488 ));
5489 assert!(!judged(
5490 detected("apigateway", "", AwsProtocol::RestJson),
5491 Some("abc123.execute-api.us-east-1.amazonaws.com"),
5492 &get,
5493 true
5494 ));
5495 }
5496
5497 #[test]
5498 fn query_action_stays_a_query_call_without_a_bucket_or_api() {
5499 let config = DispatchConfig {
5500 region: "us-east-1".to_string(),
5501 account_id: "123456789012".to_string(),
5502 verify_sigv4: false,
5503 iam_mode: IamMode::Off,
5504 credential_resolver: None,
5505 policy_evaluator: None,
5506 resource_policy_provider: None,
5507 scp_resolver: None,
5508 };
5509 let registry = ServiceRegistry::new();
5510 let route = |path_and_query: &str, headers: &[(&str, &str)]| {
5511 let uri: http::Uri = path_and_query.parse().unwrap();
5512 let query: HashMap<String, String> = uri
5513 .query()
5514 .unwrap_or("")
5515 .split('&')
5516 .filter_map(|kv| kv.split_once('='))
5517 .map(|(k, v)| (k.to_string(), v.to_string()))
5518 .collect();
5519 let mut h = http::HeaderMap::new();
5520 for (k, v) in headers {
5521 h.insert(
5522 http::header::HeaderName::from_bytes(k.as_bytes()).unwrap(),
5523 v.parse().unwrap(),
5524 );
5525 }
5526 unsigned_action_is_a_parameter(&http::Method::GET, &uri, &h, &query, ®istry, &config)
5527 };
5528 assert!(!route("/?Action=ListTopics&Version=2010-03-31", &[]));
5532 assert!(!route("/123456789012/q?Action=SendMessage", &[]));
5533 assert!(!route(
5535 "/b/k?Action=ListQueues",
5536 &[(
5537 "authorization",
5538 "AWS4-HMAC-SHA256 Credential=a/b/c/s3/aws4_request"
5539 )]
5540 ));
5541 }
5542
5543 #[test]
5544 fn dispatch_config_carries_opt_in_flags() {
5545 let cfg = DispatchConfig {
5546 region: "eu-west-1".to_string(),
5547 account_id: "000000000000".to_string(),
5548 verify_sigv4: true,
5549 iam_mode: IamMode::Strict,
5550 credential_resolver: None,
5551 policy_evaluator: None,
5552 resource_policy_provider: None,
5553 scp_resolver: None,
5554 };
5555 assert!(cfg.verify_sigv4);
5556 assert!(cfg.iam_mode.is_strict());
5557 assert!(cfg.resource_policy_provider.is_none());
5558 assert!(cfg.scp_resolver.is_none());
5559 }
5560
5561 fn s3_sigv4_headers() -> http::HeaderMap {
5562 let mut headers = http::HeaderMap::new();
5563 headers.insert(
5564 "authorization",
5565 "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/s3/aws4_request, \
5566 SignedHeaders=host, Signature=fake"
5567 .parse()
5568 .unwrap(),
5569 );
5570 headers
5571 }
5572
5573 #[test]
5574 fn streaming_route_path_style_s3_put_object() {
5575 let headers = s3_sigv4_headers();
5576 assert_eq!(
5577 streaming_route(
5578 &http::Method::PUT,
5579 "/my-bucket/key.txt",
5580 &headers,
5581 &HashMap::new(),
5582 ),
5583 Some(("s3", "")),
5584 );
5585 }
5586
5587 #[test]
5588 fn streaming_route_any_s3_put_is_a_candidate() {
5589 let headers = s3_sigv4_headers();
5592 let tagging: HashMap<String, String> = [("tagging".to_string(), String::new())].into();
5593 for (path, query) in [
5594 ("/my-bucket", HashMap::new()),
5595 ("/my-bucket/", HashMap::new()),
5596 ("/my-bucket/k", tagging),
5597 ] {
5598 assert_eq!(
5599 streaming_route(&http::Method::PUT, path, &headers, &query),
5600 Some(("s3", "")),
5601 "{path}"
5602 );
5603 }
5604 }
5605
5606 struct HeadRecorder {
5609 name: &'static str,
5610 seen: parking_lot::Mutex<Vec<(String, Option<String>)>>,
5611 }
5612
5613 #[async_trait::async_trait]
5614 impl crate::service::AwsService for HeadRecorder {
5615 fn service_name(&self) -> &str {
5616 self.name
5617 }
5618 async fn handle(
5619 &self,
5620 _request: AwsRequest,
5621 ) -> Result<crate::service::AwsResponse, crate::service::AwsServiceError> {
5622 unreachable!("only asked whether it streams")
5623 }
5624 fn supported_actions(&self) -> &[&str] {
5625 &[]
5626 }
5627 fn streams_request_body(&self, head: &crate::service::RequestHead<'_>) -> bool {
5628 let tagging = head
5629 .headers
5630 .get("x-amz-tagging")
5631 .and_then(|v| v.to_str().ok())
5632 .map(str::to_string);
5633 self.seen.lock().push((head.raw_path.to_string(), tagging));
5634 head.raw_path.ends_with("/upload")
5635 }
5636 }
5637
5638 #[test]
5639 fn service_streams_body_asks_with_the_request_as_routed() {
5640 let s3 = HeadRecorder {
5644 name: "s3",
5645 seen: Default::default(),
5646 };
5647 let mut headers = s3_sigv4_headers();
5648 headers.insert(
5649 "host",
5650 "my-bucket.s3.us-east-1.amazonaws.com".parse().unwrap(),
5651 );
5652 let presigned: HashMap<String, String> = [
5653 (
5654 "X-Amz-Credential",
5655 "test/20240101/us-east-1/s3/aws4_request",
5656 ),
5657 ("x-amz-tagging", "env=test"),
5658 ]
5659 .into_iter()
5660 .map(|(k, v)| (k.to_string(), v.to_string()))
5661 .collect();
5662 let put = http::Method::PUT;
5663 let empty = HashMap::new();
5664 let request = |wire_path, query_params| EarlyRequest {
5665 method: &put,
5666 wire_path,
5667 headers: &headers,
5668 query_params,
5669 anonymous: false,
5670 region: "us-east-1",
5671 request_id: "r",
5672 };
5673 assert!(service_streams_body(&s3, &request("/upload", &presigned)));
5674 assert!(!service_streams_body(&s3, &request("/", &empty)));
5675 assert_eq!(
5676 *s3.seen.lock(),
5677 vec![
5678 (
5679 "/my-bucket/upload".to_string(),
5680 Some("env=test".to_string())
5681 ),
5682 ("/my-bucket".to_string(), None),
5683 ]
5684 );
5685 let other = HeadRecorder {
5687 name: "ecr",
5688 seen: Default::default(),
5689 };
5690 assert!(!service_streams_body(
5691 &other,
5692 &request("/my-bucket/k", &presigned)
5693 ));
5694 assert_eq!(*other.seen.lock(), vec![("/my-bucket/k".to_string(), None)]);
5695 }
5696
5697 #[test]
5698 fn s3_routing_path_prefixes_the_host_bucket() {
5699 assert_eq!(s3_routing_path("/", Some("b")), "/b");
5700 assert_eq!(s3_routing_path("", Some("b")), "/b");
5701 assert_eq!(s3_routing_path("/k.txt", Some("b")), "/b/k.txt");
5702 assert_eq!(s3_routing_path("/dir/k", Some("a.b")), "/a.b/dir/k");
5703 assert_eq!(s3_routing_path("/b/k", None), "/b/k");
5704 }
5705
5706 #[test]
5707 fn s3_routing_path_keeps_a_key_that_starts_with_the_bucket_name() {
5708 assert_eq!(
5711 s3_routing_path("/docs/intro.html", Some("docs")),
5712 "/docs/docs/intro.html"
5713 );
5714 assert_eq!(s3_routing_path("/docs", Some("docs")), "/docs/docs");
5715 }
5716
5717 #[test]
5718 fn streaming_route_virtual_hosted_s3_put_object() {
5719 let mut headers = s3_sigv4_headers();
5720 headers.insert(
5721 "host",
5722 "vhost-bucket.s3.us-east-1.localhost.localstack.cloud:4566"
5723 .parse()
5724 .unwrap(),
5725 );
5726 assert_eq!(
5731 streaming_route(&http::Method::PUT, "/hello.txt", &headers, &HashMap::new(),),
5732 Some(("s3", "")),
5733 );
5734 }
5735
5736 #[test]
5737 fn streaming_route_virtual_hosted_path_naming_the_bucket_streams() {
5738 let mut headers = s3_sigv4_headers();
5743 headers.insert(
5744 "host",
5745 "my-bucket.s3.us-east-1.amazonaws.com".parse().unwrap(),
5746 );
5747 for path in ["/my-bucket", "/my-bucket/", "/my-bucket/key.txt"] {
5748 assert_eq!(
5749 streaming_route(&http::Method::PUT, path, &headers, &HashMap::new()),
5750 Some(("s3", "")),
5751 "{path}",
5752 );
5753 }
5754 }
5755
5756 #[test]
5757 fn streaming_route_ecr_blob_upload() {
5758 let headers = http::HeaderMap::new();
5759 assert_eq!(
5760 streaming_route(
5761 &http::Method::PATCH,
5762 "/v2/my-repo/blobs/uploads/abcd1234",
5763 &headers,
5764 &HashMap::new(),
5765 ),
5766 Some(("ecr", "")),
5767 );
5768 assert_eq!(
5769 streaming_route(
5770 &http::Method::PUT,
5771 "/v2/my-repo/blobs/uploads/abcd1234",
5772 &headers,
5773 &HashMap::new(),
5774 ),
5775 Some(("ecr", "")),
5776 );
5777 }
5778
5779 #[test]
5780 fn hoist_presigned_query_headers_skips_auth_params_and_keeps_direct_headers() {
5781 let mut headers = http::HeaderMap::new();
5782 headers.insert("x-amz-meta-color", "red".parse().unwrap());
5783 let query: HashMap<String, String> = [
5784 (
5785 "X-Amz-Credential",
5786 "AKID/20260101/us-east-1/s3/aws4_request",
5787 ),
5788 ("X-Amz-Signature", "00"),
5789 ("X-Amz-Security-Token", "tok"),
5790 ("x-amz-meta-color", "blue"),
5791 ("X-Amz-Meta-Shape", "round"),
5792 ("x-amz-meta-name", "café"),
5793 ("x-amz-tagging", "env=test"),
5794 ("response-content-type", "text/plain"),
5795 ("partNumber", "1"),
5796 ]
5797 .into_iter()
5798 .map(|(k, v)| (k.to_string(), v.to_string()))
5799 .collect();
5800 hoist_presigned_query_headers(&mut headers, &query);
5801
5802 assert_eq!(headers["x-amz-meta-color"], "red");
5803 assert_eq!(headers["x-amz-meta-shape"], "round");
5804 assert_eq!(headers["x-amz-meta-name"], "=?UTF-8?B?Y2Fmw6k=?=");
5805 assert_eq!(headers["x-amz-tagging"], "env=test");
5806 for absent in [
5807 "x-amz-credential",
5808 "x-amz-signature",
5809 "x-amz-security-token",
5810 "response-content-type",
5811 "partnumber",
5812 ] {
5813 assert!(headers.get(absent).is_none(), "{absent}");
5814 }
5815 }
5816
5817 #[test]
5818 fn streaming_route_presigned_v4_s3_put() {
5819 let headers = http::HeaderMap::new();
5820 let mut query_params = HashMap::new();
5821 query_params.insert(
5822 "X-Amz-Credential".to_string(),
5823 "test/20240101/us-east-1/s3/aws4_request".to_string(),
5824 );
5825 assert_eq!(
5826 streaming_route(
5827 &http::Method::PUT,
5828 "/my-bucket/key.txt",
5829 &headers,
5830 &query_params,
5831 ),
5832 Some(("s3", "")),
5833 );
5834 }
5835
5836 #[test]
5837 fn streaming_route_non_s3_auth_header_skipped() {
5838 let mut headers = http::HeaderMap::new();
5841 headers.insert(
5842 "authorization",
5843 "AWS4-HMAC-SHA256 Credential=test/20240101/us-east-1/lambda/aws4_request, \
5844 SignedHeaders=host, Signature=fake"
5845 .parse()
5846 .unwrap(),
5847 );
5848 assert_eq!(
5849 streaming_route(
5850 &http::Method::PUT,
5851 "/my-bucket/key.txt",
5852 &headers,
5853 &HashMap::new(),
5854 ),
5855 None,
5856 );
5857 }
5858
5859 #[test]
5860 fn streaming_route_get_skipped() {
5861 let headers = s3_sigv4_headers();
5862 assert_eq!(
5863 streaming_route(
5864 &http::Method::GET,
5865 "/my-bucket/key.txt",
5866 &headers,
5867 &HashMap::new(),
5868 ),
5869 None,
5870 );
5871 }
5872
5873 #[test]
5878 fn root_cross_account_needs_the_resource_policy() {
5879 use crate::auth::IamAction;
5880 use crate::service::{AwsResponse, AwsServiceError};
5881 struct OwnerProvider;
5882 impl crate::auth::ResourcePolicyProvider for OwnerProvider {
5883 fn resource_policy(&self, _service: &str, resource_arn: &str) -> Option<String> {
5884 resource_arn
5885 .starts_with("arn:aws:s3:::granted")
5886 .then(|| "granting-policy".to_string())
5887 }
5888 fn resource_owner_account(&self, _service: &str, resource_arn: &str) -> Option<String> {
5889 let bucket = resource_arn.strip_prefix("arn:aws:s3:::")?;
5890 let bucket = bucket.split('/').next()?;
5891 match bucket {
5892 "own" => Some("222222222222".to_string()),
5893 "granted" | "denied" => Some("111111111111".to_string()),
5894 _ => None,
5895 }
5896 }
5897 }
5898 struct PolicyEvaluator(parking_lot::Mutex<Vec<ConditionContext>>);
5899 impl IamPolicyEvaluator for PolicyEvaluator {
5900 fn evaluate(
5901 &self,
5902 _: &Principal,
5903 _: &IamAction,
5904 _: &ConditionContext,
5905 _: &[String],
5906 _: Option<&[String]>,
5907 ) -> crate::auth::IamDecision {
5908 crate::auth::IamDecision::ImplicitDeny
5909 }
5910 fn evaluate_with_resource_policy(
5911 &self,
5912 _: &Principal,
5913 _: &IamAction,
5914 _: &ConditionContext,
5915 _: Option<&str>,
5916 _: &str,
5917 _: &[String],
5918 _: Option<&[String]>,
5919 ) -> crate::auth::IamDecision {
5920 crate::auth::IamDecision::ImplicitDeny
5921 }
5922 fn evaluate_resource_policy_only(
5923 &self,
5924 _: &Principal,
5925 _: &IamAction,
5926 context: &ConditionContext,
5927 policy: Option<&str>,
5928 ) -> crate::auth::IamDecision {
5929 self.0.lock().push(context.clone());
5930 if policy == Some("granting-policy") {
5931 crate::auth::IamDecision::Allow
5932 } else {
5933 crate::auth::IamDecision::ImplicitDeny
5934 }
5935 }
5936 }
5937 struct BucketService;
5938 #[async_trait::async_trait]
5939 impl crate::service::AwsService for BucketService {
5940 fn service_name(&self) -> &str {
5941 "s3"
5942 }
5943 async fn handle(&self, _: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
5944 unreachable!()
5945 }
5946 fn supported_actions(&self) -> &[&str] {
5947 &[]
5948 }
5949 fn iam_action_for(&self, request: &AwsRequest) -> Option<IamAction> {
5950 Some(IamAction {
5951 service: "s3",
5952 action: "GetObject",
5953 resource: format!("arn:aws:s3:::{}", request.path_segments.join("/")),
5954 })
5955 }
5956 }
5957 let root = Principal {
5958 arn: "arn:aws:iam::222222222222:root".to_string(),
5959 user_id: "222222222222".to_string(),
5960 account_id: "222222222222".to_string(),
5961 principal_type: PrincipalType::Root,
5962 source_identity: None,
5963 tags: None,
5964 };
5965 let detected = protocol::DetectedRequest {
5966 service: "s3".to_string(),
5967 action: String::new(),
5968 protocol: AwsProtocol::Rest,
5969 };
5970 let request = |bucket: &str| AwsRequest {
5971 service: "s3".to_string(),
5972 action: String::new(),
5973 region: "us-east-1".to_string(),
5974 account_id: "222222222222".to_string(),
5975 request_id: "req".to_string(),
5976 headers: http::HeaderMap::new(),
5977 query_params: HashMap::new(),
5978 body: Bytes::new(),
5979 body_stream: parking_lot::Mutex::new(None),
5980 path_segments: vec![bucket.to_string(), "key".to_string()],
5981 raw_path: format!("/{bucket}/key"),
5982 raw_query: String::new(),
5983 method: http::Method::GET,
5984 is_query_protocol: false,
5985 access_key_id: Some("AKIAROOTB".to_string()),
5986 principal: Some(root.clone()),
5987 };
5988 struct Org;
5989 impl crate::auth::ScpResolver for Org {
5990 fn scps_for(&self, _: &Principal) -> Option<Vec<String>> {
5991 None
5992 }
5993 fn principal_org(&self, account: &str) -> Option<(String, String)> {
5994 (account == "222222222222").then(|| ("o-abc".to_string(), "o-abc/r-1/".to_string()))
5995 }
5996 }
5997 let issued = chrono::Utc::now();
5998 let credential = crate::auth::ResolvedCredential {
5999 secret_access_key: "secret".to_string(),
6000 session_token: Some("token".to_string()),
6001 principal: root.clone(),
6002 session_policies: Vec::new(),
6003 mfa_present: true,
6004 token_issued_at: Some(issued),
6005 federated_provider: None,
6006 role_arn: None,
6007 federated_parent_arn: None,
6008 };
6009 let evaluator = PolicyEvaluator(parking_lot::Mutex::new(Vec::new()));
6010 let run = |bucket: &str, mode: IamMode| {
6011 let mut cfg = DispatchConfig::new("us-east-1", "111111111111");
6012 cfg.iam_mode = mode;
6013 cfg.resource_policy_provider = Some(Arc::new(OwnerProvider));
6014 cfg.scp_resolver = Some(Arc::new(Org));
6015 authorize_root_cross_account(
6016 &root,
6017 &BucketService,
6018 &request(bucket),
6019 &evaluator,
6020 &cfg,
6021 &detected,
6022 "req",
6023 None,
6024 Some(&credential),
6025 &crate::service::AwsService::iam_actions_for(&BucketService, &request(bucket)),
6026 )
6027 };
6028 let denied = run("denied", IamMode::Strict).expect("no grant must deny under strict");
6029 assert_eq!(denied.status(), StatusCode::FORBIDDEN);
6030 assert!(run("granted", IamMode::Strict).is_none());
6031 assert!(run("own", IamMode::Strict).is_none());
6032 assert!(run("unclaimed", IamMode::Strict).is_none());
6033 assert!(run("denied", IamMode::Soft).is_none());
6034
6035 let contexts = evaluator.0.lock();
6039 assert!(!contexts.is_empty());
6040 for ctx in contexts.iter() {
6041 assert_eq!(
6042 ctx.lookup("aws:ResourceAccount"),
6043 Some(vec!["111111111111".into()])
6044 );
6045 assert_eq!(ctx.lookup("aws:PrincipalOrgID"), Some(vec!["o-abc".into()]));
6046 assert_eq!(
6047 ctx.lookup("aws:PrincipalOrgPaths"),
6048 Some(vec!["o-abc/r-1/".into()])
6049 );
6050 assert_eq!(ctx.aws_mfa_present, Some(true));
6051 assert_eq!(ctx.aws_token_issue_time, Some(issued));
6052 assert_eq!(ctx.aws_principal_type.as_deref(), Some("Account"));
6053 }
6054 }
6055
6056 #[test]
6057 fn denial_messages_name_the_scp_when_it_refused() {
6058 use crate::auth::IamDecision;
6059 assert_eq!(scp_denial_reason(IamDecision::Allow, "s3:GetObject"), None);
6060 use IamDecision::{Allow, ExplicitDeny, ImplicitDeny};
6063 assert_eq!(scp_cause(ExplicitDeny, ExplicitDeny), Some(ExplicitDeny));
6064 assert_eq!(scp_cause(ImplicitDeny, ImplicitDeny), Some(ImplicitDeny));
6065 assert_eq!(scp_cause(ExplicitDeny, ImplicitDeny), None);
6066 assert_eq!(scp_cause(ImplicitDeny, Allow), None);
6067 assert_eq!(scp_cause(ExplicitDeny, Allow), None);
6068 assert_eq!(access_denied_code("s3", AwsProtocol::Rest), "AccessDenied");
6071 assert_eq!(
6072 access_denied_code("sts", AwsProtocol::Query),
6073 "AccessDenied"
6074 );
6075 assert_eq!(
6076 access_denied_code("iam", AwsProtocol::Query),
6077 "AccessDenied"
6078 );
6079 assert_eq!(
6080 access_denied_code("elasticloadbalancing", AwsProtocol::Query),
6081 "AccessDenied"
6082 );
6083 assert_eq!(
6084 access_denied_code("sqs", AwsProtocol::Query),
6085 "AccessDenied"
6086 );
6087 assert_eq!(
6088 access_denied_code("sqs", AwsProtocol::Json),
6089 "AccessDeniedException"
6090 );
6091 assert_eq!(
6092 access_denied_code("sns", AwsProtocol::Query),
6093 "AuthorizationError"
6094 );
6095 assert_eq!(
6096 access_denied_code("kms", AwsProtocol::Json),
6097 "AccessDeniedException"
6098 );
6099 assert_eq!(
6100 access_denied_code("lambda", AwsProtocol::Rest),
6101 "AccessDeniedException"
6102 );
6103 let detected = |service: &str, protocol| protocol::DetectedRequest {
6105 service: service.to_string(),
6106 action: "SendMessage".to_string(),
6107 protocol,
6108 };
6109 let headers = http::HeaderMap::new();
6110 let sqs_json =
6111 access_denied_response("m", "r", &detected("sqs", AwsProtocol::Json), &headers);
6112 assert_eq!(
6113 sqs_json.headers()["x-amzn-query-error"],
6114 "AccessDenied;Sender"
6115 );
6116 let kms = access_denied_response("m", "r", &detected("kms", AwsProtocol::Json), &headers);
6117 assert!(kms.headers().get("x-amzn-query-error").is_none());
6118 let explicit = scp_denial_reason(IamDecision::ExplicitDeny, "s3:GetObject");
6119 let implicit = scp_denial_reason(IamDecision::ImplicitDeny, "s3:GetObject");
6120 let user = "arn:aws:iam::222222222222:user/u";
6121 assert_eq!(
6122 not_authorized_message(user, "s3:GetObject", "arn:aws:s3:::b/k", explicit.as_deref(), None),
6123 "User: arn:aws:iam::222222222222:user/u is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::b/k with an explicit deny in a service control policy"
6124 );
6125 assert_eq!(
6126 not_authorized_message(user, "s3:GetObject", "arn:aws:s3:::b/k", implicit.as_deref(), Some("BLOB")),
6127 "User: arn:aws:iam::222222222222:user/u is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::b/k because no service control policy allows the s3:GetObject action Encoded authorization failure message: BLOB"
6128 );
6129 assert_eq!(
6130 not_authorized_message(user, "s3:GetObject", "arn:aws:s3:::b/k", None, Some("BLOB")),
6131 "User: arn:aws:iam::222222222222:user/u is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::b/k Encoded authorization failure message: BLOB"
6132 );
6133 }
6134
6135 #[tokio::test]
6140 async fn root_requests_are_capped_by_scps() {
6141 use crate::auth::IamAction;
6142 use crate::service::{AwsResponse, AwsServiceError};
6143 struct OwnerProvider;
6144 impl crate::auth::ResourcePolicyProvider for OwnerProvider {
6145 fn resource_policy(&self, _service: &str, _resource_arn: &str) -> Option<String> {
6146 Some("granting-policy".to_string())
6147 }
6148 fn resource_owner_account(&self, _service: &str, resource_arn: &str) -> Option<String> {
6149 let bucket = resource_arn.strip_prefix("arn:aws:s3:::")?;
6150 match bucket.split('/').next()? {
6151 "own" => Some("222222222222".to_string()),
6152 _ => Some("111111111111".to_string()),
6153 }
6154 }
6155 }
6156 struct PolicyEvaluator(parking_lot::Mutex<Vec<ConditionContext>>);
6160 impl IamPolicyEvaluator for PolicyEvaluator {
6161 fn evaluate(
6162 &self,
6163 _: &Principal,
6164 _: &IamAction,
6165 _: &ConditionContext,
6166 _: &[String],
6167 _: Option<&[String]>,
6168 ) -> crate::auth::IamDecision {
6169 crate::auth::IamDecision::ImplicitDeny
6170 }
6171 fn evaluate_with_resource_policy(
6172 &self,
6173 _: &Principal,
6174 _: &IamAction,
6175 _: &ConditionContext,
6176 _: Option<&str>,
6177 _: &str,
6178 _: &[String],
6179 _: Option<&[String]>,
6180 ) -> crate::auth::IamDecision {
6181 crate::auth::IamDecision::ImplicitDeny
6182 }
6183 fn evaluate_resource_policy_only(
6184 &self,
6185 _: &Principal,
6186 _: &IamAction,
6187 _: &ConditionContext,
6188 policy: Option<&str>,
6189 ) -> crate::auth::IamDecision {
6190 if policy == Some("granting-policy") {
6191 crate::auth::IamDecision::Allow
6192 } else {
6193 crate::auth::IamDecision::ImplicitDeny
6194 }
6195 }
6196 fn evaluate_ceiling(
6197 &self,
6198 _: &Principal,
6199 _: &IamAction,
6200 context: &ConditionContext,
6201 session_policies: &[String],
6202 scps: Option<&[String]>,
6203 with_boundary: bool,
6204 ) -> crate::auth::IamDecision {
6205 assert!(session_policies.is_empty() && !with_boundary);
6206 self.0.lock().push(context.clone());
6207 let scps = scps.unwrap_or_default();
6208 if scps.iter().any(|s| s == "deny") {
6209 crate::auth::IamDecision::ExplicitDeny
6210 } else if !scps.is_empty() && scps.iter().all(|s| s == "allow") {
6211 crate::auth::IamDecision::Allow
6212 } else {
6213 crate::auth::IamDecision::ImplicitDeny
6214 }
6215 }
6216 }
6217 struct BucketService;
6218 #[async_trait::async_trait]
6219 impl crate::service::AwsService for BucketService {
6220 fn service_name(&self) -> &str {
6221 "s3"
6222 }
6223 async fn handle(&self, _: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
6224 unreachable!()
6225 }
6226 fn supported_actions(&self) -> &[&str] {
6227 &[]
6228 }
6229 fn iam_action_for(&self, request: &AwsRequest) -> Option<IamAction> {
6230 Some(IamAction {
6231 service: "s3",
6232 action: "GetObject",
6233 resource: format!("arn:aws:s3:::{}", request.path_segments.join("/")),
6234 })
6235 }
6236 }
6237 struct Org(Option<Vec<String>>);
6238 impl crate::auth::ScpResolver for Org {
6239 fn scps_for(&self, _: &Principal) -> Option<Vec<String>> {
6240 self.0.clone()
6241 }
6242 fn principal_org(&self, _: &str) -> Option<(String, String)> {
6243 Some(("o-abc".to_string(), "o-abc/r-1/".to_string()))
6244 }
6245 }
6246 let root = Principal {
6247 arn: "arn:aws:iam::222222222222:root".to_string(),
6248 user_id: "222222222222".to_string(),
6249 account_id: "222222222222".to_string(),
6250 principal_type: PrincipalType::Root,
6251 source_identity: None,
6252 tags: None,
6253 };
6254 let detected = protocol::DetectedRequest {
6255 service: "s3".to_string(),
6256 action: String::new(),
6257 protocol: AwsProtocol::Rest,
6258 };
6259 let request = |bucket: &str| AwsRequest {
6260 service: "s3".to_string(),
6261 action: String::new(),
6262 region: "us-east-1".to_string(),
6263 account_id: "222222222222".to_string(),
6264 request_id: "req".to_string(),
6265 headers: http::HeaderMap::new(),
6266 query_params: HashMap::new(),
6267 body: Bytes::new(),
6268 body_stream: parking_lot::Mutex::new(None),
6269 path_segments: vec![bucket.to_string(), "key".to_string()],
6270 raw_path: format!("/{bucket}/key"),
6271 raw_query: String::new(),
6272 method: http::Method::GET,
6273 is_query_protocol: false,
6274 access_key_id: Some("ASIAROOTB".to_string()),
6275 principal: Some(root.clone()),
6276 };
6277 let evaluator = PolicyEvaluator(parking_lot::Mutex::new(Vec::new()));
6278 let run = |bucket: &str, scps: Option<&[&str]>, mode: IamMode| {
6279 let mut cfg = DispatchConfig::new("us-east-1", "111111111111");
6280 cfg.iam_mode = mode;
6281 cfg.resource_policy_provider = Some(Arc::new(OwnerProvider));
6282 cfg.scp_resolver = Some(Arc::new(Org(
6283 scps.map(|s| s.iter().map(|d| d.to_string()).collect())
6284 )));
6285 authorize_root(
6286 &root,
6287 &BucketService,
6288 &request(bucket),
6289 &evaluator,
6290 &cfg,
6291 &detected,
6292 "req",
6293 None,
6294 None,
6295 &crate::service::AwsService::iam_actions_for(&BucketService, &request(bucket)),
6296 false,
6297 )
6298 };
6299 async fn message(resp: Response<Body>) -> String {
6300 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
6301 let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
6302 .await
6303 .unwrap();
6304 String::from_utf8(body.to_vec()).unwrap()
6305 }
6306 for bucket in ["own", "granted"] {
6307 assert!(run(bucket, None, IamMode::Strict).is_none(), "{bucket}");
6309 assert!(
6311 run(bucket, Some(&["allow"]), IamMode::Strict).is_none(),
6312 "{bucket}"
6313 );
6314 let explicit = message(
6316 run(bucket, Some(&["allow", "deny"]), IamMode::Strict)
6317 .expect("an SCP deny must refuse root"),
6318 )
6319 .await;
6320 assert!(
6321 explicit.contains("<Code>AccessDenied</Code>")
6322 && explicit.contains(&format!(
6323 "<Message>User: arn:aws:iam::222222222222:root is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::{bucket}/key with an explicit deny in a service control policy</Message>"
6324 )),
6325 "{explicit}"
6326 );
6327 let implicit = message(
6328 run(bucket, Some(&["narrow"]), IamMode::Strict)
6329 .expect("an SCP not allowing the action must refuse root"),
6330 )
6331 .await;
6332 assert!(
6333 implicit.contains("<Code>AccessDenied</Code>")
6334 && implicit.contains(&format!(
6335 "<Message>User: arn:aws:iam::222222222222:root is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::{bucket}/key because no service control policy allows the s3:GetObject action</Message>"
6336 )),
6337 "{implicit}"
6338 );
6339 assert!(
6340 run(bucket, Some(&[]), IamMode::Strict).is_some(),
6341 "{bucket}"
6342 );
6343 assert!(
6345 run(bucket, Some(&["deny"]), IamMode::Soft).is_none(),
6346 "{bucket}"
6347 );
6348 }
6349
6350 struct UnmappedService;
6353 #[async_trait::async_trait]
6354 impl crate::service::AwsService for UnmappedService {
6355 fn service_name(&self) -> &str {
6356 "s3"
6357 }
6358 async fn handle(&self, _: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
6359 unreachable!()
6360 }
6361 fn supported_actions(&self) -> &[&str] {
6362 &[]
6363 }
6364 fn iam_action_for(&self, _: &AwsRequest) -> Option<IamAction> {
6365 None
6366 }
6367 }
6368 let unmapped = |scps: Option<&[&str]>, mode: IamMode| {
6369 let mut cfg = DispatchConfig::new("us-east-1", "111111111111");
6370 cfg.iam_mode = mode;
6371 cfg.resource_policy_provider = Some(Arc::new(OwnerProvider));
6372 cfg.scp_resolver = Some(Arc::new(Org(
6373 scps.map(|s| s.iter().map(|d| d.to_string()).collect())
6374 )));
6375 authorize_root(
6376 &root,
6377 &UnmappedService,
6378 &request("own"),
6379 &evaluator,
6380 &cfg,
6381 &detected,
6382 "req",
6383 None,
6384 None,
6385 &crate::service::AwsService::iam_actions_for(&UnmappedService, &request("own")),
6386 false,
6387 )
6388 };
6389 let refused = message(
6390 unmapped(Some(&["allow"]), IamMode::Strict)
6391 .expect("an unmapped operation under SCPs must fail closed"),
6392 )
6393 .await;
6394 assert!(
6395 refused.contains("<Code>AccessDenied</Code>")
6396 && refused.contains("no IAM action mapping exists for this operation"),
6397 "{refused}"
6398 );
6399 assert!(unmapped(Some(&["allow"]), IamMode::Soft).is_none());
6400 assert!(unmapped(None, IamMode::Strict).is_none());
6403 let contexts = evaluator.0.lock();
6406 assert!(contexts
6407 .iter()
6408 .any(|c| c.lookup("aws:ResourceAccount") == Some(vec!["111111111111".into()])));
6409 assert!(contexts
6410 .iter()
6411 .any(|c| c.lookup("aws:ResourceAccount") == Some(vec!["222222222222".into()])));
6412 for ctx in contexts.iter() {
6413 assert_eq!(ctx.lookup("aws:PrincipalOrgID"), Some(vec!["o-abc".into()]));
6414 assert_eq!(
6415 ctx.aws_principal_arn.as_deref(),
6416 Some("arn:aws:iam::222222222222:root")
6417 );
6418 }
6419 }
6420}
6421
6422fn is_execute_api_host(headers: &http::HeaderMap) -> bool {
6425 headers
6426 .get(http::header::HOST)
6427 .and_then(|v| v.to_str().ok())
6428 .is_some_and(|host| host.contains(".execute-api."))
6429}
6430
6431#[cfg(test)]
6432mod execute_api_host_tests {
6433 use super::is_execute_api_host;
6434
6435 #[test]
6436 fn recognizes_execute_api_hosts_only() {
6437 let mut h = http::HeaderMap::new();
6438 h.insert(
6439 http::header::HOST,
6440 "abc123.execute-api.us-east-1.amazonaws.com"
6441 .parse()
6442 .unwrap(),
6443 );
6444 assert!(is_execute_api_host(&h));
6445 h.insert(http::header::HOST, "localhost:4566".parse().unwrap());
6446 assert!(!is_execute_api_host(&h));
6447 assert!(!is_execute_api_host(&http::HeaderMap::new()));
6448 }
6449}