#![allow(clippy::not_unsafe_ptr_arg_deref, unsafe_code)]
use crate::client::AuthClient;
use crate::errors::AuthError;
use crate::ipc::decode_ipc_msg;
use crate::{access_container, app_auth, config, revocation, Authenticator};
use env_logger::{fmt::Formatter, Builder as LoggerBuilder};
use futures::Future;
use log::trace;
use log::Record;
use safe_core::client::{test_create_balance, Client};
use safe_core::core_structs::AccessContainerEntry;
use safe_core::core_structs::UserMetadata;
use safe_core::crypto::shared_secretbox;
use safe_core::ipc::req::{container_perms_into_permission_set, ContainerPermissions};
use safe_core::ipc::{self, AppExchangeInfo, AuthGranted, AuthReq, IpcMsg, IpcReq};
use safe_core::nfs::file_helper::{self, Version};
use safe_core::nfs::{File, Mode};
use safe_core::utils::test_utils::{gen_client_id, setup_client_with_net_obs};
#[cfg(feature = "mock-network")]
use safe_core::ConnectionManager;
use safe_core::{utils, MDataInfo, NetworkEvent};
use safe_nd::{AppPermissions, Coins, PublicKey};
#[cfg(feature = "mock-network")]
use safe_nd::{Error as SndError, MDataRequest, Request, Response};
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::Write;
use std::str::FromStr;
use unwrap::unwrap;
use xor_name::XorName;
#[macro_export]
macro_rules! assert_match {
($e:expr, $p:pat) => {
match $e {
$p => (),
x => panic!("Unexpected {:?} (expecting {})", x, stringify!($p)),
}
};
}
#[derive(Debug)]
pub enum Payload {
Metadata(Vec<(Option<UserMetadata>, XorName, u64)>),
}
pub type ChannelType = Result<(IpcMsg, Option<Payload>), (i32, Option<IpcMsg>)>;
pub fn init_log() {
let do_format = move |formatter: &mut Formatter, record: &Record| {
let now = formatter.timestamp();
writeln!(
formatter,
"{} {} [{}:{}] {}",
formatter.default_styled_level(record.level()),
now,
record.file().unwrap_or_default(),
record.line().unwrap_or_default(),
record.args()
)
};
let _ = LoggerBuilder::from_default_env()
.format(do_format)
.is_test(true)
.try_init();
}
pub async fn create_authenticator() -> (Authenticator, String, String) {
let locator: String = unwrap!(utils::generate_readable_string(10));
let password: String = unwrap!(utils::generate_readable_string(10));
let client_id = gen_client_id();
let coins = unwrap!(Coins::from_str("100"));
let _ = test_create_balance(&client_id, coins).await;
let auth = unwrap!(
Authenticator::create_client_with_acc(locator.clone(), password.clone(), client_id, || (),)
.await
);
(auth, locator, password)
}
pub const SEED_SUBPARTS: usize = 4;
pub fn divide_seed(seed: &str) -> Result<[&[u8]; SEED_SUBPARTS], AuthError> {
let seed = seed.as_bytes();
if seed.len() < SEED_SUBPARTS {
let e = format!(
"Improper Seed length of {}. Please supply bigger Seed.",
seed.len()
);
return Err(AuthError::Unexpected(e));
}
let interval = seed.len() / SEED_SUBPARTS;
let mut arr: [&[u8]; SEED_SUBPARTS] = Default::default();
for (i, val) in arr.iter_mut().enumerate() {
*val = &seed[interval * i..interval * (i + 1)];
}
Ok(arr)
}
pub async fn create_account_and_login() -> Authenticator {
let (_, locator, password) = create_authenticator().await;
trace!("Created an account with random login and password, logging in");
unwrap!(Authenticator::login(locator, password, || ()).await)
}
pub async fn try_revoke(authenticator: &Authenticator, app_id: &str) -> Result<(), AuthError> {
revocation::revoke_app(&authenticator.client, app_id).await
}
pub async fn revoke(authenticator: &Authenticator, app_id: &str) {
match try_revoke(authenticator, app_id).await {
Ok(_) => (),
x => panic!("Unexpected {:?}", x),
}
}
#[cfg(all(any(test, feature = "testing"), feature = "mock-network"))]
pub async fn create_account_and_login_with_hook<F>(hook: F) -> Authenticator
where
F: Fn(ConnectionManager) -> ConnectionManager + Send + Sync + 'static,
{
let (_, locator, password) = create_authenticator().await;
unwrap!(Authenticator::login_with_hook(locator, password, || (), hook,).await)
}
pub async fn get_app_or_err(
authenticator: &Authenticator,
app_id: &str,
) -> Result<config::AppInfo, AuthError> {
let app_id = app_id.to_string();
let client = authenticator.client.clone();
config::get_app(&client, &app_id).await
}
pub async fn register_app(
authenticator: &Authenticator,
auth_req: &AuthReq,
) -> Result<AuthGranted, AuthError> {
let req_id = ipc::gen_req_id();
let msg = IpcMsg::Req {
req_id,
request: IpcReq::Auth(auth_req.clone()),
};
let client = authenticator.client.clone();
let client_clone = client.clone();
let ipc_req = decode_ipc_msg(&client_clone, msg).await?;
match ipc_req {
Ok(IpcMsg::Req {
request: IpcReq::Auth(_),
..
}) => (),
x => return Err(AuthError::Unexpected(format!("Unexpected {:?}", x))),
};
let auth_req = auth_req.clone();
trace!("Authenticating app: {:?}", auth_req);
app_auth::authenticate(&client_clone.clone(), auth_req).await
}
#[allow(clippy::implicit_hasher)]
pub async fn register_rand_app(
authenticator: &Authenticator,
app_container: bool,
containers_req: HashMap<String, ContainerPermissions>,
) -> Result<(String, AuthGranted), AuthError> {
let auth_req = AuthReq {
app: rand_app(),
app_container,
app_permissions: AppPermissions {
transfer_coins: true,
perform_mutations: true,
get_balance: true,
},
containers: containers_req,
};
let auth_granted = register_app(authenticator, &auth_req).await?;
let app_id = auth_req.app.id;
Ok((app_id, auth_granted))
}
pub fn rand_app() -> AppExchangeInfo {
AppExchangeInfo {
id: unwrap!(utils::generate_readable_string(10)),
scope: None,
name: unwrap!(utils::generate_readable_string(10)),
vendor: unwrap!(utils::generate_readable_string(10)),
}
}
pub async fn create_file<S: Into<String>>(
authenticator: &Authenticator,
container_info: MDataInfo,
name: S,
content: Vec<u8>,
published: bool,
) -> Result<(), AuthError> {
let name = name.into();
let client = authenticator.client.clone();
let c2 = client.clone();
let writer = file_helper::write(
client.clone(),
File::new(vec![], published),
Mode::Overwrite,
container_info.enc_key().cloned(),
)
.await?;
writer.write(&content).await?;
let file = writer.close().await?;
file_helper::insert(c2, container_info, name, &file)
.await
.map_err(From::from)
}
pub async fn fetch_file<S: Into<String>>(
authenticator: &Authenticator,
container_info: MDataInfo,
name: S,
) -> Result<File, AuthError> {
let name = name.into();
let client = authenticator.client.clone();
let cloned_client = client.clone();
let (_, file) = file_helper::fetch(cloned_client, container_info, name)
.await
.map_err(AuthError::from)?;
Ok(file)
}
pub async fn read_file(
authenticator: &Authenticator,
file: File,
encryption_key: Option<shared_secretbox::Key>,
) -> Result<Vec<u8>, AuthError> {
let client = authenticator.client.clone();
let reader = file_helper::read(client.clone(), &file, encryption_key).await?;
reader
.read(0, reader.size().await)
.await
.map_err(AuthError::from)
}
pub async fn delete_file<S: Into<String>>(
authenticator: &Authenticator,
container_info: MDataInfo,
name: S,
published: bool,
version: u64,
) -> Result<u64, AuthError> {
let name = name.into();
let client = authenticator.client.clone();
file_helper::delete(
client,
container_info,
name,
published,
Version::Custom(version),
)
.await
.map_err(From::from)
}
pub async fn write_file(
authenticator: &Authenticator,
file: File,
mode: Mode,
encryption_key: Option<shared_secretbox::Key>,
content: Vec<u8>,
) -> Result<(), AuthError> {
let client = authenticator.client.clone();
let writer = file_helper::write(client, file, mode, encryption_key).await?;
match writer.write(&content).await {
Ok(_) => {
let _ = writer.close().await?;
Ok(())
}
Err(err) => Err(AuthError::from(err)),
}
}
pub async fn access_container<S: Into<String>>(
authenticator: &Authenticator,
app_id: S,
auth_granted: AuthGranted,
) -> Result<AccessContainerEntry, AuthError> {
match try_access_container(authenticator, app_id, auth_granted).await? {
Some(container) => Ok(container),
None => Err(AuthError::from(
"Access container entry is empty".to_string(),
)),
}
}
pub async fn try_access_container<S: Into<String>>(
authenticator: &Authenticator,
app_id: S,
auth_granted: AuthGranted,
) -> Result<Option<AccessContainerEntry>, AuthError> {
let app_keys = auth_granted.app_keys;
let app_id = app_id.into();
let client = authenticator.client.clone();
let (_, entry) = access_container::fetch_entry(client, app_id, app_keys).await?;
Ok(entry)
}
pub async fn get_container_from_authenticator_entry(
client: &AuthClient,
container: &str,
) -> Result<MDataInfo, AuthError> {
let result = access_container::fetch_authenticator_entry(client).await;
match result {
Ok((_, mut ac_entries)) => ac_entries.remove(container).ok_or_else(|| {
AuthError::from(format!("'{}' not found in the access container", container))
}),
Err(err) => Err(err),
}
}
#[allow(clippy::implicit_hasher)]
pub async fn compare_access_container_entries(
authenticator: &Authenticator,
app_pk: PublicKey,
mut access_container: AccessContainerEntry,
expected: HashMap<String, ContainerPermissions>,
) {
let client = &authenticator.client;
let mut reqs = Vec::new();
let user = app_pk;
for (container, expected_perms) in expected {
let expected_perm_set = container_perms_into_permission_set(&expected_perms);
let (md_info, perms) = unwrap!(
access_container.remove(&container),
"No '{}' in access container {:?}",
container,
access_container
);
assert_eq!(perms, expected_perms);
let perms = unwrap!(
client
.list_mdata_user_permissions(*md_info.address(), user)
.await
);
reqs.push((perms, expected_perm_set));
}
for (perms, expected) in reqs {
assert_eq!(perms, expected);
}
}
pub fn random_client<E, C>(
) -> Result<(Box<dyn Future<Output = ()> + 'static + Send>, AuthClient), AuthError>
where
E: Debug,
{
let n = |net_event| panic!("Unexpected NetworkEvent occurred: {:?}", net_event);
random_client_with_net_obs(n)
}
pub fn random_client_with_net_obs<NetObs>(
n: NetObs,
) -> Result<(Box<dyn Future<Output = ()> + 'static + Send>, AuthClient), AuthError>
where
NetObs: FnMut(NetworkEvent) + Send + 'static,
{
let client_creator = |net_tx| -> Result<AuthClient, AuthError> {
let acc_locator = utils::generate_random_string(10)?;
let acc_password = utils::generate_random_string(10)?;
let client_id = gen_client_id();
let _ =
futures::executor::block_on(test_create_balance(&client_id, Coins::from_str("10")?));
let auth_result: AuthClient = futures::executor::block_on(AuthClient::registered(
&acc_locator,
&acc_password,
client_id,
net_tx,
))?;
Ok(auth_result)
};
setup_client_with_net_obs(&(), client_creator, n).map_err(AuthError::from)
}
#[cfg(feature = "mock-network")]
pub async fn simulate_revocation_failure<T, S>(locator: &str, password: &str, app_ids: T)
where
T: IntoIterator<Item = S>,
S: AsRef<str>,
{
let auth = unwrap!(Authenticator::login(locator, password, || ()).await);
let client = auth.client;
let ac_info = client.access_container().await;
let auth = unwrap!(
Authenticator::login_with_hook(
locator,
password,
|| (),
move |mut cm| -> ConnectionManager {
let ac_info = ac_info.clone();
cm.set_request_hook(move |request| match *request {
Request::MData(MDataRequest::DelUserPermissions { address, .. }) => {
if *address.name() == ac_info.name() && address.tag() == ac_info.type_tag()
{
Some(Response::Mutation(Err(SndError::InsufficientBalance)))
} else {
None
}
}
_ => None,
});
cm
},
)
.await
);
for app_id in app_ids {
match try_revoke(&auth, app_id.as_ref()).await {
Err(_) => (),
x => panic!("Unexpected {:?}", x),
}
}
}