Skip to content

Commit 4d394af

Browse files
authored
add support to fetch user delegated keys (#1412)
A first step in creating user-delegated SAS signatures is fetching a user delegation key. This PR implements fetching the key from the service. Note, this adds the `ISO-8601` format as used by Azure Storage, which is close but not exactly `RFC-3339`. Ref: https://learn.microsoft.com/en-us/rest/api/storageservices/get-user-delegation-key
1 parent 3b29475 commit 4d394af

File tree

7 files changed

+258
-1
lines changed

7 files changed

+258
-1
lines changed

sdk/core/src/date/iso8601.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
use crate::error::{ErrorKind, ResultExt};
2+
use serde::{self, de, Deserialize, Deserializer, Serializer};
3+
use time::{
4+
format_description::well_known::{
5+
iso8601::{Config, EncodedConfig, TimePrecision},
6+
Iso8601,
7+
},
8+
OffsetDateTime, UtcOffset,
9+
};
10+
11+
const SERDE_CONFIG: EncodedConfig = Config::DEFAULT
12+
.set_year_is_six_digits(false)
13+
.set_time_precision(TimePrecision::Second {
14+
decimal_digits: None,
15+
})
16+
.encode();
17+
18+
pub fn parse_iso8601(s: &str) -> crate::Result<OffsetDateTime> {
19+
OffsetDateTime::parse(s, &Iso8601::<SERDE_CONFIG>)
20+
.with_context(ErrorKind::DataConversion, || {
21+
format!("unable to parse iso8601 date '{s}")
22+
})
23+
}
24+
25+
pub fn to_iso8601(date: &OffsetDateTime) -> crate::Result<String> {
26+
date.format(&Iso8601::<SERDE_CONFIG>)
27+
.with_context(ErrorKind::DataConversion, || {
28+
format!("unable to format date '{date:?}")
29+
})
30+
}
31+
32+
pub fn deserialize<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
33+
where
34+
D: Deserializer<'de>,
35+
{
36+
let s = String::deserialize(deserializer)?;
37+
parse_iso8601(&s).map_err(de::Error::custom)
38+
}
39+
40+
pub fn serialize<S>(date: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
41+
where
42+
S: Serializer,
43+
{
44+
date.to_offset(UtcOffset::UTC);
45+
let as_str = to_iso8601(date).map_err(serde::ser::Error::custom)?;
46+
serializer.serialize_str(&as_str)
47+
}
48+
49+
pub mod option {
50+
use crate::date::iso8601::{parse_iso8601, to_iso8601};
51+
use serde::{Deserialize, Deserializer, Serializer};
52+
use time::OffsetDateTime;
53+
54+
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<OffsetDateTime>, D::Error>
55+
where
56+
D: Deserializer<'de>,
57+
{
58+
let s: Option<String> = Option::deserialize(deserializer)?;
59+
s.map(|s| parse_iso8601(&s).map_err(serde::de::Error::custom))
60+
.transpose()
61+
}
62+
63+
pub fn serialize<S>(date: &Option<OffsetDateTime>, serializer: S) -> Result<S::Ok, S::Error>
64+
where
65+
S: Serializer,
66+
{
67+
if let Some(date) = date {
68+
serializer.serialize_str(&to_iso8601(date).map_err(serde::ser::Error::custom)?)
69+
} else {
70+
serializer.serialize_none()
71+
}
72+
}
73+
}

sdk/core/src/date/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use time::{
1414
// Serde modules
1515
pub use time::serde::rfc3339;
1616
pub use time::serde::timestamp;
17+
pub mod iso8601;
1718
pub mod rfc1123;
1819

1920
/// RFC 3339: Date and Time on the Internet: Timestamps

sdk/storage_blobs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ RustyXML = "0.3"
2424
serde = { version = "1.0" }
2525
serde_derive = "1.0"
2626
serde_json = "1.0"
27-
uuid = { version = "1.0", features = ["v4"] }
27+
uuid = { version = "1.0", features = ["v4", "serde"] }
2828
url = "2.2"
2929

3030
[dev-dependencies]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use azure_identity::DefaultAzureCredential;
2+
use azure_storage::prelude::*;
3+
use azure_storage_blobs::prelude::*;
4+
use clap::Parser;
5+
use std::{sync::Arc, time::Duration};
6+
use time::OffsetDateTime;
7+
8+
#[derive(Debug, Parser)]
9+
struct Args {
10+
/// storage account name
11+
#[clap(env = "STORAGE_ACCOUNT")]
12+
account: String,
13+
}
14+
15+
#[tokio::main]
16+
async fn main() -> azure_core::Result<()> {
17+
env_logger::init();
18+
let args = Args::parse();
19+
20+
let storage_credentials =
21+
StorageCredentials::token_credential(Arc::new(DefaultAzureCredential::default()));
22+
let client = BlobServiceClient::new(&args.account, storage_credentials);
23+
24+
let start = OffsetDateTime::now_utc();
25+
let expiry = start + Duration::from_secs(60 * 60);
26+
let response = client.get_user_deligation_key(start, expiry).await?;
27+
println!("{:#?}", response.user_deligation_key);
28+
Ok(())
29+
}

sdk/storage_blobs/src/clients/blob_service_client.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ impl BlobServiceClient {
179179
ContainerClient::new(self.clone(), container_name.into())
180180
}
181181

182+
pub fn get_user_deligation_key(
183+
&self,
184+
start: OffsetDateTime,
185+
expiry: OffsetDateTime,
186+
) -> GetUserDelegationKeyBuilder {
187+
GetUserDelegationKeyBuilder::new(self.clone(), start, expiry)
188+
}
189+
182190
pub fn shared_access_signature(
183191
&self,
184192
resource_type: AccountSasResourceType,
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
use crate::prelude::BlobServiceClient;
2+
use azure_core::{
3+
date::iso8601,
4+
headers::Headers,
5+
xml::{read_xml_str, to_xml},
6+
Method,
7+
};
8+
use azure_storage::headers::CommonStorageResponseHeaders;
9+
use time::OffsetDateTime;
10+
use uuid::Uuid;
11+
12+
operation! {
13+
GetUserDelegationKey,
14+
client: BlobServiceClient,
15+
start_time: OffsetDateTime,
16+
expiry_time: OffsetDateTime,
17+
}
18+
19+
impl GetUserDelegationKeyBuilder {
20+
pub fn into_future(mut self) -> GetUserDelegationKey {
21+
Box::pin(async move {
22+
let mut url = self.client.url()?;
23+
24+
url.query_pairs_mut()
25+
.extend_pairs([("restype", "service"), ("comp", "userdelegationkey")]);
26+
27+
let body = GetUserDelegationKeyRequest {
28+
start: self.start_time,
29+
expiry: self.expiry_time,
30+
}
31+
.as_string()?;
32+
33+
let mut request = BlobServiceClient::finalize_request(
34+
url,
35+
Method::Post,
36+
Headers::new(),
37+
Some(body.into()),
38+
)?;
39+
40+
let response = self.client.send(&mut self.context, &mut request).await?;
41+
42+
let (_, headers, body) = response.deconstruct();
43+
let body = body.collect_string().await?;
44+
GetUserDelegationKeyResponse::try_from(&headers, &body)
45+
})
46+
}
47+
}
48+
49+
#[derive(Serialize)]
50+
#[serde(rename = "KeyInfo")]
51+
struct GetUserDelegationKeyRequest {
52+
#[serde(rename = "Start", with = "iso8601")]
53+
start: OffsetDateTime,
54+
#[serde(rename = "Expiry", with = "iso8601")]
55+
expiry: OffsetDateTime,
56+
}
57+
58+
impl GetUserDelegationKeyRequest {
59+
pub fn as_string(&self) -> azure_core::Result<String> {
60+
Ok(format!(
61+
"<?xml version=\"1.0\" encoding=\"utf-8\"?>{}",
62+
to_xml(self)?
63+
))
64+
}
65+
}
66+
67+
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
68+
#[serde(rename_all = "PascalCase")]
69+
pub struct UserDeligationKey {
70+
pub signed_oid: Uuid,
71+
pub signed_tid: Uuid,
72+
#[serde(with = "iso8601")]
73+
pub signed_start: OffsetDateTime,
74+
#[serde(with = "iso8601")]
75+
pub signed_expiry: OffsetDateTime,
76+
pub signed_service: String,
77+
pub signed_version: String,
78+
pub value: String,
79+
}
80+
81+
#[derive(Debug)]
82+
pub struct GetUserDelegationKeyResponse {
83+
pub common: CommonStorageResponseHeaders,
84+
pub user_deligation_key: UserDeligationKey,
85+
}
86+
87+
impl GetUserDelegationKeyResponse {
88+
pub(crate) fn try_from(headers: &Headers, body: &str) -> azure_core::Result<Self> {
89+
let common = CommonStorageResponseHeaders::try_from(headers)?;
90+
let user_deligation_key: UserDeligationKey = read_xml_str(body)?;
91+
92+
Ok(Self {
93+
common,
94+
user_deligation_key,
95+
})
96+
}
97+
}
98+
99+
#[cfg(test)]
100+
mod test {
101+
use super::*;
102+
103+
const BASIC_REQUEST: &str = "<?xml version=\"1.0\" encoding=\"utf-8\"?><KeyInfo><Start>1970-01-01T00:00:00Z</Start><Expiry>1970-01-01T00:00:01Z</Expiry></KeyInfo>";
104+
const BASIC_RESPONSE: &str = "
105+
<UserDeligationKey>
106+
<SignedOid>00000000-0000-0000-0000-000000000000</SignedOid>
107+
<SignedTid>00000000-0000-0000-0000-000000000001</SignedTid>
108+
<SignedStart>1970-01-01T00:00:00Z</SignedStart>
109+
<SignedExpiry>1970-01-01T00:00:01Z</SignedExpiry>
110+
<SignedService>b</SignedService>
111+
<SignedVersion>c</SignedVersion>
112+
<Value>d</Value>
113+
</UserDeligationKey>
114+
";
115+
116+
#[test]
117+
fn request_xml() -> azure_core::Result<()> {
118+
let request = GetUserDelegationKeyRequest {
119+
start: OffsetDateTime::from_unix_timestamp(0).unwrap(),
120+
expiry: OffsetDateTime::from_unix_timestamp(1).unwrap(),
121+
}
122+
.as_string()?;
123+
assert_eq!(BASIC_REQUEST, request);
124+
Ok(())
125+
}
126+
127+
#[test]
128+
fn parse_response() -> azure_core::Result<()> {
129+
let expected = UserDeligationKey {
130+
signed_oid: Uuid::from_u128(0),
131+
signed_tid: Uuid::from_u128(1),
132+
signed_start: OffsetDateTime::from_unix_timestamp(0).unwrap(),
133+
signed_expiry: OffsetDateTime::from_unix_timestamp(1).unwrap(),
134+
signed_service: "b".to_owned(),
135+
signed_version: "c".to_owned(),
136+
value: "d".to_owned(),
137+
};
138+
139+
let deserialized: UserDeligationKey = read_xml_str(BASIC_RESPONSE)?;
140+
assert_eq!(deserialized, expected);
141+
142+
Ok(())
143+
}
144+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
mod find_blobs_by_tags;
22
mod get_account_information;
3+
mod get_user_delegation_key;
34
mod list_containers;
45

56
pub use find_blobs_by_tags::*;
67
pub use get_account_information::*;
8+
pub use get_user_delegation_key::*;
79
pub use list_containers::*;

0 commit comments

Comments
 (0)