azure_storage/shared_access_signature/
service_sas.rs

1use 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,         // b
12    BlobVersion,  // bv
13    BlobSnapshot, // bs
14    Container,    // c
15    Directory,    // d
16}
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,             // r - Container | Directory | Blob
34    pub add: bool,              // a - Container | Directory | Blob
35    pub create: bool,           // c - Container | Directory | Blob
36    pub write: bool,            // w - Container | Directory | Blob
37    pub delete: bool,           // d - Container | Directory | Blob
38    pub delete_version: bool,   // x - Container | Blob
39    pub permanent_delete: bool, // y - Blob
40    pub list: bool,             // l - Container | Directory
41    pub tags: bool,             // t - Tags
42    pub move_: bool,            // m - Container | Directory | Blob
43    pub execute: bool,          // e - Container | Directory | Blob
44    pub ownership: bool,        // o - Container | Directory | Blob
45    pub permissions: bool,      // p - Container | Directory | Blob
46                                // SetImmunabilityPolicy: bool, // i  -- container
47}
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, // sp
130    expiry: OffsetDateTime,          // se
131    start: Option<OffsetDateTime>,   // st
132    identifier: Option<String>,
133    ip: Option<String>,
134    protocol: Option<SasProtocol>,
135    signed_directory_depth: Option<usize>, // sdd
136}
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(), // SIGNED AUTHORIZED_OID
197                    String::new(), // SIGNED UNAUTHORIZED_OID
198                    String::new(), // SIGNED CORRELATION ID
199                ];
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(), // snapshot time
212            String::new(), // SIGNED ENCRYPTION SCOPE
213            String::new(), // SIGNED CACHE CONTROL
214            String::new(), // SIGNED CONTENT DISPOSITION
215            String::new(), // SIGNED CONTENT ENCODING
216            String::new(), // SIGNED CONTENT LANGUAGE
217            String::new(), // SIGNED CONTENT TYPE
218        ]);
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        // BlobSignedResource::Blob
296        assert!(parsed.find(|(k, v)| k == "sr" && v == "b").is_some());
297
298        // signed_directory_depth NOT set
299        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        // BlobSignedResource::Directory
324        assert!(parsed.find(|(k, v)| k == "sr" && v == "d").is_some());
325
326        // signed_directory_depth set
327        assert!(parsed.find(|(k, v)| k == "sdd" && v == "2").is_some());
328        Ok(())
329    }
330}