diff --git a/sdk/core/src/date/iso8601.rs b/sdk/core/src/date/iso8601.rs new file mode 100644 index 0000000000..5ed35bbe19 --- /dev/null +++ b/sdk/core/src/date/iso8601.rs @@ -0,0 +1,73 @@ +use crate::error::{ErrorKind, ResultExt}; +use serde::{self, de, Deserialize, Deserializer, Serializer}; +use time::{ + format_description::well_known::{ + iso8601::{Config, EncodedConfig, TimePrecision}, + Iso8601, + }, + OffsetDateTime, UtcOffset, +}; + +const SERDE_CONFIG: EncodedConfig = Config::DEFAULT + .set_year_is_six_digits(false) + .set_time_precision(TimePrecision::Second { + decimal_digits: None, + }) + .encode(); + +pub fn parse_iso8601(s: &str) -> crate::Result { + OffsetDateTime::parse(s, &Iso8601::) + .with_context(ErrorKind::DataConversion, || { + format!("unable to parse iso8601 date '{s}") + }) +} + +pub fn to_iso8601(date: &OffsetDateTime) -> crate::Result { + date.format(&Iso8601::) + .with_context(ErrorKind::DataConversion, || { + format!("unable to format date '{date:?}") + }) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + parse_iso8601(&s).map_err(de::Error::custom) +} + +pub fn serialize(date: &OffsetDateTime, serializer: S) -> Result +where + S: Serializer, +{ + date.to_offset(UtcOffset::UTC); + let as_str = to_iso8601(date).map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&as_str) +} + +pub mod option { + use crate::date::iso8601::{parse_iso8601, to_iso8601}; + use serde::{Deserialize, Deserializer, Serializer}; + use time::OffsetDateTime; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let s: Option = Option::deserialize(deserializer)?; + s.map(|s| parse_iso8601(&s).map_err(serde::de::Error::custom)) + .transpose() + } + + pub fn serialize(date: &Option, serializer: S) -> Result + where + S: Serializer, + { + if let Some(date) = date { + serializer.serialize_str(&to_iso8601(date).map_err(serde::ser::Error::custom)?) + } else { + serializer.serialize_none() + } + } +} diff --git a/sdk/core/src/date/mod.rs b/sdk/core/src/date/mod.rs index c922665a74..80896127d6 100644 --- a/sdk/core/src/date/mod.rs +++ b/sdk/core/src/date/mod.rs @@ -14,6 +14,7 @@ use time::{ // Serde modules pub use time::serde::rfc3339; pub use time::serde::timestamp; +pub mod iso8601; pub mod rfc1123; /// RFC 3339: Date and Time on the Internet: Timestamps diff --git a/sdk/storage_blobs/Cargo.toml b/sdk/storage_blobs/Cargo.toml index ff65982f9f..fd6615d7a8 100644 --- a/sdk/storage_blobs/Cargo.toml +++ b/sdk/storage_blobs/Cargo.toml @@ -24,7 +24,7 @@ RustyXML = "0.3" serde = { version = "1.0" } serde_derive = "1.0" serde_json = "1.0" -uuid = { version = "1.0", features = ["v4"] } +uuid = { version = "1.0", features = ["v4", "serde"] } url = "2.2" [dev-dependencies] diff --git a/sdk/storage_blobs/examples/user_delegation_key.rs b/sdk/storage_blobs/examples/user_delegation_key.rs new file mode 100644 index 0000000000..44e98e0bfe --- /dev/null +++ b/sdk/storage_blobs/examples/user_delegation_key.rs @@ -0,0 +1,29 @@ +use azure_identity::DefaultAzureCredential; +use azure_storage::prelude::*; +use azure_storage_blobs::prelude::*; +use clap::Parser; +use std::{sync::Arc, time::Duration}; +use time::OffsetDateTime; + +#[derive(Debug, Parser)] +struct Args { + /// storage account name + #[clap(env = "STORAGE_ACCOUNT")] + account: String, +} + +#[tokio::main] +async fn main() -> azure_core::Result<()> { + env_logger::init(); + let args = Args::parse(); + + let storage_credentials = + StorageCredentials::token_credential(Arc::new(DefaultAzureCredential::default())); + let client = BlobServiceClient::new(&args.account, storage_credentials); + + let start = OffsetDateTime::now_utc(); + let expiry = start + Duration::from_secs(60 * 60); + let response = client.get_user_deligation_key(start, expiry).await?; + println!("{:#?}", response.user_deligation_key); + Ok(()) +} diff --git a/sdk/storage_blobs/src/clients/blob_service_client.rs b/sdk/storage_blobs/src/clients/blob_service_client.rs index 5c8ee48909..8f68a782f7 100644 --- a/sdk/storage_blobs/src/clients/blob_service_client.rs +++ b/sdk/storage_blobs/src/clients/blob_service_client.rs @@ -179,6 +179,14 @@ impl BlobServiceClient { ContainerClient::new(self.clone(), container_name.into()) } + pub fn get_user_deligation_key( + &self, + start: OffsetDateTime, + expiry: OffsetDateTime, + ) -> GetUserDelegationKeyBuilder { + GetUserDelegationKeyBuilder::new(self.clone(), start, expiry) + } + pub fn shared_access_signature( &self, resource_type: AccountSasResourceType, diff --git a/sdk/storage_blobs/src/service/operations/get_user_delegation_key.rs b/sdk/storage_blobs/src/service/operations/get_user_delegation_key.rs new file mode 100644 index 0000000000..34c3b4cb5e --- /dev/null +++ b/sdk/storage_blobs/src/service/operations/get_user_delegation_key.rs @@ -0,0 +1,144 @@ +use crate::prelude::BlobServiceClient; +use azure_core::{ + date::iso8601, + headers::Headers, + xml::{read_xml_str, to_xml}, + Method, +}; +use azure_storage::headers::CommonStorageResponseHeaders; +use time::OffsetDateTime; +use uuid::Uuid; + +operation! { + GetUserDelegationKey, + client: BlobServiceClient, + start_time: OffsetDateTime, + expiry_time: OffsetDateTime, +} + +impl GetUserDelegationKeyBuilder { + pub fn into_future(mut self) -> GetUserDelegationKey { + Box::pin(async move { + let mut url = self.client.url()?; + + url.query_pairs_mut() + .extend_pairs([("restype", "service"), ("comp", "userdelegationkey")]); + + let body = GetUserDelegationKeyRequest { + start: self.start_time, + expiry: self.expiry_time, + } + .as_string()?; + + let mut request = BlobServiceClient::finalize_request( + url, + Method::Post, + Headers::new(), + Some(body.into()), + )?; + + let response = self.client.send(&mut self.context, &mut request).await?; + + let (_, headers, body) = response.deconstruct(); + let body = body.collect_string().await?; + GetUserDelegationKeyResponse::try_from(&headers, &body) + }) + } +} + +#[derive(Serialize)] +#[serde(rename = "KeyInfo")] +struct GetUserDelegationKeyRequest { + #[serde(rename = "Start", with = "iso8601")] + start: OffsetDateTime, + #[serde(rename = "Expiry", with = "iso8601")] + expiry: OffsetDateTime, +} + +impl GetUserDelegationKeyRequest { + pub fn as_string(&self) -> azure_core::Result { + Ok(format!( + "{}", + to_xml(self)? + )) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "PascalCase")] +pub struct UserDeligationKey { + pub signed_oid: Uuid, + pub signed_tid: Uuid, + #[serde(with = "iso8601")] + pub signed_start: OffsetDateTime, + #[serde(with = "iso8601")] + pub signed_expiry: OffsetDateTime, + pub signed_service: String, + pub signed_version: String, + pub value: String, +} + +#[derive(Debug)] +pub struct GetUserDelegationKeyResponse { + pub common: CommonStorageResponseHeaders, + pub user_deligation_key: UserDeligationKey, +} + +impl GetUserDelegationKeyResponse { + pub(crate) fn try_from(headers: &Headers, body: &str) -> azure_core::Result { + let common = CommonStorageResponseHeaders::try_from(headers)?; + let user_deligation_key: UserDeligationKey = read_xml_str(body)?; + + Ok(Self { + common, + user_deligation_key, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + const BASIC_REQUEST: &str = "1970-01-01T00:00:00Z1970-01-01T00:00:01Z"; + const BASIC_RESPONSE: &str = " + + 00000000-0000-0000-0000-000000000000 + 00000000-0000-0000-0000-000000000001 + 1970-01-01T00:00:00Z + 1970-01-01T00:00:01Z + b + c + d + + "; + + #[test] + fn request_xml() -> azure_core::Result<()> { + let request = GetUserDelegationKeyRequest { + start: OffsetDateTime::from_unix_timestamp(0).unwrap(), + expiry: OffsetDateTime::from_unix_timestamp(1).unwrap(), + } + .as_string()?; + assert_eq!(BASIC_REQUEST, request); + Ok(()) + } + + #[test] + fn parse_response() -> azure_core::Result<()> { + let expected = UserDeligationKey { + signed_oid: Uuid::from_u128(0), + signed_tid: Uuid::from_u128(1), + signed_start: OffsetDateTime::from_unix_timestamp(0).unwrap(), + signed_expiry: OffsetDateTime::from_unix_timestamp(1).unwrap(), + signed_service: "b".to_owned(), + signed_version: "c".to_owned(), + value: "d".to_owned(), + }; + + let deserialized: UserDeligationKey = read_xml_str(BASIC_RESPONSE)?; + assert_eq!(deserialized, expected); + + Ok(()) + } +} diff --git a/sdk/storage_blobs/src/service/operations/mod.rs b/sdk/storage_blobs/src/service/operations/mod.rs index cd7ff68ad8..c4fa71db48 100644 --- a/sdk/storage_blobs/src/service/operations/mod.rs +++ b/sdk/storage_blobs/src/service/operations/mod.rs @@ -1,7 +1,9 @@ mod find_blobs_by_tags; mod get_account_information; +mod get_user_delegation_key; mod list_containers; pub use find_blobs_by_tags::*; pub use get_account_information::*; +pub use get_user_delegation_key::*; pub use list_containers::*;