From 37db397034d1ea2e84aa7991da9c19c916f38273 Mon Sep 17 00:00:00 2001 From: John Batty Date: Thu, 17 Aug 2023 11:36:06 +0100 Subject: [PATCH 1/4] Implement Cosmos AAD authentication --- sdk/data_cosmos/src/authorization_policy.rs | 51 ++++++++++++++----- .../permission/authorization_token.rs | 27 +++++++++- .../resources/permission/permission_token.rs | 5 ++ 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/sdk/data_cosmos/src/authorization_policy.rs b/sdk/data_cosmos/src/authorization_policy.rs index 1c8698de37..ed11f934cd 100644 --- a/sdk/data_cosmos/src/authorization_policy.rs +++ b/sdk/data_cosmos/src/authorization_policy.rs @@ -26,7 +26,7 @@ const VERSION_NUMBER: &str = "1.0"; /// /// This struct implements `Debug` but secrets are encrypted by `AuthorizationToken` so there is no risk of /// leaks in debug logs (secrets are stored in cleartext in memory: dumps are still leaky). -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct AuthorizationPolicy { authorization_token: AuthorizationToken, } @@ -67,6 +67,7 @@ impl Policy for AuthorizationPolicy { &resource_link, time_nonce, ) + .await? }; trace!( @@ -136,17 +137,23 @@ fn generate_resource_link(request: &Request) -> String { } } -/// The `CosmosDB` authorization can either be "primary" (i.e., one of the two service-level tokens) or -/// "resource" (i.e., a single database). In the first case the signature must be constructed by -/// signing the HTTP method, resource type, resource link (the relative URI) and the current time. -/// In the second case, the signature is just the resource key. -fn generate_authorization( +/// The `CosmosDB` authorization can either be: +/// - "primary": one of the two service-level tokens +/// - "resource: e.g. a single database +/// - "aad": Azure Active Directory token +/// In the "primary" case the signature must be constructed by signing the HTTP method, +/// resource type, resource link (the relative URI) and the current time. +/// +/// In the "resource" case, the signature is just the resource key. +/// +/// In the "aad" case, the signature is the AAD token. +async fn generate_authorization( auth_token: &AuthorizationToken, http_method: &azure_core::Method, resource_type: &ResourceType, resource_link: &str, time_nonce: OffsetDateTime, -) -> String { +) -> azure_core::Result { let (authorization_type, signature) = match auth_token { AuthorizationToken::Primary(key) => { let string_to_sign = @@ -157,6 +164,17 @@ fn generate_authorization( ) } AuthorizationToken::Resource(key) => ("resource", Cow::Borrowed(key)), + AuthorizationToken::TokenCredential(token_credential) => ( + "aad", + Cow::Owned( + token_credential + .get_token(resource_link) + .await? + .token + .secret() + .to_string(), + ), + ), }; let str_unencoded = format!("type={authorization_type}&ver={VERSION_NUMBER}&sig={signature}"); @@ -165,7 +183,7 @@ fn generate_authorization( str_unencoded ); - form_urlencoded::byte_serialize(str_unencoded.as_bytes()).collect::() + Ok(form_urlencoded::byte_serialize(str_unencoded.as_bytes()).collect::()) } /// This function generates a valid authorization string, according to the documentation. @@ -255,8 +273,8 @@ mon, 01 jan 1900 01:00:00 gmt ); } - #[test] - fn generate_authorization_00() { + #[tokio::test] + async fn generate_authorization_00() { let time = date::parse_rfc3339("1900-01-01T01:00:00.000000000+00:00").unwrap(); let auth_token = AuthorizationToken::primary_from_base64( @@ -270,15 +288,18 @@ mon, 01 jan 1900 01:00:00 gmt &ResourceType::Databases, "dbs/MyDatabase/colls/MyCollection", time, - ); + ) + .await + .unwrap(); + assert_eq!( ret, "type%3Dmaster%26ver%3D1.0%26sig%3DQkz%2Fr%2B1N2%2BPEnNijxGbGB%2FADvLsLBQmZ7uBBMuIwf4I%3D" ); } - #[test] - fn generate_authorization_01() { + #[tokio::test] + async fn generate_authorization_01() { let time = date::parse_rfc3339("2017-04-27T00:51:12.000000000+00:00").unwrap(); let auth_token = AuthorizationToken::primary_from_base64( @@ -292,7 +313,9 @@ mon, 01 jan 1900 01:00:00 gmt &ResourceType::Databases, "dbs/ToDoList", time, - ); + ) + .await + .unwrap(); // This is the result shown in the MSDN page. It's clearly wrong :) // below is the correct one. diff --git a/sdk/data_cosmos/src/resources/permission/authorization_token.rs b/sdk/data_cosmos/src/resources/permission/authorization_token.rs index af15fa6486..cb5feb52f6 100644 --- a/sdk/data_cosmos/src/resources/permission/authorization_token.rs +++ b/sdk/data_cosmos/src/resources/permission/authorization_token.rs @@ -1,21 +1,40 @@ use super::PermissionToken; +use azure_core::auth::TokenCredential; use azure_core::{ base64, error::{Error, ErrorKind}, }; use std::fmt; +use std::sync::Arc; /// Authorization tokens for accessing Cosmos. /// /// Learn more about the different types of tokens [here](https://docs.microsoft.com/azure/cosmos-db/secure-access-to-data). -#[derive(PartialEq, Clone, Eq)] +#[derive(Clone)] pub enum AuthorizationToken { /// Used for administrative resources: database accounts, databases, users, and permissions Primary(Vec), /// Used for application resources: containers, documents, attachments, stored procedures, triggers, and UDFs Resource(String), + /// AAD token credential + TokenCredential(Arc), } +impl PartialEq for AuthorizationToken { + fn eq(&self, other: &Self) -> bool { + use AuthorizationToken::*; + match (self, other) { + (Primary(a), Primary(b)) => a == b, + (Resource(a), Resource(b)) => a == b, + // Consider two token credentials equal if they point to the same object. + (TokenCredential(a), TokenCredential(b)) => Arc::ptr_eq(a, b), + _ => false, + } + } +} + +impl Eq for AuthorizationToken {} + impl AuthorizationToken { /// Create a primary `AuthorizationToken` from base64 encoded data /// @@ -32,6 +51,11 @@ impl AuthorizationToken { pub fn new_resource(resource: String) -> AuthorizationToken { AuthorizationToken::Resource(resource) } + + /// Create an `AuthorizationToken` from a `TokenCredential`. + pub fn from_token_credential(token_credential: Arc) -> AuthorizationToken { + AuthorizationToken::TokenCredential(token_credential) + } } impl fmt::Debug for AuthorizationToken { @@ -43,6 +67,7 @@ impl fmt::Debug for AuthorizationToken { match self { AuthorizationToken::Primary(_) => "Master", AuthorizationToken::Resource(_) => "Resource", + AuthorizationToken::TokenCredential(_) => "TokenCredential", } ) } diff --git a/sdk/data_cosmos/src/resources/permission/permission_token.rs b/sdk/data_cosmos/src/resources/permission/permission_token.rs index 9454f3e26e..d1aa88371a 100644 --- a/sdk/data_cosmos/src/resources/permission/permission_token.rs +++ b/sdk/data_cosmos/src/resources/permission/permission_token.rs @@ -30,6 +30,11 @@ impl std::fmt::Display for PermissionToken { let (permission_type, signature) = match &self.token { AuthorizationToken::Resource(s) => ("resource", Cow::Borrowed(s)), AuthorizationToken::Primary(s) => ("master", Cow::Owned(base64::encode(s))), + // @@@TODO: Do we need to support TokenCredential for PermissionToken? + // It is painful to implement because we can't easily get the + // token string from the TokenCredential (the function is async and fallible, + // and fmt() is neither!). + AuthorizationToken::TokenCredential(_s) => ("aad", Cow::Owned("xxx".to_string())), }; write!( f, From e26458e51fcf45c4ddb7831b6e96247363b253c2 Mon Sep 17 00:00:00 2001 From: John Batty Date: Wed, 23 Aug 2023 08:55:08 +0100 Subject: [PATCH 2/4] Generate AAD token scope from URL --- sdk/data_cosmos/src/authorization_policy.rs | 28 +++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/sdk/data_cosmos/src/authorization_policy.rs b/sdk/data_cosmos/src/authorization_policy.rs index ed11f934cd..544956a5dd 100644 --- a/sdk/data_cosmos/src/authorization_policy.rs +++ b/sdk/data_cosmos/src/authorization_policy.rs @@ -2,7 +2,7 @@ use crate::resources::permission::AuthorizationToken; use crate::resources::ResourceType; use azure_core::base64; use azure_core::headers::{HeaderValue, AUTHORIZATION, MS_DATE, VERSION}; -use azure_core::{date, Context, Policy, PolicyResult, Request}; +use azure_core::{date, Context, Policy, PolicyResult, Request, Url}; use hmac::{Hmac, Mac}; use sha2::Sha256; use std::borrow::Cow; @@ -62,6 +62,7 @@ impl Policy for AuthorizationPolicy { generate_authorization( &self.authorization_token, request.method(), + request.url(), ctx.get() .expect("ResourceType must be in the Context at this point"), &resource_link, @@ -150,6 +151,7 @@ fn generate_resource_link(request: &Request) -> String { async fn generate_authorization( auth_token: &AuthorizationToken, http_method: &azure_core::Method, + url: &Url, resource_type: &ResourceType, resource_link: &str, time_nonce: OffsetDateTime, @@ -168,7 +170,7 @@ async fn generate_authorization( "aad", Cow::Owned( token_credential - .get_token(resource_link) + .get_token(&scope_from_url(url)) .await? .token .secret() @@ -186,6 +188,14 @@ async fn generate_authorization( Ok(form_urlencoded::byte_serialize(str_unencoded.as_bytes()).collect::()) } +/// This function generates the scope string from the passed url. The scope string is used to +/// request the AAD token. +fn scope_from_url(url: &Url) -> String { + let scheme = url.scheme(); + let hostname = url.host_str().unwrap(); + return format!("{scheme}://{hostname}/.default"); +} + /// 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. @@ -282,9 +292,12 @@ mon, 01 jan 1900 01:00:00 gmt ) .unwrap(); + let url = azure_core::Url::parse("https://.documents.azure.com/dbs/ToDoList").unwrap(); + let ret = generate_authorization( &auth_token, &azure_core::Method::Get, + &url, &ResourceType::Databases, "dbs/MyDatabase/colls/MyCollection", time, @@ -307,9 +320,12 @@ mon, 01 jan 1900 01:00:00 gmt ) .unwrap(); + let url = azure_core::Url::parse("https://.documents.azure.com/dbs/ToDoList").unwrap(); + let ret = generate_authorization( &auth_token, &azure_core::Method::Get, + &url, &ResourceType::Databases, "dbs/ToDoList", time, @@ -363,4 +379,12 @@ mon, 01 jan 1900 01:00:00 gmt ); assert_eq!(&generate_resource_link(&request), "dbs/test_db"); } + + #[test] + fn scope_from_url_01() { + let scope = scope_from_url( + &azure_core::Url::parse("https://.documents.azure.com/dbs/test_db/colls").unwrap(), + ); + assert_eq!(scope, "https://.documents.azure.com/.default"); + } } From 10df9daeae4162af913f1f3ac1754017e0cb9776 Mon Sep 17 00:00:00 2001 From: John Batty Date: Wed, 23 Aug 2023 14:22:49 +0100 Subject: [PATCH 3/4] Fix up scope generation --- sdk/data_cosmos/src/authorization_policy.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/data_cosmos/src/authorization_policy.rs b/sdk/data_cosmos/src/authorization_policy.rs index 544956a5dd..a954fe46da 100644 --- a/sdk/data_cosmos/src/authorization_policy.rs +++ b/sdk/data_cosmos/src/authorization_policy.rs @@ -193,7 +193,9 @@ async fn generate_authorization( fn scope_from_url(url: &Url) -> String { let scheme = url.scheme(); let hostname = url.host_str().unwrap(); - return format!("{scheme}://{hostname}/.default"); + // TODO: Investigate why this did not work in testing... + //return format!("{scheme}://{hostname}/.default"); + return format!("{scheme}://{hostname}"); } /// This function generates a valid authorization string, according to the documentation. @@ -385,6 +387,6 @@ mon, 01 jan 1900 01:00:00 gmt let scope = scope_from_url( &azure_core::Url::parse("https://.documents.azure.com/dbs/test_db/colls").unwrap(), ); - assert_eq!(scope, "https://.documents.azure.com/.default"); + assert_eq!(scope, "https://.documents.azure.com"); } } From dc15804b4844fbfa6b0ed8d4282f38c2d392b039 Mon Sep 17 00:00:00 2001 From: John Batty Date: Tue, 29 Aug 2023 22:23:48 +0100 Subject: [PATCH 4/4] Cleanups --- sdk/data_cosmos/src/authorization_policy.rs | 6 ++--- .../permission/authorization_token.rs | 15 ------------ .../src/resources/permission/permission.rs | 3 ++- .../permission/permission_response.rs | 2 +- .../resources/permission/permission_token.rs | 23 ++++++++++++++----- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/sdk/data_cosmos/src/authorization_policy.rs b/sdk/data_cosmos/src/authorization_policy.rs index a954fe46da..812579b7e3 100644 --- a/sdk/data_cosmos/src/authorization_policy.rs +++ b/sdk/data_cosmos/src/authorization_policy.rs @@ -140,7 +140,7 @@ fn generate_resource_link(request: &Request) -> String { /// The `CosmosDB` authorization can either be: /// - "primary": one of the two service-level tokens -/// - "resource: e.g. a single database +/// - "resource": e.g. a single database /// - "aad": Azure Active Directory token /// In the "primary" case the signature must be constructed by signing the HTTP method, /// resource type, resource link (the relative URI) and the current time. @@ -193,9 +193,7 @@ async fn generate_authorization( fn scope_from_url(url: &Url) -> String { let scheme = url.scheme(); let hostname = url.host_str().unwrap(); - // TODO: Investigate why this did not work in testing... - //return format!("{scheme}://{hostname}/.default"); - return format!("{scheme}://{hostname}"); + format!("{scheme}://{hostname}") } /// This function generates a valid authorization string, according to the documentation. diff --git a/sdk/data_cosmos/src/resources/permission/authorization_token.rs b/sdk/data_cosmos/src/resources/permission/authorization_token.rs index cb5feb52f6..89cca9d01a 100644 --- a/sdk/data_cosmos/src/resources/permission/authorization_token.rs +++ b/sdk/data_cosmos/src/resources/permission/authorization_token.rs @@ -20,21 +20,6 @@ pub enum AuthorizationToken { TokenCredential(Arc), } -impl PartialEq for AuthorizationToken { - fn eq(&self, other: &Self) -> bool { - use AuthorizationToken::*; - match (self, other) { - (Primary(a), Primary(b)) => a == b, - (Resource(a), Resource(b)) => a == b, - // Consider two token credentials equal if they point to the same object. - (TokenCredential(a), TokenCredential(b)) => Arc::ptr_eq(a, b), - _ => false, - } - } -} - -impl Eq for AuthorizationToken {} - impl AuthorizationToken { /// Create a primary `AuthorizationToken` from base64 encoded data /// diff --git a/sdk/data_cosmos/src/resources/permission/permission.rs b/sdk/data_cosmos/src/resources/permission/permission.rs index f80acaa0a4..20f2b9cbe2 100644 --- a/sdk/data_cosmos/src/resources/permission/permission.rs +++ b/sdk/data_cosmos/src/resources/permission/permission.rs @@ -10,7 +10,8 @@ use std::borrow::Cow; /// access to a specific resource. It is used to manage access to collections, documents, /// attachments, stored procedures, triggers, and user-defined functions for a particular user. /// You can learn more about permissions [here](https://docs.microsoft.com/rest/api/cosmos-db/permissions). -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +//#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Permission { /// The unique name that identifies the permission. pub id: String, diff --git a/sdk/data_cosmos/src/resources/permission/permission_response.rs b/sdk/data_cosmos/src/resources/permission/permission_response.rs index fbb4ed77e9..a18d559446 100644 --- a/sdk/data_cosmos/src/resources/permission/permission_response.rs +++ b/sdk/data_cosmos/src/resources/permission/permission_response.rs @@ -5,7 +5,7 @@ use azure_core::Response as HttpResponse; use super::Permission; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct PermissionResponse { pub permission: Permission, pub charge: f64, diff --git a/sdk/data_cosmos/src/resources/permission/permission_token.rs b/sdk/data_cosmos/src/resources/permission/permission_token.rs index d1aa88371a..4783e86d9d 100644 --- a/sdk/data_cosmos/src/resources/permission/permission_token.rs +++ b/sdk/data_cosmos/src/resources/permission/permission_token.rs @@ -9,12 +9,25 @@ const SIGNATURE_PREFIX: &str = "sig="; /// /// This field is a url encoded string with the type of permission, the signature, and the version (currently only 1.0) /// This type is a wrapper around `AuthorizationToken`. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(try_from = "String")] pub struct PermissionToken { pub(crate) token: AuthorizationToken, } +impl PartialEq for PermissionToken { + fn eq(&self, other: &Self) -> bool { + use AuthorizationToken::*; + match (&self.token, &other.token) { + (Primary(a), Primary(b)) => a == b, + (Resource(a), Resource(b)) => a == b, + _ => false, + } + } +} + +impl Eq for PermissionToken {} + impl serde::Serialize for PermissionToken { fn serialize(&self, serializer: S) -> Result where @@ -30,11 +43,9 @@ impl std::fmt::Display for PermissionToken { let (permission_type, signature) = match &self.token { AuthorizationToken::Resource(s) => ("resource", Cow::Borrowed(s)), AuthorizationToken::Primary(s) => ("master", Cow::Owned(base64::encode(s))), - // @@@TODO: Do we need to support TokenCredential for PermissionToken? - // It is painful to implement because we can't easily get the - // token string from the TokenCredential (the function is async and fallible, - // and fmt() is neither!). - AuthorizationToken::TokenCredential(_s) => ("aad", Cow::Owned("xxx".to_string())), + AuthorizationToken::TokenCredential(_) => { + panic!("TokenCredential not supported for PermissionToken") + } }; write!( f,