b2_client/
account.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2   License, v. 2.0. If a copy of the MPL was not distributed with this
3   file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6//! Account-related B2 API calls.
7
8use 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/// Authorization token and related information obtained from
24/// [authorize_account].
25///
26/// The token is valid for no more than 24 hours.
27#[derive(Debug)]
28pub struct Authorization<C>
29    where C: HttpClient,
30{
31    pub(crate) client: C,
32    pub(crate) account_id: String,
33    // The authorization token to use for all future API calls.
34    //
35    // The token is valid for no more than 24 hours.
36    pub(crate) authorization_token: String,
37    allowed: Capabilities,
38    // The base URL for all API calls except uploading or downloading files.
39    pub(crate) api_url: String,
40    // The base URL to use for downloading files.
41    pub(crate) download_url: String,
42    recommended_part_size: u64,
43    absolute_minimum_part_size: u64,
44    // The base URL to use for all API calls using the AWS S3-compatible API.j
45    _s3_api_url: String,
46}
47
48impl<C> Authorization<C>
49    where C: HttpClient,
50{
51    // Allow tests to create fake Authorizations.
52    #[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    /// The authorization token used for Backblaze requests.
80    pub fn authorization_token(&self) -> &str { &self.authorization_token }
81
82    /// The ID for the account.
83    pub fn account_id(&self) -> &str { &self.account_id }
84    /// The capabilities granted to this auth token.
85    pub fn capabilities(&self) -> &Capabilities { &self.allowed }
86    /// The recommended size in bytes for each part of a large file.
87    pub fn recommended_part_size(&self) -> u64 { self.recommended_part_size }
88    /// The smallest possible size in bytes of a part of a large file, except
89    /// the final part.
90    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    /// Return the API url to the specified service endpoint.
97    ///
98    /// This URL is used for all API calls except downloading files.
99    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    /// Return the API url for GET requests to the specified service download
104    /// endpoint.
105    pub(crate) fn download_get_url(&self) -> &str {
106        &self.download_url
107    }
108
109    /// Return the API url for POST requests to the specified service download
110    /// endpoint.
111    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/// The authorization information received from B2
117///
118/// The public [Authorization] object contains everything here, plus private
119/// data used by this API implementation, such as the HTTP client.
120#[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/// The set of capabilities and associated information granted by an
150/// authorization token.
151#[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    // Allow tests to create Capabilities.
162    #[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    /// The list of capabilities granted.
179    pub fn capabilities(&self) -> &[Capability] { &self.capabilities }
180    /// If the capabilities are limited to a single bucket, this is the bucket's
181    /// ID.
182    pub fn bucket_id(&self) -> Option<&String> { self.bucket_id.as_ref() }
183    /// If the bucket is valid and hasn't been deleted, the name of the bucket
184    /// corresponding to `bucket_id`. If the bucket referred to by `bucket_id`
185    /// no longer exists, this will be `None`.
186    pub fn bucket_name(&self) -> Option<&String> { self.bucket_name.as_ref() }
187    /// If set, access is limited to files whose names begin with this prefix.
188    pub fn name_prefix(&self) -> Option<&String> { self.name_prefix.as_ref() }
189
190    /// Check if the provided capability is granted to the object containing
191    /// this [Capabilities] object.
192    pub fn has_capability(&self, cap: Capability) -> bool {
193        self.capabilities.iter().any(|&c| c == cap)
194    }
195}
196
197/// A capability potentially granted by an authorization token.
198#[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
227/// Log onto the B2 API.
228///
229/// The returned [Authorization] object must be passed to subsequent API calls.
230///
231/// You can obtain the `key_id` and `key` from the B2 administration pages or
232/// from [create_key].
233///
234/// See <https://www.backblaze.com/b2/docs/b2_authorize_account.html> for
235/// further information.
236///
237/// # Examples
238///
239/// ```no_run
240/// # #[cfg(feature = "with_surf")]
241/// # use b2_client::{
242/// #     client::{HttpClient, SurfClient},
243/// #     account::{authorize_account, delete_key_by_id},
244/// # };
245/// # #[cfg(feature = "with_surf")]
246/// # async fn f() -> anyhow::Result<()> {
247/// let mut auth = authorize_account(
248///     SurfClient::default(),
249///     "MY KEY ID",
250///     "MY KEY"
251/// ).await?;
252///
253/// let removed_key = delete_key_by_id(&mut auth, "OTHER KEY ID").await?;
254/// # Ok(()) }
255/// ```
256pub 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/// A request to create a B2 API key with certain capabilities.
279///
280/// Use [CreateKeyBuilder] to create a `CreateKey` object, then pass it to
281/// [create_key] to create a new application [Key] from the request.
282#[derive(Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct CreateKey<'a> {
285    // account_id is provided by the Authorization object.
286    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/// A builder to create a [CreateKey] object.
304///
305/// After creating the `CreateKey`, pass it to [create_key] to obtain a new
306/// application key.
307///
308/// See <https://www.backblaze.com/b2/docs/b2_create_key.html> for more
309/// information.
310#[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    /// Create a new builder, with the key's name provided.
321    pub fn name<S: Into<String>>(mut self, name: S)
322    -> Result<Self, ValidationError> {
323        // TODO: Validation: name must be ASCII (not explicitly documented).
324        let name = name.into();
325
326        if name.is_empty() {
327            // I don't know the minimum name size, whether all characters can be
328            // '-', etc. They're not documented but I wouldn't be surprised if
329            // there are such restrictions.
330            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    /// Create the key with the specified capabilities.
352    ///
353    /// At least one capability must be provided.
354    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    /// Set an expiration duration for the key.
369    ///
370    /// If provided, the key must be positive and no more than 1,000 days.
371    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    /// Limit the key's access to the specified bucket.
388    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    /// Limit access to files to those that begin with the specified prefix.
395    pub fn name_prefix<S: Into<String>>(mut self, prefix: S)
396    -> Result<Self, ValidationError> {
397        let prefix = prefix.into();
398        // TODO: Validate prefix
399
400        self.name_prefix = Some(prefix);
401        Ok(self)
402    }
403
404    /// Create a new [CreateKey].
405    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/// An application key and associated information.
464#[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    // options: Option<Vec<String>>, // Currently unused by B2.
475}
476
477impl Key {
478    /// The name assigned to this key.
479    pub fn key_name(&self) -> &str { &self.key_name }
480    /// The application key ID. This, combined with the secret returned when the
481    /// key was created, allows you to create an [Authorization] token to make
482    /// API calls.
483    pub fn key_id(&self) -> &str { &self.application_key_id }
484    /// The list of capabilities granted by this key.
485    pub fn capabilities(&self) -> &[Capability] { &self.capabilities }
486    /// The account this key is for.
487    pub fn account_id(&self) -> &str { &self.account_id }
488    /// If present, this key's capabilities are restricted to the returned
489    /// bucket.
490    pub fn bucket_id(&self) -> Option<&String> { self.bucket_id.as_ref() }
491    /// If set, access is limited to files whose names begin with this prefix.
492    pub fn name_prefix(&self) -> Option<&String> { self.name_prefix.as_ref() }
493
494    /// If present, the expiration date and time of this key.
495    pub fn expiration(&self) -> Option<DateTime<Utc>> {
496        self.expiration_timestamp
497    }
498
499    /// Check if the provided capability is granted by this key.
500    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    // The private part of the key. This is only returned upon key creation, so
509    // must be stored in a safe place.
510    application_key: String,
511
512    // The rest of these are part of (and moved to) the Key.
513    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    // options: Option<Vec<String>>, Currently unused by B2.
521}
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
541/// Create a new API application key.
542///
543/// Returns a tuple of the key secret and the key capability information. The
544/// secret is never obtainable except by this function, so must be stored in a
545/// secure location.
546///
547/// See <https://www.backblaze.com/b2/docs/b2_create_key.html> for further
548/// information.
549///
550/// # Examples
551///
552/// ```no_run
553/// # #[cfg(feature = "with_surf")]
554/// # use b2_client::{
555/// #     client::{HttpClient, SurfClient},
556/// #     account::{authorize_account, create_key, Capability, CreateKey},
557/// # };
558/// # #[cfg(feature = "with_surf")]
559/// # async fn f() -> anyhow::Result<()> {
560/// let mut auth = authorize_account(
561///     SurfClient::default(),
562///     "MY KEY ID",
563///     "MY KEY"
564/// ).await?;
565///
566/// let create_key_request = CreateKey::builder()
567///     .name("my-key")?
568///     .capabilities([Capability::ListFiles])?
569///     .build()?;
570///
571/// let (secret, new_key) = create_key(&mut auth, create_key_request).await?;
572/// # Ok(()) }
573/// ```
574pub 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
596/// Delete the given [Key].
597///
598/// Returns a `Key` describing the just-deleted key.
599///
600/// See <https://www.backblaze.com/b2/docs/b2_delete_key.html> for further
601/// information.
602///
603/// ```no_run
604/// # #[cfg(feature = "with_surf")]
605/// # use b2_client::{
606/// #     client::{HttpClient, SurfClient},
607/// #     account::{
608/// #         authorize_account, create_key, delete_key, Capability, CreateKey,
609/// #     },
610/// # };
611/// # #[cfg(feature = "with_surf")]
612/// # async fn f() -> anyhow::Result<()> {
613/// let mut auth = authorize_account(
614///     SurfClient::default(),
615///     "MY KEY ID",
616///     "MY KEY"
617/// ).await?;
618///
619/// let create_key_request = CreateKey::builder()
620///     .name("my-key")?
621///     .capabilities([Capability::ListFiles])?
622///     .build()?;
623///
624/// let (_secret, new_key) = create_key(&mut auth, create_key_request).await?;
625///
626/// let deleted_key = delete_key(&mut auth, new_key).await?;
627/// # Ok(()) }
628/// ```
629pub 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
637/// Delete the key with the specified key ID.
638///
639/// Returns a [Key] describing the just-deleted key.
640///
641/// See <https://www.backblaze.com/b2/docs/b2_delete_key.html> for further
642/// information.
643///
644/// # Examples
645///
646/// ```no_run
647/// # #[cfg(feature = "with_surf")]
648/// # use b2_client::{
649/// #     client::{HttpClient, SurfClient},
650/// #     account::{authorize_account, delete_key_by_id},
651/// # };
652/// # #[cfg(feature = "with_surf")]
653/// # async fn f() -> anyhow::Result<()> {
654/// let mut auth = authorize_account(
655///     SurfClient::default(),
656///     "MY KEY ID",
657///     "MY KEY"
658/// ).await?;
659///
660/// let removed_key = delete_key_by_id(&mut auth, "OTHER KEY ID").await?;
661/// # Ok(()) }
662/// ```
663pub 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/// A request to obtain a list of keys associated with an account.
685///
686/// Use [ListKeysBuilder] to create a `ListKeys`, then pass it to
687/// [list_keys] to obtain the list of keys.
688#[derive(Debug, Clone, Serialize)]
689#[serde(rename_all = "camelCase")]
690pub struct ListKeys<'a> {
691    // account_id is provided by an Authorization.
692    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/// A builder to create a [ListKeys] object.
711///
712/// After creating the `ListKeys`, pass it to [list_keys] to obtain the
713/// list.
714///
715/// See <https://www.backblaze.com/b2/docs/b2_list_keys.html> for further
716/// information.
717#[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    /// Set the maximum number of keys to return in a single call to
734    /// [list_keys].
735    ///
736    /// The default is 100 and maximum is 10,000.
737    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    /// Set the key ID at which to begin listing.
749    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    /// Create a [ListKeys].
756    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
772/// List application keys associated with the account of the given
773/// [Authorization].
774///
775/// The `Authorization` must have [Capability::ListKeys].
776///
777/// Returns a tuple of the list of keys and the next [ListKeys] to obtain
778/// the next set of keys, if there are keys that have not yet been returned. The
779/// new key list request will have the same maximum key limit as the previous
780/// request.
781///
782/// A single function call can generate multiple Class C transactions, which may
783/// result in charges to your account. See
784/// <https://www.backblaze.com/b2/docs/b2_list_keys.html> for further
785/// information.
786///
787/// # Examples
788///
789/// ```no_run
790/// # #[cfg(feature = "with_surf")]
791/// # use b2_client::{
792/// #     client::{HttpClient, SurfClient},
793/// #     account::{
794/// #         authorize_account, list_keys,
795/// #         Capability, ListKeys,
796/// #     },
797/// # };
798/// # #[cfg(feature = "with_surf")]
799/// # async fn f() -> anyhow::Result<()> {
800/// let mut auth = authorize_account(
801///     SurfClient::default(),
802///     "MY KEY ID",
803///     "MY KEY"
804/// ).await?;
805///
806/// let req = ListKeys::builder()
807///     .max_keys(500)?
808///     .build();
809///
810/// let (keys, _next_req) = list_keys(&mut auth, req).await?;
811///
812/// for key in keys.iter() {
813///     // ...
814/// }
815/// # Ok(()) }
816/// ```
817pub 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// TODO: Find a good way to mock responses for any/all backends.
858#[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    /// Get the (key, id) pair from the environment for authorization tests.
870    ///
871    /// To make a call against the B2 API, the `B2_CLIENT_TEST_KEY` and
872    /// `B2_CLIENT_TEST_KEY_ID` environment variables must be set. Otherwise,
873    /// non-functional strings will be used that are adequate for replaying
874    /// pre-recorded tests.
875    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            // The B2 documentation says we'll receive `unauthorized`, but this
913            // is what we get.
914            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            // The B2 documentation says we'll receive `unauthorized`, but this
934            // is what we get.
935            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        // To run a real test against the B2 API, a valid key ID needs to be
970        // provided below.
971
972        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}