use crate::models::{Credential, ResourceType};
use azure_core::http::Method;
use tracing::trace;
use crate::models::ResourcePaths;
const COSMOS_AAD_SCOPE: &str = "https://cosmos.azure.com/.default";
pub(crate) enum ResourceLink {
Paths(ResourcePaths),
Owned(String),
}
impl ResourceLink {
pub(crate) fn as_str(&self) -> &str {
match self {
Self::Paths(p) => p.signing_link(),
Self::Owned(s) => s.as_str(),
}
}
}
impl std::fmt::Debug for ResourceLink {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub(crate) struct AuthorizationContext {
pub(crate) method: Method,
pub(crate) resource_type: ResourceType,
pub(crate) resource_link: ResourceLink,
}
impl AuthorizationContext {
pub(crate) fn new(
method: Method,
resource_type: ResourceType,
resource_link: impl Into<String>,
) -> Self {
Self {
method,
resource_type,
resource_link: ResourceLink::Owned(resource_link.into()),
}
}
pub(crate) fn from_paths(
method: Method,
resource_type: ResourceType,
paths: ResourcePaths,
) -> Self {
Self {
method,
resource_type,
resource_link: ResourceLink::Paths(paths),
}
}
}
pub(crate) async fn generate_authorization(
credential: &Credential,
auth_ctx: &AuthorizationContext,
date_string: &str,
) -> azure_core::Result<String> {
let token = match credential {
Credential::TokenCredential(cred) => {
let token = cred
.get_token(&[COSMOS_AAD_SCOPE], None)
.await?
.token
.secret()
.to_string();
let mut s = String::with_capacity(20 + token.len());
s.push_str("type=aad&ver=1.0&sig=");
s.push_str(&token);
s
}
Credential::MasterKey(key) => {
let string_to_sign = build_string_to_sign(auth_ctx, date_string);
trace!(signature_payload = ?string_to_sign, "generating Cosmos auth signature");
let signature = azure_core::hmac::hmac_sha256(&string_to_sign, key)?;
let mut s = String::with_capacity(24 + signature.len());
s.push_str("type=master&ver=1.0&sig=");
s.push_str(&signature);
s
}
};
Ok(url_encode(&token))
}
fn build_string_to_sign(auth_ctx: &AuthorizationContext, date_string: &str) -> String {
let method_str = match auth_ctx.method {
Method::Get => "get",
Method::Put => "put",
Method::Post => "post",
Method::Delete => "delete",
Method::Head => "head",
Method::Patch => "patch",
_ => "extension",
};
let resource_type = auth_ctx.resource_type.path_segment();
let resource_link = auth_ctx.resource_link.as_str();
let capacity =
method_str.len() + resource_type.len() + resource_link.len() + date_string.len() + 6;
let mut s = String::with_capacity(capacity);
use std::fmt::Write as _;
let _ = write!(
s,
"{method_str}\n{resource_type}\n{resource_link}\n{date_string}\n\n"
);
s
}
fn url_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
out.extend(url::form_urlencoded::byte_serialize(s.as_bytes()));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_string_to_sign_format() {
let auth_ctx = AuthorizationContext::new(
Method::Get,
ResourceType::DocumentCollection,
"dbs/MyDatabase/colls/MyCollection",
);
let date_string = "mon, 01 jan 1900 01:00:00 gmt";
let result = build_string_to_sign(&auth_ctx, date_string);
let expected =
"get\ncolls\ndbs/MyDatabase/colls/MyCollection\nmon, 01 jan 1900 01:00:00 gmt\n\n";
assert_eq!(result, expected);
}
#[test]
fn build_string_to_sign_for_feed() {
let auth_ctx = AuthorizationContext::new(Method::Get, ResourceType::Database, "");
let date_string = "mon, 01 jan 1900 01:00:00 gmt";
let result = build_string_to_sign(&auth_ctx, date_string);
let expected = "get\ndbs\n\nmon, 01 jan 1900 01:00:00 gmt\n\n";
assert_eq!(result, expected);
}
}