1use std::fmt;
9
10use crate::{
11 prelude::*,
12 client::HttpClient,
13 error::{ValidationError, Error},
14 types::*,
15};
16
17use chrono::{DateTime, Utc};
18use serde::{Serialize, Deserialize};
19
20
21const B2_AUTH_URL: &str = "https://api.backblazeb2.com/b2api/v2/";
22
23#[derive(Debug)]
28pub struct Authorization<C>
29 where C: HttpClient,
30{
31 pub(crate) client: C,
32 pub(crate) account_id: String,
33 pub(crate) authorization_token: String,
37 allowed: Capabilities,
38 pub(crate) api_url: String,
40 pub(crate) download_url: String,
42 recommended_part_size: u64,
43 absolute_minimum_part_size: u64,
44 _s3_api_url: String,
46}
47
48impl<C> Authorization<C>
49 where C: HttpClient,
50{
51 #[cfg(test)]
53 #[allow(dead_code)]
54 #[allow(clippy::too_many_arguments)]
55 pub(crate) fn new(
56 client: C,
57 account_id: String,
58 authorization_token: String,
59 allowed: Capabilities,
60 api_url: String,
61 download_url: String,
62 recommended_part_size: u64,
63 absolute_minimum_part_size: u64,
64 _s3_api_url: String,
65 ) -> Self {
66 Self {
67 client,
68 account_id,
69 authorization_token,
70 allowed,
71 api_url,
72 download_url,
73 recommended_part_size,
74 absolute_minimum_part_size,
75 _s3_api_url,
76 }
77 }
78
79 pub fn authorization_token(&self) -> &str { &self.authorization_token }
81
82 pub fn account_id(&self) -> &str { &self.account_id }
84 pub fn capabilities(&self) -> &Capabilities { &self.allowed }
86 pub fn recommended_part_size(&self) -> u64 { self.recommended_part_size }
88 pub fn minimum_part_size(&self) -> u64 { self.absolute_minimum_part_size }
91
92 pub fn has_capability(&self, cap: Capability) -> bool {
93 self.allowed.has_capability(cap)
94 }
95
96 pub(crate) fn api_url<S: AsRef<str>>(&self, endpoint: S) -> String {
100 format!("{}/b2api/v2/{}", self.api_url, endpoint.as_ref())
101 }
102
103 pub(crate) fn download_get_url(&self) -> &str {
106 &self.download_url
107 }
108
109 pub(crate) fn download_url<S: AsRef<str>>(&self, endpoint: S) -> String {
112 format!("{}/b2api/v2/{}", self.download_url, endpoint.as_ref())
113 }
114}
115
116#[derive(Debug, Deserialize)]
121#[serde(rename_all = "camelCase")]
122struct ProtoAuthorization {
123 account_id: String,
124 authorization_token: String,
125 allowed: Capabilities,
126 api_url: String,
127 download_url: String,
128 recommended_part_size: u64,
129 absolute_minimum_part_size: u64,
130 _s3_api_url: String,
131}
132
133impl ProtoAuthorization {
134 fn create_authorization<C: HttpClient>(self, c: C) -> Authorization<C> {
135 Authorization {
136 client: c,
137 account_id: self.account_id,
138 authorization_token: self.authorization_token,
139 allowed: self.allowed,
140 api_url: self.api_url,
141 download_url: self.download_url,
142 recommended_part_size: self.recommended_part_size,
143 absolute_minimum_part_size: self.absolute_minimum_part_size,
144 _s3_api_url: self._s3_api_url,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct Capabilities {
154 capabilities: Vec<Capability>,
155 bucket_id: Option<String>,
156 bucket_name: Option<String>,
157 name_prefix: Option<String>,
158}
159
160impl Capabilities {
161 #[cfg(test)]
163 #[allow(dead_code)]
164 pub(crate) fn new(
165 capabilities: Vec<Capability>,
166 bucket_id: Option<String>,
167 bucket_name: Option<String>,
168 name_prefix: Option<String>,
169 ) -> Self {
170 Self {
171 capabilities,
172 bucket_id,
173 bucket_name,
174 name_prefix,
175 }
176 }
177
178 pub fn capabilities(&self) -> &[Capability] { &self.capabilities }
180 pub fn bucket_id(&self) -> Option<&String> { self.bucket_id.as_ref() }
183 pub fn bucket_name(&self) -> Option<&String> { self.bucket_name.as_ref() }
187 pub fn name_prefix(&self) -> Option<&String> { self.name_prefix.as_ref() }
189
190 pub fn has_capability(&self, cap: Capability) -> bool {
193 self.capabilities.iter().any(|&c| c == cap)
194 }
195}
196
197#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub enum Capability {
201 ListKeys,
202 WriteKeys,
203 DeleteKeys,
204 ListAllBucketNames,
205 ListBuckets,
206 ReadBuckets,
207 WriteBuckets,
208 DeleteBuckets,
209 ReadBucketRetentions,
210 WriteBucketRetentions,
211 ReadBucketEncryption,
212 WriteBucketEncryption,
213 ListFiles,
214 ReadFiles,
215 ShareFiles,
216 WriteFiles,
217 DeleteFiles,
218 ReadFileLegalHolds,
219 WriteFileLegalHolds,
220 ReadFileRetentions,
221 WriteFileRetentions,
222 BypassGovernance,
223 ReadBucketReplications,
224 WriteBucketReplications,
225}
226
227pub async fn authorize_account<C, E>(mut client: C, key_id: &str, key: &str)
257-> Result<Authorization<C>, Error<E>>
258 where C: HttpClient<Error=Error<E>>,
259 E: fmt::Debug + fmt::Display,
260{
261 let id_and_key = format!("{}:{}", key_id, key);
262 let id_and_key = base64::encode(id_and_key.as_bytes());
263
264 let mut auth = String::from("Basic ");
265 auth.push_str(&id_and_key);
266
267 let req = client.get(
268 format!("{}b2_authorize_account", B2_AUTH_URL)
269 ).expect("Invalid URL")
270 .with_header("Authorization", &auth).unwrap();
271
272 let res = req.send().await?;
273
274 let auth: B2Result<ProtoAuthorization> = serde_json::from_slice(&res)?;
275 auth.map(|v| v.create_authorization(client)).into()
276}
277
278#[derive(Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct CreateKey<'a> {
285 account_id: Option<&'a str>,
287 capabilities: Vec<Capability>,
288 key_name: String,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 valid_duration_in_seconds: Option<Duration>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 bucket_id: Option<String>,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 name_prefix: Option<String>,
295}
296
297impl<'a> CreateKey<'a> {
298 pub fn builder() -> CreateKeyBuilder {
299 CreateKeyBuilder::default()
300 }
301}
302
303#[derive(Default)]
311pub struct CreateKeyBuilder {
312 capabilities: Option<Vec<Capability>>,
313 name: Option<String>,
314 valid_duration: Option<Duration>,
315 bucket_id: Option<String>,
316 name_prefix: Option<String>,
317}
318
319impl CreateKeyBuilder {
320 pub fn name<S: Into<String>>(mut self, name: S)
322 -> Result<Self, ValidationError> {
323 let name = name.into();
325
326 if name.is_empty() {
327 return Err(ValidationError::MissingData(
331 "A key name must be present".into()
332 ));
333 } else if name.len() > 100 {
334 return Err(ValidationError::BadFormat(
335 "Name must be no more than 100 characters.".into()
336 ));
337 }
338
339 let invalid_char = |c: &char| !(c.is_alphanumeric() || *c == '-');
340
341 if let Some(ch) = name.chars().find(invalid_char) {
342 return Err(
343 ValidationError::BadFormat(format!("Invalid character: {}", ch))
344 );
345 }
346
347 self.name = Some(name);
348 Ok(self)
349 }
350
351 pub fn capabilities<V: Into<Vec<Capability>>>(mut self, caps: V)
355 -> Result<Self, ValidationError> {
356 let caps = caps.into();
357
358 if caps.is_empty() {
359 return Err(ValidationError::MissingData(
360 "A key must have at least one capability.".into()
361 ));
362 }
363
364 self.capabilities = Some(caps);
365 Ok(self)
366 }
367
368 pub fn expires_after(mut self, dur: chrono::Duration)
372 -> Result<Self, ValidationError> {
373 if dur >= chrono::Duration::days(1000) {
374 return Err(ValidationError::OutOfBounds(
375 "Expiration must be less than 1000 days".into()
376 ));
377 } else if dur < chrono::Duration::seconds(1) {
378 return Err(ValidationError::OutOfBounds(
379 "Expiration must be a positive number of seconds".into()
380 ));
381 }
382
383 self.valid_duration = Some(Duration(dur));
384 Ok(self)
385 }
386
387 pub fn limit_to_bucket<S: Into<String>>(mut self, id: S)
389 -> Result<Self, ValidationError> {
390 self.bucket_id = Some(id.into());
391 Ok(self)
392 }
393
394 pub fn name_prefix<S: Into<String>>(mut self, prefix: S)
396 -> Result<Self, ValidationError> {
397 let prefix = prefix.into();
398 self.name_prefix = Some(prefix);
401 Ok(self)
402 }
403
404 pub fn build<'a>(self) -> Result<CreateKey<'a>, ValidationError> {
406 let name = self.name.ok_or_else(||
407 ValidationError::MissingData(
408 "A name for the key must be provided".into()
409 )
410 )?;
411
412 let capabilities = self.capabilities.ok_or_else(||
413 ValidationError::MissingData(
414 "A list of capabilities for the key is required.".into()
415 )
416 )?;
417
418 if self.bucket_id.is_some() {
419 for cap in &capabilities {
420 match cap {
421 Capability::ListAllBucketNames
422 | Capability::ListBuckets
423 | Capability::ReadBuckets
424 | Capability::ReadBucketEncryption
425 | Capability::WriteBucketEncryption
426 | Capability::ReadBucketRetentions
427 | Capability::WriteBucketRetentions
428 | Capability::ListFiles
429 | Capability::ReadFiles
430 | Capability::ShareFiles
431 | Capability::WriteFiles
432 | Capability::DeleteFiles
433 | Capability::ReadFileLegalHolds
434 | Capability::WriteFileLegalHolds
435 | Capability::ReadFileRetentions
436 | Capability::WriteFileRetentions
437 | Capability::BypassGovernance
438 | Capability::ReadBucketReplications
439 | Capability::WriteBucketReplications => {},
440 cap => return Err(ValidationError::Incompatible(format!(
441 "Invalid capability when bucket_id is set: {:?}",
442 cap
443 ))),
444 }
445 }
446 } else if self.name_prefix.is_some() {
447 return Err(ValidationError::MissingData(
448 "bucket_id must be set when name_prefix is given".into()
449 ));
450 }
451
452 Ok(CreateKey {
453 account_id: None,
454 capabilities,
455 key_name: name,
456 valid_duration_in_seconds: self.valid_duration,
457 bucket_id: self.bucket_id,
458 name_prefix: self.name_prefix,
459 })
460 }
461}
462
463#[derive(Debug, Clone, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct Key {
467 key_name: String,
468 application_key_id: String,
469 capabilities: Vec<Capability>,
470 account_id: String,
471 expiration_timestamp: Option<DateTime<Utc>>,
472 bucket_id: Option<String>,
473 name_prefix: Option<String>,
474 }
476
477impl Key {
478 pub fn key_name(&self) -> &str { &self.key_name }
480 pub fn key_id(&self) -> &str { &self.application_key_id }
484 pub fn capabilities(&self) -> &[Capability] { &self.capabilities }
486 pub fn account_id(&self) -> &str { &self.account_id }
488 pub fn bucket_id(&self) -> Option<&String> { self.bucket_id.as_ref() }
491 pub fn name_prefix(&self) -> Option<&String> { self.name_prefix.as_ref() }
493
494 pub fn expiration(&self) -> Option<DateTime<Utc>> {
496 self.expiration_timestamp
497 }
498
499 pub fn has_capability(&self, cap: Capability) -> bool {
501 self.capabilities.iter().any(|&c| c == cap)
502 }
503}
504
505#[derive(Debug, Deserialize)]
506#[serde(rename_all = "camelCase")]
507struct NewlyCreatedKey {
508 application_key: String,
511
512 key_name: String,
514 application_key_id: String,
515 capabilities: Vec<Capability>,
516 account_id: String,
517 expiration_timestamp: Option<DateTime<Utc>>,
518 bucket_id: Option<String>,
519 name_prefix: Option<String>,
520 }
522
523impl NewlyCreatedKey {
524 fn create_public_key(self) -> (String, Key) {
525 let secret = self.application_key;
526
527 let key = Key {
528 key_name: self.key_name,
529 application_key_id: self.application_key_id,
530 capabilities: self.capabilities,
531 account_id: self.account_id,
532 expiration_timestamp: self.expiration_timestamp,
533 bucket_id: self.bucket_id,
534 name_prefix: self.name_prefix,
535 };
536
537 (secret, key)
538 }
539}
540
541pub async fn create_key<C, E>(
575 auth: &mut Authorization<C>,
576 new_key_info: CreateKey<'_>
577) -> Result<(String, Key), Error<E>>
578 where C: HttpClient<Error=Error<E>>,
579 E: fmt::Debug + fmt::Display,
580{
581 require_capability!(auth, Capability::WriteKeys);
582
583 let mut new_key_info = new_key_info;
584 new_key_info.account_id = Some(&auth.account_id);
585
586 let res = auth.client.post(auth.api_url("b2_create_key"))
587 .expect("Invalid URL")
588 .with_header("Authorization", &auth.authorization_token).unwrap()
589 .with_body_json(serde_json::to_value(new_key_info)?)
590 .send().await?;
591
592 let new_key: B2Result<NewlyCreatedKey> = serde_json::from_slice(&res)?;
593 new_key.map(|key| key.create_public_key()).into()
594}
595
596pub async fn delete_key<C, E>(auth: &mut Authorization<C>, key: Key)
630-> Result<Key, Error<E>>
631 where C: HttpClient<Error=Error<E>>,
632 E: fmt::Debug + fmt::Display,
633{
634 delete_key_by_id(auth, key.application_key_id).await
635}
636
637pub async fn delete_key_by_id<C, E, S: AsRef<str>>(
664 auth: &mut Authorization<C>,
665 key_id: S
666) -> Result<Key, Error<E>>
667 where C: HttpClient<Error=Error<E>>,
668 E: fmt::Debug + fmt::Display,
669{
670 require_capability!(auth, Capability::DeleteKeys);
671
672 let res = auth.client.post(auth.api_url("b2_delete_key"))
673 .expect("Invalid URL")
674 .with_header("Authorization", &auth.authorization_token).unwrap()
675 .with_body_json(serde_json::json!(
676 {"applicationKeyId": key_id.as_ref()}
677 ))
678 .send().await?;
679
680 let key: B2Result<Key> = serde_json::from_slice(&res)?;
681 key.into()
682}
683
684#[derive(Debug, Clone, Serialize)]
689#[serde(rename_all = "camelCase")]
690pub struct ListKeys<'a> {
691 account_id: Option<&'a str>,
693 max_key_count: u16,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 start_application_key_id: Option<String>,
696}
697
698impl<'a> ListKeys<'a> {
699 pub fn builder() -> ListKeysBuilder {
700 ListKeysBuilder::default()
701 }
702}
703
704impl<'a> Default for ListKeys<'a> {
705 fn default() -> Self {
706 ListKeysBuilder::default().build()
707 }
708}
709
710#[derive(Debug)]
718pub struct ListKeysBuilder {
719 max_keys: u16,
720 start_key_id: Option<String>,
721}
722
723impl Default for ListKeysBuilder {
724 fn default() -> Self {
725 Self {
726 max_keys: 100,
727 start_key_id: None,
728 }
729 }
730}
731
732impl ListKeysBuilder {
733 pub fn max_keys(mut self, limit: u16) -> Result<Self, ValidationError> {
738 if limit > 10000 {
739 return Err(ValidationError::OutOfBounds(
740 "Key listing limit is 10,000".into()
741 ));
742 }
743
744 self.max_keys = limit;
745 Ok(self)
746 }
747
748 pub fn start_at_key(mut self, id: impl Into<String>)
750 -> Result<Self, ValidationError> {
751 self.start_key_id = Some(id.into());
752 Ok(self)
753 }
754
755 pub fn build<'a>(self) -> ListKeys<'a> {
757 ListKeys {
758 account_id: None,
759 max_key_count: self.max_keys,
760 start_application_key_id: self.start_key_id,
761 }
762 }
763}
764
765#[derive(Debug, Deserialize)]
766#[serde(rename_all = "camelCase")]
767struct KeyList {
768 keys: Vec<Key>,
769 next_application_key_id: Option<String>,
770}
771
772pub async fn list_keys<'a, C, E>(
818 auth: &'a mut Authorization<C>,
819 list_req: ListKeys<'a>
820) -> Result<(Vec<Key>, Option<ListKeys<'a>>), Error<E>>
821 where C: HttpClient<Error=Error<E>>,
822 E: fmt::Debug + fmt::Display,
823{
824 require_capability!(auth, Capability::ListKeys);
825
826 let mut list_req = list_req;
827 list_req.account_id = Some(&auth.account_id);
828
829 let res = auth.client.post(auth.api_url("b2_list_keys"))
830 .expect("Invalid URL")
831 .with_header("Authorization", &auth.authorization_token).unwrap()
832 .with_body_json(serde_json::to_value(list_req.clone())?)
833 .send().await?;
834
835 let keys: B2Result<KeyList> = serde_json::from_slice(&res)?;
836
837 match keys {
838 B2Result::Ok(keys) => {
839 if let Some(id) = keys.next_application_key_id {
840 Ok((
841 keys.keys,
842 Some(ListKeys {
843 account_id: Some(&auth.account_id),
844 max_key_count: list_req.max_key_count,
845 start_application_key_id: Some(id),
846 })
847 ))
848 } else {
849 Ok((keys.keys, None))
850 }
851 },
852 B2Result::Err(e) => Err(e.into())
853 }
854}
855
856
857#[cfg(feature = "with_surf")]
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use crate::{
863 error::ErrorCode,
864 test_utils::{create_test_auth, create_test_client},
865 };
866 use surf_vcr::VcrMode;
867
868
869 fn get_key() -> (String, String) {
876 let id = std::env::var("B2_CLIENT_TEST_KEY_ID")
877 .unwrap_or_else(|_| "B2_KEY_ID".to_owned());
878 let key = std::env::var("B2_CLIENT_TEST_KEY")
879 .unwrap_or_else(|_| "B2_AUTH_KEY".to_owned());
880
881 (id, key)
882 }
883
884 #[async_std::test]
885 async fn test_authorize_account() -> Result<(), anyhow::Error> {
886 let client = create_test_client(
887 VcrMode::Replay,
888 "test_sessions/auth_account.yaml",
889 None, None
890 ).await?;
891
892 let (id, key) = get_key();
893
894 let auth = authorize_account(client, &id, &key).await?;
895 assert!(auth.allowed.capabilities.contains(&Capability::ListBuckets));
896
897 Ok(())
898 }
899
900 #[async_std::test]
901 async fn authorize_account_bad_key() -> Result<(), anyhow::Error> {
902 let client = create_test_client(
903 VcrMode::Replay,
904 "test_sessions/bad_auth_account.yaml",
905 None, None
906 ).await?;
907
908 let (id, _) = get_key();
909 let auth = authorize_account(client, &id, "wrong-key").await;
910
911 match auth.unwrap_err() {
912 Error::B2(e) => assert_eq!(e.code(), ErrorCode::BadAuthToken),
915 _ => panic!("Unexpected error type"),
916 }
917
918 Ok(())
919 }
920
921 #[async_std::test]
922 async fn authorize_account_bad_key_id() -> Result<(), anyhow::Error> {
923 let client = create_test_client(
924 VcrMode::Replay,
925 "test_sessions/bad_auth_account.yaml",
926 None, None
927 ).await?;
928
929 let (_, key) = get_key();
930 let auth = authorize_account(client, "wrong-id", &key).await;
931
932 match auth.unwrap_err() {
933 Error::B2(e) => assert_eq!(e.code(), ErrorCode::BadAuthToken),
936 e => panic!("Unexpected error type: {:?}", e),
937 }
938
939 Ok(())
940 }
941
942 #[async_std::test]
943 async fn test_create_key() -> Result<(), anyhow::Error> {
944 let client = create_test_client(
945 VcrMode::Replay,
946 "test_sessions/auth_account.yaml",
947 None, None
948 ).await?;
949
950 let mut auth = create_test_auth(client, vec![Capability::WriteKeys])
951 .await;
952
953 let new_key_info = CreateKey::builder()
954 .name("my-special-key")
955 .unwrap()
956 .capabilities(vec![Capability::ListFiles]).unwrap()
957 .build().unwrap();
958
959 let (secret, key) = create_key(&mut auth, new_key_info).await?;
960 assert!(! secret.is_empty());
961 assert_eq!(key.capabilities.len(), 1);
962 assert_eq!(key.capabilities[0], Capability::ListFiles);
963
964 Ok(())
965 }
966
967 #[async_std::test]
968 async fn test_delete_key() -> Result<(), anyhow::Error> {
969 let client = create_test_client(
973 VcrMode::Replay,
974 "test_sessions/auth_account.yaml",
975 None, None
976 ).await?;
977
978 let mut auth = create_test_auth(client, vec![Capability::DeleteKeys])
979 .await;
980
981 let removed_key = delete_key_by_id(
982 &mut auth, "002d2e6b27577ea0000000008"
983 ).await?;
984
985 assert_eq!(removed_key.key_name, "my-special-key");
986
987 Ok(())
988 }
989
990 #[async_std::test]
991 async fn test_list_keys() -> Result<(), anyhow::Error> {
992 let client = create_test_client(
993 VcrMode::Replay,
994 "test_sessions/auth_account.yaml",
995 None, None
996 ).await?;
997
998 let mut auth = create_test_auth(client, vec![Capability::ListKeys])
999 .await;
1000
1001 let req = ListKeys::default();
1002
1003 let (keys, next) = list_keys(&mut auth, req).await?;
1004 assert_eq!(keys.len(), 1);
1005 assert!(next.is_none());
1006
1007 Ok(())
1008 }
1009}