azure_data_cosmos 0.32.0

Rust wrappers around Microsoft Azure REST APIs - Azure Cosmos DB
Documentation
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#[cfg_attr(not(feature = "key_auth"), allow(unused_imports))]
use azure_core::{credentials::Secret, hmac::hmac_sha256, http::Method};

use crate::resource_context::ResourceLink;

#[cfg_attr(not(feature = "key_auth"), allow(dead_code))]
pub(crate) struct SignatureTarget<'a> {
    http_method: Method,
    link: &'a ResourceLink,
    date_string: &'a str,
}

impl<'a> SignatureTarget<'a> {
    pub fn new(http_method: Method, link: &'a ResourceLink, date_string: &'a str) -> Self {
        SignatureTarget {
            http_method,
            link,
            date_string,
        }
    }

    #[cfg(feature = "key_auth")]
    pub fn into_authorization(self, key: &Secret) -> azure_core::Result<String> {
        let string_to_sign = self.into_signable_string();
        // The signature payload is NOT SECRET. The signature IS SECRET, but we can safely log the signature payload (which can be useful for diagnosing auth errors)
        tracing::debug!(signature_payload = ?string_to_sign, "generating Cosmos auth signature");
        let signature = hmac_sha256(&string_to_sign, key)?;
        Ok(format!("type=master&ver=1.0&sig={signature}"))
    }

    /// This function generates a valid authorization string, according to the documentation.
    /// In case of authorization problems we can compare the `string_to_sign` generated by Azure against
    /// our own.
    #[cfg(feature = "key_auth")]
    fn into_signable_string(self) -> String {
        // From official docs:
        // StringToSign =
        //      Verb.toLowerCase() + "\n" +
        //      ResourceType.toLowerCase() + "\n" +
        //      ResourceLink + "\n" +
        //      Date.toLowerCase() + "\n" +
        //      "" + "\n";
        // Notice the empty string at the end so we need to add two new lines

        format!(
            "{}\n{}\n{}\n{}\n\n",
            // Cosmos' signature algorithm requires lower-case methods, so we use our own match instead of the impl of AsRef<str>, which is uppercase.
            match self.http_method {
                Method::Get => "get",
                Method::Put => "put",
                Method::Post => "post",
                Method::Delete => "delete",
                Method::Head => "head",
                Method::Patch => "patch",
                _ => "extension",
            },
            self.link.resource_type().path_segment(),
            self.link.link_for_signing(),
            self.date_string,
        )
    }
}

#[cfg(test)]
#[cfg(feature = "key_auth")]
mod tests {
    use azure_core::{http::Method, time};

    use crate::{
        pipeline::signature_target::SignatureTarget,
        resource_context::{ResourceLink, ResourceType},
    };

    // We test the full authorization header in authorization_policy.
    // However, testing the signable string here is useful to isolate failures in constructing the string to be signed
    #[test]
    fn into_signable_string_generates_correct_value() {
        let time_nonce = time::parse_rfc3339("1900-01-01T01:00:00.000000000+00:00").unwrap();
        let date_string = time::to_rfc7231(&time_nonce).to_lowercase();

        let ret = SignatureTarget::new(
            Method::Get,
            &ResourceLink::root(ResourceType::Databases)
                .item("MyDatabase")
                .feed(ResourceType::Containers)
                .item("MyCollection"),
            &date_string,
        )
        .into_signable_string();
        assert_eq!(
            ret,
            "get
colls
dbs/MyDatabase/colls/MyCollection
mon, 01 jan 1900 01:00:00 gmt

"
        );
    }

    #[test]
    fn into_signable_string_does_not_url_encode_rid_links() {
        let time_nonce = time::parse_rfc3339("1900-01-01T01:00:00.000000000+00:00").unwrap();
        let date_string = time::to_rfc7231(&time_nonce).to_lowercase();

        let ret = SignatureTarget::new(
            Method::Get,
            &ResourceLink::root(ResourceType::Databases)
                .item_by_rid("ABCDEF==")
                .feed(ResourceType::Containers)
                .item_by_rid("XYZ123+="),
            &date_string,
        )
        .into_signable_string();
        assert_eq!(
            ret,
            "get
colls
dbs/ABCDEF==/colls/XYZ123+=
mon, 01 jan 1900 01:00:00 gmt

"
        );
    }
}