@@ -907,6 +907,97 @@ async fn bucket_policy_identity_allow_no_bucket_policy_still_works() {
907907 . unwrap ( ) ;
908908}
909909
910+ /// A hand-crafted signed Authorization header carrying `akid`'s access key in
911+ /// the S3 credential scope. With SigV4 verification off the dummy signature is
912+ /// never checked, but dispatch still parses the access key and resolves the
913+ /// principal — letting a test drive requests that the AWS SDK would never
914+ /// emit (here, an S3 sub-resource form with no IAM-action mapping).
915+ fn s3_signed_auth_header ( akid : & str ) -> String {
916+ format ! (
917+ "AWS4-HMAC-SHA256 Credential={akid}/20240101/us-east-1/s3/aws4_request, \
918+ SignedHeaders=host, Signature=deadbeef"
919+ )
920+ }
921+
922+ #[ tokio:: test]
923+ async fn strict_mode_denies_unmapped_enforceable_action ( ) {
924+ // Fix 5.2: strict enforcement must fail closed when an `iam_enforceable`
925+ // service returns no `IamAction` for an operation. `POST /bucket?acl` is
926+ // not a valid S3 op (ACL is GET/PUT only), so `s3_detect_action` returns
927+ // None — the enforceable-but-unmapped branch. Previously dispatch warned
928+ // and fell through to the handler, running the op with no policy check.
929+ //
930+ // SigV4 verification is left OFF so the hand-crafted request reaches the
931+ // IAM layer (a bad signature would otherwise 403 earlier, masking the
932+ // behavior under test).
933+ let server = TestServer :: start_with_env ( & [ ( "FAKECLOUD_IAM" , "strict" ) ] ) . await ;
934+
935+ let boot = sdk_config_with ( & server, "test" , "test" ) . await ;
936+ aws_sdk_s3:: Client :: new ( & boot)
937+ . create_bucket ( )
938+ . bucket ( "unmapped-probe" )
939+ . send ( )
940+ . await
941+ . unwrap ( ) ;
942+
943+ let ( akid, _secret) = bootstrap_user ( & server, "unmapped_user" ) . await ;
944+ // Allow-all identity policy: any *mapped* action would be permitted, so a
945+ // 403 here can only come from the missing IAM-action mapping, not a policy
946+ // deny. This isolates the fail-closed behavior.
947+ attach_inline_policy (
948+ & server,
949+ "unmapped_user" ,
950+ "AllowEverything" ,
951+ r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}"# ,
952+ )
953+ . await ;
954+
955+ let http = reqwest:: Client :: new ( ) ;
956+ let resp = http
957+ . post ( format ! ( "{}/unmapped-probe?acl" , server. endpoint( ) ) )
958+ . header ( "authorization" , s3_signed_auth_header ( & akid) )
959+ . send ( )
960+ . await
961+ . unwrap ( ) ;
962+ assert_eq ! (
963+ resp. status( ) ,
964+ 403 ,
965+ "strict mode must deny an enforceable action with no IAM-action mapping (fail closed)"
966+ ) ;
967+ }
968+
969+ #[ tokio:: test]
970+ async fn soft_mode_allows_unmapped_enforceable_action_through ( ) {
971+ // Counterpart guarantee to `strict_mode_denies_unmapped_enforceable_action`:
972+ // the new deny is gated on strict mode only. In soft mode the same unmapped
973+ // enforceable action is NOT denied by the IAM layer — it warns and falls
974+ // through to the handler, preserving historical rollout behavior.
975+ let server = TestServer :: start_with_env ( & [ ( "FAKECLOUD_IAM" , "soft" ) ] ) . await ;
976+
977+ let boot = sdk_config_with ( & server, "test" , "test" ) . await ;
978+ aws_sdk_s3:: Client :: new ( & boot)
979+ . create_bucket ( )
980+ . bucket ( "unmapped-soft-probe" )
981+ . send ( )
982+ . await
983+ . unwrap ( ) ;
984+
985+ let ( akid, _secret) = bootstrap_user ( & server, "unmapped_soft_user" ) . await ;
986+
987+ let http = reqwest:: Client :: new ( ) ;
988+ let resp = http
989+ . post ( format ! ( "{}/unmapped-soft-probe?acl" , server. endpoint( ) ) )
990+ . header ( "authorization" , s3_signed_auth_header ( & akid) )
991+ . send ( )
992+ . await
993+ . unwrap ( ) ;
994+ assert_ne ! (
995+ resp. status( ) ,
996+ 403 ,
997+ "soft mode must not deny an unmapped enforceable action; it falls through to the handler"
998+ ) ;
999+ }
1000+
9101001#[ tokio:: test]
9111002async fn bucket_policy_principal_wildcard_grants_any_user ( ) {
9121003 // Public bucket idiom: `"Principal": "*"`. Any non-root caller
0 commit comments