use std::fmt;
use crate::{
prelude::*,
client::HttpClient,
error::{ValidationError, Error},
types::*,
};
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};
const B2_AUTH_URL: &str = "https://api.backblazeb2.com/b2api/v2/";
#[derive(Debug)]
pub struct Authorization<C>
where C: HttpClient,
{
pub(crate) client: C,
pub(crate) account_id: String,
pub(crate) authorization_token: String,
allowed: Capabilities,
pub(crate) api_url: String,
pub(crate) download_url: String,
recommended_part_size: u64,
absolute_minimum_part_size: u64,
_s3_api_url: String,
}
impl<C> Authorization<C>
where C: HttpClient,
{
#[cfg(test)]
#[allow(dead_code)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
client: C,
account_id: String,
authorization_token: String,
allowed: Capabilities,
api_url: String,
download_url: String,
recommended_part_size: u64,
absolute_minimum_part_size: u64,
_s3_api_url: String,
) -> Self {
Self {
client,
account_id,
authorization_token,
allowed,
api_url,
download_url,
recommended_part_size,
absolute_minimum_part_size,
_s3_api_url,
}
}
pub fn authorization_token(&self) -> &str { &self.authorization_token }
pub fn account_id(&self) -> &str { &self.account_id }
pub fn capabilities(&self) -> &Capabilities { &self.allowed }
pub fn recommended_part_size(&self) -> u64 { self.recommended_part_size }
pub fn minimum_part_size(&self) -> u64 { self.absolute_minimum_part_size }
pub fn has_capability(&self, cap: Capability) -> bool {
self.allowed.has_capability(cap)
}
pub(crate) fn api_url<S: AsRef<str>>(&self, endpoint: S) -> String {
format!("{}/b2api/v2/{}", self.api_url, endpoint.as_ref())
}
pub(crate) fn download_get_url(&self) -> &str {
&self.download_url
}
pub(crate) fn download_url<S: AsRef<str>>(&self, endpoint: S) -> String {
format!("{}/b2api/v2/{}", self.download_url, endpoint.as_ref())
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ProtoAuthorization {
account_id: String,
authorization_token: String,
allowed: Capabilities,
api_url: String,
download_url: String,
recommended_part_size: u64,
absolute_minimum_part_size: u64,
_s3_api_url: String,
}
impl ProtoAuthorization {
fn create_authorization<C: HttpClient>(self, c: C) -> Authorization<C> {
Authorization {
client: c,
account_id: self.account_id,
authorization_token: self.authorization_token,
allowed: self.allowed,
api_url: self.api_url,
download_url: self.download_url,
recommended_part_size: self.recommended_part_size,
absolute_minimum_part_size: self.absolute_minimum_part_size,
_s3_api_url: self._s3_api_url,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Capabilities {
capabilities: Vec<Capability>,
bucket_id: Option<String>,
bucket_name: Option<String>,
name_prefix: Option<String>,
}
impl Capabilities {
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn new(
capabilities: Vec<Capability>,
bucket_id: Option<String>,
bucket_name: Option<String>,
name_prefix: Option<String>,
) -> Self {
Self {
capabilities,
bucket_id,
bucket_name,
name_prefix,
}
}
pub fn capabilities(&self) -> &[Capability] { &self.capabilities }
pub fn bucket_id(&self) -> Option<&String> { self.bucket_id.as_ref() }
pub fn bucket_name(&self) -> Option<&String> { self.bucket_name.as_ref() }
pub fn name_prefix(&self) -> Option<&String> { self.name_prefix.as_ref() }
pub fn has_capability(&self, cap: Capability) -> bool {
self.capabilities.iter().any(|&c| c == cap)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Capability {
ListKeys,
WriteKeys,
DeleteKeys,
ListAllBucketNames,
ListBuckets,
ReadBuckets,
WriteBuckets,
DeleteBuckets,
ReadBucketRetentions,
WriteBucketRetentions,
ReadBucketEncryption,
WriteBucketEncryption,
ListFiles,
ReadFiles,
ShareFiles,
WriteFiles,
DeleteFiles,
ReadFileLegalHolds,
WriteFileLegalHolds,
ReadFileRetentions,
WriteFileRetentions,
BypassGovernance,
ReadBucketReplications,
WriteBucketReplications,
}
pub async fn authorize_account<C, E>(mut client: C, key_id: &str, key: &str)
-> Result<Authorization<C>, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
let id_and_key = format!("{}:{}", key_id, key);
let id_and_key = base64::encode(id_and_key.as_bytes());
let mut auth = String::from("Basic ");
auth.push_str(&id_and_key);
let req = client.get(
format!("{}b2_authorize_account", B2_AUTH_URL)
).expect("Invalid URL")
.with_header("Authorization", &auth).unwrap();
let res = req.send().await?;
let auth: B2Result<ProtoAuthorization> = serde_json::from_slice(&res)?;
auth.map(|v| v.create_authorization(client)).into()
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateKey<'a> {
account_id: Option<&'a str>,
capabilities: Vec<Capability>,
key_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
valid_duration_in_seconds: Option<Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name_prefix: Option<String>,
}
impl<'a> CreateKey<'a> {
pub fn builder() -> CreateKeyBuilder {
CreateKeyBuilder::default()
}
}
#[derive(Default)]
pub struct CreateKeyBuilder {
capabilities: Option<Vec<Capability>>,
name: Option<String>,
valid_duration: Option<Duration>,
bucket_id: Option<String>,
name_prefix: Option<String>,
}
impl CreateKeyBuilder {
pub fn name<S: Into<String>>(mut self, name: S)
-> Result<Self, ValidationError> {
let name = name.into();
if name.is_empty() {
return Err(ValidationError::MissingData(
"A key name must be present".into()
));
} else if name.len() > 100 {
return Err(ValidationError::BadFormat(
"Name must be no more than 100 characters.".into()
));
}
let invalid_char = |c: &char| !(c.is_alphanumeric() || *c == '-');
if let Some(ch) = name.chars().find(invalid_char) {
return Err(
ValidationError::BadFormat(format!("Invalid character: {}", ch))
);
}
self.name = Some(name);
Ok(self)
}
pub fn capabilities<V: Into<Vec<Capability>>>(mut self, caps: V)
-> Result<Self, ValidationError> {
let caps = caps.into();
if caps.is_empty() {
return Err(ValidationError::MissingData(
"A key must have at least one capability.".into()
));
}
self.capabilities = Some(caps);
Ok(self)
}
pub fn expires_after(mut self, dur: chrono::Duration)
-> Result<Self, ValidationError> {
if dur >= chrono::Duration::days(1000) {
return Err(ValidationError::OutOfBounds(
"Expiration must be less than 1000 days".into()
));
} else if dur < chrono::Duration::seconds(1) {
return Err(ValidationError::OutOfBounds(
"Expiration must be a positive number of seconds".into()
));
}
self.valid_duration = Some(Duration(dur));
Ok(self)
}
pub fn limit_to_bucket<S: Into<String>>(mut self, id: S)
-> Result<Self, ValidationError> {
self.bucket_id = Some(id.into());
Ok(self)
}
pub fn name_prefix<S: Into<String>>(mut self, prefix: S)
-> Result<Self, ValidationError> {
let prefix = prefix.into();
self.name_prefix = Some(prefix);
Ok(self)
}
pub fn build<'a>(self) -> Result<CreateKey<'a>, ValidationError> {
let name = self.name.ok_or_else(||
ValidationError::MissingData(
"A name for the key must be provided".into()
)
)?;
let capabilities = self.capabilities.ok_or_else(||
ValidationError::MissingData(
"A list of capabilities for the key is required.".into()
)
)?;
if self.bucket_id.is_some() {
for cap in &capabilities {
match cap {
Capability::ListAllBucketNames
| Capability::ListBuckets
| Capability::ReadBuckets
| Capability::ReadBucketEncryption
| Capability::WriteBucketEncryption
| Capability::ReadBucketRetentions
| Capability::WriteBucketRetentions
| Capability::ListFiles
| Capability::ReadFiles
| Capability::ShareFiles
| Capability::WriteFiles
| Capability::DeleteFiles
| Capability::ReadFileLegalHolds
| Capability::WriteFileLegalHolds
| Capability::ReadFileRetentions
| Capability::WriteFileRetentions
| Capability::BypassGovernance
| Capability::ReadBucketReplications
| Capability::WriteBucketReplications => {},
cap => return Err(ValidationError::Incompatible(format!(
"Invalid capability when bucket_id is set: {:?}",
cap
))),
}
}
} else if self.name_prefix.is_some() {
return Err(ValidationError::MissingData(
"bucket_id must be set when name_prefix is given".into()
));
}
Ok(CreateKey {
account_id: None,
capabilities,
key_name: name,
valid_duration_in_seconds: self.valid_duration,
bucket_id: self.bucket_id,
name_prefix: self.name_prefix,
})
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Key {
key_name: String,
application_key_id: String,
capabilities: Vec<Capability>,
account_id: String,
expiration_timestamp: Option<DateTime<Utc>>,
bucket_id: Option<String>,
name_prefix: Option<String>,
}
impl Key {
pub fn key_name(&self) -> &str { &self.key_name }
pub fn key_id(&self) -> &str { &self.application_key_id }
pub fn capabilities(&self) -> &[Capability] { &self.capabilities }
pub fn account_id(&self) -> &str { &self.account_id }
pub fn bucket_id(&self) -> Option<&String> { self.bucket_id.as_ref() }
pub fn name_prefix(&self) -> Option<&String> { self.name_prefix.as_ref() }
pub fn expiration(&self) -> Option<DateTime<Utc>> {
self.expiration_timestamp
}
pub fn has_capability(&self, cap: Capability) -> bool {
self.capabilities.iter().any(|&c| c == cap)
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct NewlyCreatedKey {
application_key: String,
key_name: String,
application_key_id: String,
capabilities: Vec<Capability>,
account_id: String,
expiration_timestamp: Option<DateTime<Utc>>,
bucket_id: Option<String>,
name_prefix: Option<String>,
}
impl NewlyCreatedKey {
fn create_public_key(self) -> (String, Key) {
let secret = self.application_key;
let key = Key {
key_name: self.key_name,
application_key_id: self.application_key_id,
capabilities: self.capabilities,
account_id: self.account_id,
expiration_timestamp: self.expiration_timestamp,
bucket_id: self.bucket_id,
name_prefix: self.name_prefix,
};
(secret, key)
}
}
pub async fn create_key<C, E>(
auth: &mut Authorization<C>,
new_key_info: CreateKey<'_>
) -> Result<(String, Key), Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::WriteKeys);
let mut new_key_info = new_key_info;
new_key_info.account_id = Some(&auth.account_id);
let res = auth.client.post(auth.api_url("b2_create_key"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::to_value(new_key_info)?)
.send().await?;
let new_key: B2Result<NewlyCreatedKey> = serde_json::from_slice(&res)?;
new_key.map(|key| key.create_public_key()).into()
}
pub async fn delete_key<C, E>(auth: &mut Authorization<C>, key: Key)
-> Result<Key, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
delete_key_by_id(auth, key.application_key_id).await
}
pub async fn delete_key_by_id<C, E, S: AsRef<str>>(
auth: &mut Authorization<C>,
key_id: S
) -> Result<Key, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::DeleteKeys);
let res = auth.client.post(auth.api_url("b2_delete_key"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::json!(
{"applicationKeyId": key_id.as_ref()}
))
.send().await?;
let key: B2Result<Key> = serde_json::from_slice(&res)?;
key.into()
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ListKeys<'a> {
account_id: Option<&'a str>,
max_key_count: u16,
#[serde(skip_serializing_if = "Option::is_none")]
start_application_key_id: Option<String>,
}
impl<'a> ListKeys<'a> {
pub fn builder() -> ListKeysBuilder {
ListKeysBuilder::default()
}
}
impl<'a> Default for ListKeys<'a> {
fn default() -> Self {
ListKeysBuilder::default().build()
}
}
#[derive(Debug)]
pub struct ListKeysBuilder {
max_keys: u16,
start_key_id: Option<String>,
}
impl Default for ListKeysBuilder {
fn default() -> Self {
Self {
max_keys: 100,
start_key_id: None,
}
}
}
impl ListKeysBuilder {
pub fn max_keys(mut self, limit: u16) -> Result<Self, ValidationError> {
if limit > 10000 {
return Err(ValidationError::OutOfBounds(
"Key listing limit is 10,000".into()
));
}
self.max_keys = limit;
Ok(self)
}
pub fn start_at_key(mut self, id: impl Into<String>)
-> Result<Self, ValidationError> {
self.start_key_id = Some(id.into());
Ok(self)
}
pub fn build<'a>(self) -> ListKeys<'a> {
ListKeys {
account_id: None,
max_key_count: self.max_keys,
start_application_key_id: self.start_key_id,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct KeyList {
keys: Vec<Key>,
next_application_key_id: Option<String>,
}
pub async fn list_keys<'a, C, E>(
auth: &'a mut Authorization<C>,
list_req: ListKeys<'a>
) -> Result<(Vec<Key>, Option<ListKeys<'a>>), Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::ListKeys);
let mut list_req = list_req;
list_req.account_id = Some(&auth.account_id);
let res = auth.client.post(auth.api_url("b2_list_keys"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::to_value(list_req.clone())?)
.send().await?;
let keys: B2Result<KeyList> = serde_json::from_slice(&res)?;
match keys {
B2Result::Ok(keys) => {
if let Some(id) = keys.next_application_key_id {
Ok((
keys.keys,
Some(ListKeys {
account_id: Some(&auth.account_id),
max_key_count: list_req.max_key_count,
start_application_key_id: Some(id),
})
))
} else {
Ok((keys.keys, None))
}
},
B2Result::Err(e) => Err(e.into())
}
}
#[cfg(feature = "with_surf")]
#[cfg(test)]
mod tests {
use super::*;
use crate::{
error::ErrorCode,
test_utils::{create_test_auth, create_test_client},
};
use surf_vcr::VcrMode;
fn get_key() -> (String, String) {
let id = std::env::var("B2_CLIENT_TEST_KEY_ID")
.unwrap_or_else(|_| "B2_KEY_ID".to_owned());
let key = std::env::var("B2_CLIENT_TEST_KEY")
.unwrap_or_else(|_| "B2_AUTH_KEY".to_owned());
(id, key)
}
#[async_std::test]
async fn test_authorize_account() -> Result<(), anyhow::Error> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/auth_account.yaml",
None, None
).await?;
let (id, key) = get_key();
let auth = authorize_account(client, &id, &key).await?;
assert!(auth.allowed.capabilities.contains(&Capability::ListBuckets));
Ok(())
}
#[async_std::test]
async fn authorize_account_bad_key() -> Result<(), anyhow::Error> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/bad_auth_account.yaml",
None, None
).await?;
let (id, _) = get_key();
let auth = authorize_account(client, &id, "wrong-key").await;
match auth.unwrap_err() {
Error::B2(e) => assert_eq!(e.code(), ErrorCode::BadAuthToken),
_ => panic!("Unexpected error type"),
}
Ok(())
}
#[async_std::test]
async fn authorize_account_bad_key_id() -> Result<(), anyhow::Error> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/bad_auth_account.yaml",
None, None
).await?;
let (_, key) = get_key();
let auth = authorize_account(client, "wrong-id", &key).await;
match auth.unwrap_err() {
Error::B2(e) => assert_eq!(e.code(), ErrorCode::BadAuthToken),
e => panic!("Unexpected error type: {:?}", e),
}
Ok(())
}
#[async_std::test]
async fn test_create_key() -> Result<(), anyhow::Error> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/auth_account.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::WriteKeys])
.await;
let new_key_info = CreateKey::builder()
.name("my-special-key")
.unwrap()
.capabilities(vec![Capability::ListFiles]).unwrap()
.build().unwrap();
let (secret, key) = create_key(&mut auth, new_key_info).await?;
assert!(! secret.is_empty());
assert_eq!(key.capabilities.len(), 1);
assert_eq!(key.capabilities[0], Capability::ListFiles);
Ok(())
}
#[async_std::test]
async fn test_delete_key() -> Result<(), anyhow::Error> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/auth_account.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::DeleteKeys])
.await;
let removed_key = delete_key_by_id(
&mut auth, "002d2e6b27577ea0000000008"
).await?;
assert_eq!(removed_key.key_name, "my-special-key");
Ok(())
}
#[async_std::test]
async fn test_list_keys() -> Result<(), anyhow::Error> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/auth_account.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::ListKeys])
.await;
let req = ListKeys::default();
let (keys, next) = list_keys(&mut auth, req).await?;
assert_eq!(keys.len(), 1);
assert!(next.is_none());
Ok(())
}
}