Unverified Commit 63afb410 authored by Zelda Hessler's avatar Zelda Hessler Committed by GitHub
Browse files

Add "invalid xml body root" check exemption for S3's `GetObjectAttributes` (#1665)

* add: ability for certain operations to be exempt from XML body root checking
* add: XML body root checking exemption for com.amazonaws.s3#GetObjectAttributesOutput
parent 5abd687a
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -226,3 +226,18 @@ message = "Bump [MSRV](https://github.com/awslabs/aws-sdk-rust#supported-rust-ve
references = ["smithy-rs#1699"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "all"}
author = "Velfi"

[[aws-sdk-rust]]
message = "The AWS S3 `GetObjectAttributes` operation will no longer fail with an XML error."
references = ["aws-sdk-rust#609"]
meta = { "breaking" = false, "tada" = true, "bug" = true }
author = "Velfi"

[[smithy-rs]]
message = """
It is now possible to exempt specific operations from XML body root checking. To do this, add the `AllowInvalidXmlRoot`
trait to the output struct of the operation you want to exempt.
"""
references = ["aws-sdk-rust#609"]
meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client"}
author = "Velfi"
 No newline at end of file
+1 −1
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ import software.amazon.smithy.model.traits.AnnotationTrait
/**
 * Indicates that a member should have their resource ID prefix stripped
 */
class TrimResourceId() : AnnotationTrait(ID, Node.objectNode()) {
class TrimResourceId : AnnotationTrait(ID, Node.objectNode()) {
    companion object {
        val ID: ShapeId = ShapeId.from("aws.api.internal#trimResourceId")
    }
+28 −1
Original line number Diff line number Diff line
@@ -6,8 +6,13 @@
package software.amazon.smithy.rustsdk.customize.s3

import software.amazon.smithy.aws.traits.protocols.RestXmlTrait
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.transform.ModelTransformer
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.RustModule
import software.amazon.smithy.rust.codegen.rustlang.Writable
@@ -23,17 +28,24 @@ import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
import software.amazon.smithy.rust.codegen.smithy.letIf
import software.amazon.smithy.rust.codegen.smithy.protocols.AllowInvalidXmlRoot
import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolMap
import software.amazon.smithy.rust.codegen.smithy.protocols.RestXml
import software.amazon.smithy.rust.codegen.smithy.protocols.RestXmlFactory
import software.amazon.smithy.rustsdk.AwsRuntimeType
import java.util.logging.Logger

/**
 * Top level decorator for S3
 */
class S3Decorator : RustCodegenDecorator<ClientCodegenContext> {
    override val name: String = "S3ExtendedError"
    override val name: String = "S3"
    override val order: Byte = 0
    private val logger: Logger = Logger.getLogger(javaClass.name)
    private val invalidXmlRootAllowList = setOf(
        // API returns GetObjectAttributes_Response_ instead of Output
        ShapeId.from("com.amazonaws.s3#GetObjectAttributesOutput"),
    )

    private fun applies(serviceId: ShapeId) =
        serviceId == ShapeId.from("com.amazonaws.s3#AmazonS3")
@@ -50,6 +62,17 @@ class S3Decorator : RustCodegenDecorator<ClientCodegenContext> {
            )
        }

    override fun transformModel(service: ServiceShape, model: Model): Model {
        return model.letIf(applies(service.id)) {
            ModelTransformer.create().mapShapes(model) { shape ->
                shape.letIf(isInInvalidXmlRootAllowList(shape)) {
                    logger.info("Adding AllowInvalidXmlRoot trait to $shape")
                    (shape as StructureShape).toBuilder().addTrait(AllowInvalidXmlRoot()).build()
                }
            }
        }
    }

    override fun libRsCustomizations(
        codegenContext: ClientCodegenContext,
        baseCustomizations: List<LibRsCustomization>,
@@ -59,6 +82,10 @@ class S3Decorator : RustCodegenDecorator<ClientCodegenContext> {

    override fun supportsCodegenContext(clazz: Class<out CoreCodegenContext>): Boolean =
        clazz.isAssignableFrom(ClientCodegenContext::class.java)

    private fun isInInvalidXmlRootAllowList(shape: Shape): Boolean {
        return shape.isStructureShape && invalidXmlRootAllowList.contains(shape.id)
    }
}

class S3(coreCodegenContext: CoreCodegenContext) : RestXml(coreCodegenContext) {
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

use aws_http::user_agent::AwsUserAgent;
use aws_sdk_s3::{
    middleware::DefaultMiddleware, model::ObjectAttributes, operation::GetObjectAttributes,
    Credentials, Region,
};
use aws_smithy_client::{test_connection::TestConnection, Client as CoreClient};
use aws_smithy_http::body::SdkBody;
use std::time::{Duration, UNIX_EPOCH};

pub type Client<C> = CoreClient<C, DefaultMiddleware>;

const RESPONSE_BODY_XML: &[u8] = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<GetObjectAttributesResponse xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Checksum><ChecksumSHA1>e1AsOh9IyGCa4hLN+2Od7jlnP14=</ChecksumSHA1></Checksum></GetObjectAttributesResponse>";

#[tokio::test]
async fn ignore_invalid_xml_body_root() {
    tracing_subscriber::fmt::init();

    let conn = TestConnection::new(vec![
        (http::Request::builder()
             .header("x-amz-object-attributes", "Checksum")
             .header("x-amz-user-agent", "aws-sdk-rust/0.123.test api/test-service/0.123 os/windows/XPSP3 lang/rust/1.50.0")
             .header("x-amz-date", "20210618T170728Z")
             .header("authorization", "AWS4-HMAC-SHA256 Credential=ANOTREAL/20210618/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-object-attributes;x-amz-security-token;x-amz-user-agent, Signature=0e6ec749db5a0af07890a83f553319eda95be0e498d058c64880471a474c5378")
             .header("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
             .header("x-amz-security-token", "notarealsessiontoken")
             .uri(http::Uri::from_static("https://s3.us-east-1.amazonaws.com/some-test-bucket/test.txt?attributes"))
             .body(SdkBody::empty())
             .unwrap(),
         http::Response::builder()
             .header(
                 "x-amz-id-2",
                 "rbipIUyF3YKPIcqpz6hrP9x9mzYMSqkHzDEp6TEN/STcKvylDIE/LLN6x9t6EKJRrgctNsdNHWk=",
             )
             .header("x-amz-request-id", "K8036R3D4NZNMMVC")
             .header("date", "Tue, 23 Aug 2022 18:17:23 GMT")
             .header("last-modified", "Tue, 21 Jun 2022 16:30:01 GMT")
             .header("server", "AmazonS3")
             .header("content-length", "224")
             .status(200)
             .body(RESPONSE_BODY_XML)
             .unwrap())
    ]);
    let creds = Credentials::new(
        "ANOTREAL",
        "notrealrnrELgWzOk3IfjzDKtFBhDby",
        Some("notarealsessiontoken".to_string()),
        None,
        "test",
    );
    let conf = aws_sdk_s3::Config::builder()
        .credentials_provider(creds)
        .region(Region::new("us-east-1"))
        .build();
    let client = Client::new(conn.clone());

    let mut op = GetObjectAttributes::builder()
        .bucket("some-test-bucket")
        .key("test.txt")
        .object_attributes(ObjectAttributes::Checksum)
        .build()
        .unwrap()
        .make_operation(&conf)
        .await
        .unwrap();
    op.properties_mut()
        .insert(UNIX_EPOCH + Duration::from_secs(1624036048));
    op.properties_mut().insert(AwsUserAgent::for_tests());

    let res = client.call(op).await.unwrap();

    conn.assert_requests_match(&[]);

    println!("res: {:#?}", res)
}
+12 −0
Original line number Diff line number Diff line
@@ -6,7 +6,10 @@
package software.amazon.smithy.rust.codegen.smithy.protocols

import software.amazon.smithy.aws.traits.protocols.RestXmlTrait
import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.AnnotationTrait
import software.amazon.smithy.model.traits.TimestampFormatTrait
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.RustModule
@@ -110,3 +113,12 @@ open class RestXml(private val coreCodegenContext: CoreCodegenContext) : Protoco

    override fun serverRouterRuntimeConstructor() = "new_rest_xml_router"
}

/**
 * Indicates that a service is expected to send XML where the root element name does not match the modeled member name.
 */
class AllowInvalidXmlRoot : AnnotationTrait(ID, Node.objectNode()) {
    companion object {
        val ID: ShapeId = ShapeId.from("smithy.api.internal#allowInvalidXmlRoot")
    }
}
Loading