opensearch 2.4.0

Official OpenSearch Rust client
Documentation
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * The OpenSearch Contributors require contributions made to
 * this file be licensed under the Apache-2.0 license or a
 * compatible open source license.
 *
 * Modifications Copyright OpenSearch Contributors. See
 * GitHub history for details.
 */

#![cfg(feature = "aws-auth")]

pub mod common;
use aws_credential_types::{provider::SharedCredentialsProvider, Credentials as AwsCredentials};
use aws_smithy_async::time::StaticTimeSource;
use aws_types::region::Region;
use common::{server::MockServer, tracing_init};
use opensearch::{
    http::{headers::HOST, transport::TransportBuilder},
    indices::IndicesCreateParts,
};
use reqwest::header::HeaderValue;
use serde_json::json;
use test_case::test_case;

fn sigv4_config(transport: TransportBuilder, service_name: &str) -> TransportBuilder {
    let aws_creds = AwsCredentials::new("test-access-key", "test-secret-key", None, None, "test");
    let region = Region::new("ap-southeast-2");
    let time_source = StaticTimeSource::from_secs(1673626117); // 2023-01-13 16:08:37 +0000

    transport
        .auth(opensearch::auth::Credentials::AwsSigV4(
            SharedCredentialsProvider::new(aws_creds),
            region,
        ))
        .service_name(service_name)
        .sigv4_time_source(time_source.into())
}

const LOCALHOST: HeaderValue = HeaderValue::from_static("localhost");

#[test_case("es", "10c9be415f4b9f15b12abbb16bd3e3730b2e6c76e0cf40db75d08a44ed04a3a1"; "when service name is es")]
#[test_case("aoss", "34903aef90423aa7dd60575d3d45316c6ef2d57bbe564a152b41bf8f5917abf6"; "when service name is aoss")]
#[test_case("arbitrary", "156e65c504ea2b2722a481b7515062e7692d27217b477828854e715f507e6a36"; "when service name is arbitrary")]
#[tokio::test]
async fn aws_auth_signs_correctly(
    service_name: &str,
    expected_signature: &str,
) -> anyhow::Result<()> {
    tracing_init();

    let mut server = MockServer::start()?;

    let host = format!("aaabbbcccddd111222333.ap-southeast-2.{service_name}.amazonaws.com");

    let client =
        server.client_with(|b| sigv4_config(b, service_name).header(HOST, host.parse().unwrap()));

    let _ = client
        .indices()
        .create(IndicesCreateParts::Index("sample-index1"))
        .body(json!({
            "aliases": {
                "sample-alias1": {}
            },
            "mappings": {
                "properties": {
                    "age": {
                        "type": "integer"
                    }
                }
            },
            "settings": {
                "index.number_of_replicas": 1,
                "index.number_of_shards": 2
            }
        }))
        .send()
        .await?;

    let sent_req = server.received_request().await?;

    assert_eq!(sent_req.header("accept"), Some("application/json"));
    assert_eq!(sent_req.header("content-type"), Some("application/json"));
    assert_eq!(sent_req.header("host"), Some(host.as_str()));
    assert_eq!(sent_req.header("x-amz-date"), Some("20230113T160837Z"));
    assert_eq!(
        sent_req.header("x-amz-content-sha256"),
        Some("4c770eaed349122a28302ff73d34437cad600acda5a9dd373efc7da2910f8564")
    );
    assert_eq!(sent_req.header("authorization"), Some(format!("AWS4-HMAC-SHA256 Credential=test-access-key/20230113/ap-southeast-2/{service_name}/aws4_request, SignedHeaders=accept;content-type;host;x-amz-content-sha256;x-amz-date, Signature={expected_signature}").as_str()));

    Ok(())
}

#[tokio::test]
async fn aws_auth_get() -> anyhow::Result<()> {
    tracing_init();

    let mut server = MockServer::start()?;

    let client = server.client_with(|b| sigv4_config(b, "custom").header(HOST, LOCALHOST));

    let _ = client.ping().send().await?;

    let sent_req = server.received_request().await?;

    assert_eq!(sent_req.header("authorization"), Some("AWS4-HMAC-SHA256 Credential=test-access-key/20230113/ap-southeast-2/custom/aws4_request, SignedHeaders=accept;content-type;host;x-amz-content-sha256;x-amz-date, Signature=e5aa6e5d9e1b86b86ed31fbb10dd62b4e93423b77830f8189701421d3e9f65bd"));
    assert_eq!(
        sent_req.header("x-amz-content-sha256"),
        Some("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
    ); // SHA of zero-length body

    Ok(())
}

#[tokio::test]
async fn aws_auth_post() -> anyhow::Result<()> {
    tracing_init();

    let mut server = MockServer::start()?;

    let client = server.client_with(|b| sigv4_config(b, "custom").header(HOST, LOCALHOST));

    let _ = client
        .index(opensearch::IndexParts::Index("movies"))
        .body(serde_json::json!({
                "title": "Moneyball",
                "director": "Bennett Miller",
                "year": 2011
            }
        ))
        .send()
        .await?;

    let sent_req = server.received_request().await?;

    assert_eq!(
        sent_req.header("x-amz-content-sha256"),
        Some("f3a842f988a653a734ebe4e57c45f19293a002241a72f0b3abbff71e4f5297b9")
    ); // SHA of the JSON

    Ok(())
}