azure_storage/
connection_string.rs

1use crate::StorageCredentials;
2use azure_core::{
3    auth::Secret,
4    error::{Error, ErrorKind},
5};
6use tracing::warn;
7
8// Key names.
9pub const ACCOUNT_KEY_KEY_NAME: &str = "AccountKey";
10pub const ACCOUNT_NAME_KEY_NAME: &str = "AccountName";
11pub const SAS_KEY_NAME: &str = "SharedAccessSignature";
12pub const ENDPOINT_SUFFIX_KEY_NAME: &str = "EndpointSuffix";
13pub const DEFAULT_ENDPOINTS_PROTOCOL_KEY_NAME: &str = "DefaultEndpointsProtocol";
14pub const USE_DEVELOPMENT_STORAGE_KEY_NAME: &str = "UseDevelopmentStorage";
15pub const DEVELOPMENT_STORAGE_PROXY_URI_KEY_NAME: &str = "DevelopmentStorageProxyUri";
16pub const BLOB_ENDPOINT_KEY_NAME: &str = "BlobEndpoint";
17pub const BLOB_SECONDARY_ENDPOINT_KEY_NAME: &str = "BlobSecondaryEndpoint";
18pub const TABLE_ENDPOINT_KEY_NAME: &str = "TableEndpoint";
19pub const TABLE_SECONDARY_ENDPOINT_KEY_NAME: &str = "TableSecondaryEndpoint";
20pub const QUEUE_ENDPOINT_KEY_NAME: &str = "QueueEndpoint";
21pub const QUEUE_SECONDARY_ENDPOINT_KEY_NAME: &str = "QueueSecondaryEndpoint";
22pub const FILE_ENDPOINT_KEY_NAME: &str = "FileEndpoint";
23pub const FILE_SECONDARY_ENDPOINT_KEY_NAME: &str = "FileSecondaryEndpoint";
24
25#[derive(Debug, PartialEq, Eq)]
26pub enum EndpointProtocol {
27    Http,
28    Https,
29}
30
31impl std::fmt::Display for EndpointProtocol {
32    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
33        let s = match self {
34            EndpointProtocol::Https => "https",
35            EndpointProtocol::Http => "http",
36        };
37        write!(f, "{s}")
38    }
39}
40
41/// A storage connection string.
42///
43/// The key are a subset of what is defined in the
44/// [.NET SDK](https://github.com/Azure/azure-storage-net/blob/ed89733dfb170707d65b7b8b0be36cb5bd6512e4/Lib/Common/CloudStorageAccount.cs#L63-L261).
45#[derive(Debug, Default, PartialEq, Eq)]
46pub struct ConnectionString<'a> {
47    /// Name of the storage account.
48    pub account_name: Option<&'a str>,
49    /// Account key of the storage account.
50    pub account_key: Option<&'a str>,
51    /// SAS (Shared Access Signature) key of the storage account.
52    pub sas: Option<&'a str>,
53    /// Default protocol to use for storage endpoints.
54    pub default_endpoints_protocol: Option<EndpointProtocol>,
55    /// Whether to use development storage.
56    pub endpoint_suffix: Option<&'a str>,
57    /// The development storage proxy URI.
58    pub use_development_storage: Option<bool>,
59    /// Custom storage endpoint suffix.
60    pub development_storage_proxy_uri: Option<&'a str>,
61    /// Custom blob storage endpoint.
62    pub blob_endpoint: Option<&'a str>,
63    /// Custom blob storage secondary endpoint.
64    pub blob_secondary_endpoint: Option<&'a str>,
65    /// Custom table storage endpoint.
66    pub table_endpoint: Option<&'a str>,
67    /// Custom table storage secondary endpoint.
68    pub table_secondary_endpoint: Option<&'a str>,
69    /// Custom queue storage endpoint.
70    pub queue_endpoint: Option<&'a str>,
71    /// Custom queue storage secondary endpoint.
72    pub queue_secondary_endpoint: Option<&'a str>,
73    /// Custom file storage endpoint.
74    pub file_endpoint: Option<&'a str>,
75    /// Custom file storage secondary endpoint.
76    pub file_secondary_endpoint: Option<&'a str>,
77}
78
79impl<'a> ConnectionString<'a> {
80    pub fn new(connection_string: &'a str) -> azure_core::Result<Self> {
81        let mut account_name = None;
82        let mut account_key = None;
83        let mut sas = None;
84        let mut endpoint_suffix = None;
85        let mut default_endpoints_protocol = None;
86        let mut use_development_storage = None;
87        let mut development_storage_proxy_uri = None;
88        let mut blob_endpoint = None;
89        let mut blob_secondary_endpoint = None;
90        let mut table_endpoint = None;
91        let mut table_secondary_endpoint = None;
92        let mut queue_endpoint = None;
93        let mut queue_secondary_endpoint = None;
94        let mut file_endpoint = None;
95        let mut file_secondary_endpoint = None;
96
97        let kv_str_pairs = connection_string
98            .split(';')
99            .filter(|s| !s.chars().all(char::is_whitespace));
100
101        for kv_pair_str in kv_str_pairs {
102            let kv = kv_pair_str.trim().split_once('=');
103
104            let (k, v) = match kv {
105                Some((k, _)) if (k.chars().all(char::is_whitespace) || k.trim() == "") => {
106                    return Err(Error::with_message(ErrorKind::Other, || {
107                        format!("no key found in connection string: {connection_string}")
108                    }))
109                }
110                Some((k, v)) if (v.chars().all(char::is_whitespace) || v.trim() == "") => {
111                    return Err(Error::with_message(ErrorKind::Other, || {
112                        format!("missing value in connection string: {connection_string} key: {k}")
113                    }))
114                }
115                Some((k, v)) => (k.trim(), v.trim()),
116                None => {
117                    return Err(Error::with_message(ErrorKind::Other, || {
118                        format!("no key/value found in connection string: {connection_string}")
119                    }))
120                }
121            };
122
123            match k {
124                ACCOUNT_NAME_KEY_NAME => account_name = Some(v),
125                ACCOUNT_KEY_KEY_NAME => account_key = Some(v),
126                SAS_KEY_NAME => sas = Some(v),
127                ENDPOINT_SUFFIX_KEY_NAME => endpoint_suffix = Some(v),
128                DEFAULT_ENDPOINTS_PROTOCOL_KEY_NAME => {
129                    let protocol = match v {
130                        "http" => EndpointProtocol::Http,
131                        "https" => EndpointProtocol::Https,
132                        _ => {
133                            return Err(Error::with_message(ErrorKind::Other, || {
134                                format!("connection string unsupported protocol: {v}")
135                            }));
136                        }
137                    };
138                    default_endpoints_protocol = Some(protocol);
139                }
140                USE_DEVELOPMENT_STORAGE_KEY_NAME => match v {
141                    "true" => use_development_storage = Some(true),
142                    "false" => use_development_storage = Some(false),
143                    _ => {
144                        return Err(Error::with_message(ErrorKind::Other, || {
145                            format!("connection string unexpected value. {USE_DEVELOPMENT_STORAGE_KEY_NAME}: {v}. Please specify true or false.")
146                        }));
147                    }
148                },
149                DEVELOPMENT_STORAGE_PROXY_URI_KEY_NAME => development_storage_proxy_uri = Some(v),
150                BLOB_ENDPOINT_KEY_NAME => blob_endpoint = Some(v),
151                BLOB_SECONDARY_ENDPOINT_KEY_NAME => blob_secondary_endpoint = Some(v),
152                TABLE_ENDPOINT_KEY_NAME => table_endpoint = Some(v),
153                TABLE_SECONDARY_ENDPOINT_KEY_NAME => table_secondary_endpoint = Some(v),
154                QUEUE_ENDPOINT_KEY_NAME => queue_endpoint = Some(v),
155                QUEUE_SECONDARY_ENDPOINT_KEY_NAME => queue_secondary_endpoint = Some(v),
156                FILE_ENDPOINT_KEY_NAME => file_endpoint = Some(v),
157                FILE_SECONDARY_ENDPOINT_KEY_NAME => file_secondary_endpoint = Some(v),
158                k => {
159                    return Err(Error::with_message(ErrorKind::Other, || {
160                        format!("connection string unexpected key: {k}")
161                    }))
162                }
163            }
164        }
165
166        Ok(Self {
167            account_name,
168            account_key,
169            sas,
170            default_endpoints_protocol,
171            endpoint_suffix,
172            use_development_storage,
173            development_storage_proxy_uri,
174            blob_endpoint,
175            blob_secondary_endpoint,
176            table_endpoint,
177            table_secondary_endpoint,
178            queue_endpoint,
179            queue_secondary_endpoint,
180            file_endpoint,
181            file_secondary_endpoint,
182        })
183    }
184
185    pub fn storage_credentials(&self) -> azure_core::Result<StorageCredentials> {
186        match self {
187            ConnectionString {
188                sas: Some(sas_token),
189                ..
190            } => {
191                if self.account_key.is_some() {
192                    warn!("Both account key and SAS defined in connection string. Using only the provided SAS.");
193                }
194                StorageCredentials::sas_token(*sas_token)
195            }
196            ConnectionString {
197                account_name: Some(account),
198                account_key: Some(key),
199                ..
200            } =>  Ok(StorageCredentials::access_key(*account, Secret::new((*key).to_string()))),
201           _ => {
202                Err(Error::message(ErrorKind::Credential,
203                    "Could not create a `StorageCredentail` from the provided connection string. Please validate that you have specified a means of authentication (key, SAS, etc.)."
204                ))
205            }
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    #[allow(unused_imports)]
213    use super::*;
214
215    #[test]
216    fn it_returns_expected_errors() {
217        assert!(ConnectionString::new("AccountName=").is_err());
218        assert!(ConnectionString::new("AccountName    =").is_err());
219        assert!(ConnectionString::new("MissingEquals").is_err());
220        assert!(ConnectionString::new("=").is_err());
221        assert!(ConnectionString::new("x=123;").is_err());
222    }
223
224    #[test]
225    fn it_parses_empty_connection_string() {
226        assert_eq!(
227            ConnectionString::new("").unwrap(),
228            ConnectionString::default()
229        );
230    }
231
232    #[test]
233    fn it_parses_basic_cases() {
234        assert!(matches!(
235            ConnectionString::new("AccountName=guy"),
236            Ok(ConnectionString {
237                account_name: Some("guy"),
238                ..
239            })
240        ));
241        assert!(matches!(
242            ConnectionString::new("AccountName=guy;"),
243            Ok(ConnectionString {
244                account_name: Some("guy"),
245                ..
246            })
247        ));
248        assert!(matches!(
249            ConnectionString::new("AccountName=guywald;AccountKey=1234"),
250            Ok(ConnectionString {
251                account_name: Some("guywald"),
252                account_key: Some("1234"),
253                ..
254            })
255        ));
256        assert!(matches!(
257            ConnectionString::new("AccountName=guywald;SharedAccessSignature=s"),
258            Ok(ConnectionString {
259                account_name: Some("guywald"),
260                sas: Some("s"),
261                ..
262            })
263        ));
264        assert!(matches!(
265            ConnectionString::new("AccountName=guywald;SharedAccessSignature=se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
266            Ok(ConnectionString {
267                account_name: Some("guywald"),
268                sas: Some("se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
269                ..
270            })
271        ));
272
273        assert!(matches!(
274            ConnectionString::new("AccountName = guywald;SharedAccessSignature = se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
275            Ok(ConnectionString {
276                account_name: Some("guywald"),
277                sas: Some("se=2036-01-01&sp=acw&sv=2018-11-09&sr=c&sig=c2lnbmF0dXJlCg%3D%3D"),
278                ..
279            })
280        ));
281    }
282
283    #[test]
284    fn it_parses_all_properties() {
285        assert!(matches!(
286                ConnectionString::new("AccountName=a;AccountKey=b;DefaultEndpointsProtocol=https;UseDevelopmentStorage=true;DevelopmentStorageProxyUri=c;BlobEndpoint=d;TableEndpoint=e;QueueEndpoint=f;SharedAccessSignature=g;"),
287                Ok(ConnectionString {
288                    account_name: Some("a"),
289                    account_key: Some("b"),
290                    default_endpoints_protocol: Some(EndpointProtocol::Https),
291                    use_development_storage: Some(true),
292                    development_storage_proxy_uri: Some("c"),
293                    blob_endpoint: Some("d"),
294                    table_endpoint: Some("e"),
295                    sas: Some("g"),
296                    ..
297                })
298            ));
299        assert!(matches!(
300                ConnectionString::new("BlobEndpoint=b1;BlobSecondaryEndpoint=b2;TableEndpoint=t1;TableSecondaryEndpoint=t2;QueueEndpoint=q1;QueueSecondaryEndpoint=q2;FileEndpoint=f1;FileSecondaryEndpoint=f2;"),
301                Ok(ConnectionString {
302                    blob_endpoint: Some("b1"),
303                    blob_secondary_endpoint: Some("b2"),
304                    table_endpoint: Some("t1"),
305                    table_secondary_endpoint: Some("t2"),
306                    queue_endpoint: Some("q1"),
307                    queue_secondary_endpoint: Some("q2"),
308                    file_endpoint: Some("f1"),
309                    file_secondary_endpoint: Some("f2"),
310                    ..
311                })
312            ));
313    }
314
315    #[test]
316    fn it_parses_correct_endpoint_protocols() {
317        assert!(matches!(
318            ConnectionString::new("DefaultEndpointsProtocol=https"),
319            Ok(ConnectionString {
320                default_endpoints_protocol: Some(EndpointProtocol::Https),
321                ..
322            })
323        ));
324        assert!(matches!(
325            ConnectionString::new("DefaultEndpointsProtocol=http"),
326            Ok(ConnectionString {
327                default_endpoints_protocol: Some(EndpointProtocol::Http),
328                ..
329            })
330        ));
331    }
332}