azure_storage/shared_access_signature/
service_sas.rs1use crate::shared_access_signature::{format_date, SasProtocol, SasToken};
2use azure_core::{auth::Secret, date::iso8601, hmac::hmac_sha256};
3use std::fmt;
4use time::OffsetDateTime;
5use url::form_urlencoded;
6use uuid::Uuid;
7
8const SERVICE_SAS_VERSION: &str = "2022-11-02";
9
10pub enum BlobSignedResource {
11 Blob, BlobVersion, BlobSnapshot, Container, Directory, }
17
18impl fmt::Display for BlobSignedResource {
19 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20 match *self {
21 Self::Blob => write!(f, "b"),
22 Self::BlobVersion => write!(f, "bv"),
23 Self::BlobSnapshot => write!(f, "bs"),
24 Self::Container => write!(f, "c"),
25 Self::Directory => write!(f, "d"),
26 }
27 }
28}
29
30#[allow(clippy::struct_excessive_bools)]
31#[derive(Default)]
32pub struct BlobSasPermissions {
33 pub read: bool, pub add: bool, pub create: bool, pub write: bool, pub delete: bool, pub delete_version: bool, pub permanent_delete: bool, pub list: bool, pub tags: bool, pub move_: bool, pub execute: bool, pub ownership: bool, pub permissions: bool, }
48
49impl fmt::Display for BlobSasPermissions {
50 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51 if self.read {
52 write!(f, "r")?;
53 };
54 if self.add {
55 write!(f, "a")?;
56 };
57 if self.create {
58 write!(f, "c")?;
59 };
60 if self.write {
61 write!(f, "w")?;
62 };
63 if self.delete {
64 write!(f, "d")?;
65 };
66 if self.delete_version {
67 write!(f, "x")?;
68 };
69 if self.permanent_delete {
70 write!(f, "y")?;
71 };
72 if self.list {
73 write!(f, "l")?;
74 };
75 if self.tags {
76 write!(f, "t")?;
77 };
78 if self.move_ {
79 write!(f, "m")?;
80 };
81 if self.execute {
82 write!(f, "e")?;
83 };
84 if self.ownership {
85 write!(f, "o")?;
86 };
87 if self.permissions {
88 write!(f, "p")?;
89 };
90 Ok(())
91 }
92}
93
94#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
95#[serde(rename_all = "PascalCase")]
96pub struct UserDeligationKey {
97 pub signed_oid: Uuid,
98 pub signed_tid: Uuid,
99 #[serde(with = "iso8601")]
100 pub signed_start: OffsetDateTime,
101 #[serde(with = "iso8601")]
102 pub signed_expiry: OffsetDateTime,
103 pub signed_service: String,
104 pub signed_version: String,
105 pub value: Secret,
106}
107
108pub enum SasKey {
109 Key(Secret),
110 UserDelegationKey(UserDeligationKey),
111}
112
113impl From<Secret> for SasKey {
114 fn from(key: Secret) -> Self {
115 Self::Key(key)
116 }
117}
118
119impl From<UserDeligationKey> for SasKey {
120 fn from(key: UserDeligationKey) -> Self {
121 Self::UserDelegationKey(key)
122 }
123}
124
125pub struct BlobSharedAccessSignature {
126 key: SasKey,
127 canonicalized_resource: String,
128 resource: BlobSignedResource,
129 permissions: BlobSasPermissions, expiry: OffsetDateTime, start: Option<OffsetDateTime>, identifier: Option<String>,
133 ip: Option<String>,
134 protocol: Option<SasProtocol>,
135 signed_directory_depth: Option<usize>, }
137
138impl BlobSharedAccessSignature {
139 pub fn new<K>(
140 key: K,
141 canonicalized_resource: String,
142 permissions: BlobSasPermissions,
143 expiry: OffsetDateTime,
144 resource: BlobSignedResource,
145 ) -> Self
146 where
147 K: Into<SasKey>,
148 {
149 Self {
150 key: key.into(),
151 canonicalized_resource,
152 resource,
153 permissions,
154 expiry,
155 start: None,
156 identifier: None,
157 ip: None,
158 protocol: None,
159 signed_directory_depth: None,
160 }
161 }
162
163 setters! {
164 start: OffsetDateTime => Some(start),
165 identifier: String => Some(identifier),
166 ip: String => Some(ip),
167 protocol: SasProtocol => Some(protocol),
168 signed_directory_depth: usize => Some(signed_directory_depth),
169 }
170
171 fn sign(&self) -> azure_core::Result<String> {
172 let mut content = vec![
173 self.permissions.to_string(),
174 self.start.map_or(String::new(), format_date),
175 format_date(self.expiry),
176 self.canonicalized_resource.clone(),
177 ];
178
179 let key = match &self.key {
180 SasKey::Key(key) => {
181 content.extend([self
182 .identifier
183 .as_ref()
184 .unwrap_or(&String::new())
185 .to_string()]);
186 key
187 }
188 SasKey::UserDelegationKey(key) => {
189 let user_delegated = [
190 key.signed_oid.to_string(),
191 key.signed_tid.to_string(),
192 format_date(key.signed_start),
193 format_date(key.signed_expiry),
194 key.signed_service.to_string(),
195 key.signed_version.to_string(),
196 String::new(), String::new(), String::new(), ];
200
201 content.extend(user_delegated);
202 &key.value
203 }
204 };
205
206 content.extend([
207 self.ip.as_ref().unwrap_or(&String::new()).to_string(),
208 self.protocol.map(|x| x.to_string()).unwrap_or_default(),
209 SERVICE_SAS_VERSION.to_string(),
210 self.resource.to_string(),
211 String::new(), String::new(), String::new(), String::new(), String::new(), String::new(), String::new(), ]);
219
220 hmac_sha256(&content.join("\n"), key)
221 }
222}
223
224impl SasToken for BlobSharedAccessSignature {
225 fn token(&self) -> azure_core::Result<String> {
226 let mut form = form_urlencoded::Serializer::new(String::new());
227
228 if let SasKey::UserDelegationKey(key) = &self.key {
229 form.extend_pairs(&[
230 ("skoid", &key.signed_oid.to_string()),
231 ("sktid", &key.signed_tid.to_string()),
232 ("skt", &format_date(key.signed_start)),
233 ("ske", &format_date(key.signed_expiry)),
234 ("sks", &key.signed_service),
235 ("skv", &key.signed_version),
236 ]);
237 }
238
239 form.extend_pairs(&[
240 ("sv", SERVICE_SAS_VERSION),
241 ("sp", &self.permissions.to_string()),
242 ("sr", &self.resource.to_string()),
243 ("se", &format_date(self.expiry)),
244 ]);
245
246 if let Some(start) = &self.start {
247 form.append_pair("st", &format_date(*start));
248 }
249
250 if let Some(ip) = &self.ip {
251 form.append_pair("sip", ip);
252 }
253
254 if let Some(protocol) = &self.protocol {
255 form.append_pair("spr", &protocol.to_string());
256 }
257
258 if let Some(signed_directory_depth) = &self.signed_directory_depth {
259 form.append_pair("sdd", &signed_directory_depth.to_string());
260 }
261
262 let sig = self.sign()?;
263 form.append_pair("sig", &sig);
264 Ok(form.finish())
265 }
266}
267
268#[cfg(test)]
269mod test {
270 use super::*;
271 use time::Duration;
272
273 const MOCK_SECRET_KEY: &str = "RZfi3m1W7eyQ5zD4ymSmGANVdJ2SDQmg4sE89SW104s=";
274 const MOCK_CANONICALIZED_RESOURCE: &str = "/blob/STORAGE_ACCOUNT_NAME/CONTAINER_NAME/";
275
276 #[test]
277 fn test_blob_scoped_sas_token() -> azure_core::Result<()> {
278 let permissions = BlobSasPermissions {
279 read: true,
280 ..Default::default()
281 };
282 let signed_token = BlobSharedAccessSignature::new(
283 Secret::new(MOCK_SECRET_KEY),
284 String::from(MOCK_CANONICALIZED_RESOURCE),
285 permissions,
286 OffsetDateTime::UNIX_EPOCH + Duration::days(7),
287 BlobSignedResource::Blob,
288 )
289 .token()?;
290
291 assert_eq!(signed_token, "sv=2022-11-02&sp=r&sr=b&se=1970-01-08T00%3A00%3A00Z&sig=VRZjVZ1c%2FLz7IXCp17Sdx9%2BR9JDrnJdzE3NW56DMjNs%3D");
292
293 let mut parsed = url::form_urlencoded::parse(&signed_token.as_bytes());
294
295 assert!(parsed.find(|(k, v)| k == "sr" && v == "b").is_some());
297
298 assert!(parsed.find(|(k, _)| k == "sdd").is_none());
300 Ok(())
301 }
302
303 #[test]
304 fn test_directory_scoped_sas_token() -> azure_core::Result<()> {
305 let permissions = BlobSasPermissions {
306 read: true,
307 ..Default::default()
308 };
309 let signed_token = BlobSharedAccessSignature::new(
310 Secret::new(MOCK_SECRET_KEY),
311 String::from(MOCK_CANONICALIZED_RESOURCE),
312 permissions,
313 OffsetDateTime::UNIX_EPOCH + Duration::days(7),
314 BlobSignedResource::Directory,
315 )
316 .signed_directory_depth(2_usize)
317 .token()?;
318
319 assert_eq!(signed_token, "sv=2022-11-02&sp=r&sr=d&se=1970-01-08T00%3A00%3A00Z&sdd=2&sig=zVN%2FRgDWllHZH6%2FqWt5gFrV89vzp4EU6ULDTdYoHils%3D");
320
321 let mut parsed = url::form_urlencoded::parse(&signed_token.as_bytes());
322
323 assert!(parsed.find(|(k, v)| k == "sr" && v == "d").is_some());
325
326 assert!(parsed.find(|(k, v)| k == "sdd" && v == "2").is_some());
328 Ok(())
329 }
330}