use super::utils::{corrupt_container, create_containers_req};
use crate::{
app_auth::{app_state, AppState},
assert_match,
client::AuthClient,
config::{self, get_app_revocation_queue, push_to_app_revocation_queue},
errors::AuthError,
revocation,
test_utils::{
access_container, create_account_and_login, create_authenticator, create_file, fetch_file,
get_container_from_authenticator_entry, rand_app, register_app, register_rand_app, revoke,
try_access_container, try_revoke,
},
{access_container, Authenticator},
};
use log::debug;
use safe_core::{
app_container_name, btree_set,
client::AuthActions,
core_structs::AccessContainerEntry,
ipc::req::container_perms_into_permission_set,
ipc::{AuthReq, Permission},
Client, CoreError, MDataInfo,
};
use safe_nd::{AppPermissions, Error as SndError, MDataAddress, MDataSeqEntryActions};
use std::collections::HashMap;
use tiny_keccak::sha3_256;
use unwrap::unwrap;
async fn verify_app_is_revoked(
client: &AuthClient,
app_id: String,
prev_ac_entry: AccessContainerEntry,
) -> Result<(), AuthError> {
let (_, apps) = config::list_apps(client).await?;
let (auth_keys, _) = client.list_auth_keys_and_version().await?;
let state = app_state(&client, &apps, &app_id).await?;
let app_hash = sha3_256(app_id.as_bytes());
let app_key = unwrap!(apps.get(&app_hash)).keys.public_key();
if auth_keys.contains_key(&app_key) {
return Err(AuthError::Unexpected("App is still authenticated".into()));
}
assert_match!(state, AppState::Revoked);
for (_, (mdata_info, _)) in prev_ac_entry.into_iter() {
let res = client
.list_mdata_user_permissions(*mdata_info.address(), app_key)
.await;
assert_match!(res, Err(CoreError::DataError(SndError::NoSuchKey)));
}
Ok(())
}
async fn verify_app_is_authenticated(
client: &AuthClient,
app_id: String,
expected_permissions: AppPermissions,
) -> Result<(), AuthError> {
let (_, mut apps) = config::list_apps(client).await?;
let app_hash = sha3_256(app_id.as_bytes());
let app_keys = unwrap!(apps.remove(&app_hash)).keys;
let (auth_keys, _) = client.list_auth_keys_and_version().await?;
let app_key = app_keys.public_key();
match auth_keys.get(&app_key) {
Some(app_permissions) => assert_eq!(*app_permissions, expected_permissions),
None => panic!("App is not authenticated"),
}
let (_, entry) = access_container::fetch_entry(client.clone(), app_id, app_keys).await?;
let user = app_key;
let ac_entry = unwrap!(entry);
for (_, (mdata_info, permissions)) in ac_entry.into_iter() {
let expected_perms = container_perms_into_permission_set(&permissions);
let perms = client
.list_mdata_user_permissions(*mdata_info.address(), user)
.await?;
assert_eq!(perms, expected_perms);
let entries = client
.list_seq_mdata_entries(mdata_info.name(), mdata_info.type_tag())
.await?;
for (key, value) in entries {
if value.data.is_empty() {
continue;
}
let _ = unwrap!(mdata_info.decrypt(&key));
let _ = unwrap!(mdata_info.decrypt(&value.data));
}
}
Ok(())
}
#[cfg(feature = "mock-network")]
mod mock_routing {
use super::*;
use crate::test_utils::{
get_container_from_authenticator_entry, register_rand_app, simulate_revocation_failure,
try_revoke,
};
use rand::rngs::StdRng;
use rand::SeedableRng;
use safe_core::client::AuthActions;
use safe_core::ipc::{IpcError, Permission};
use safe_core::utils::test_utils::Synchronizer;
use std::{
collections::HashMap,
iter,
sync::{Arc, Barrier},
};
use tokio::task::LocalSet;
#[tokio::test]
async fn app_revocation() -> Result<(), AuthError> {
let (auth, locator, password) = create_authenticator().await;
let client = auth.client.clone();
let auth_req = AuthReq {
app: rand_app(),
app_container: false,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let app_id = auth_req.app.id.clone();
let auth_granted = register_app(&auth, &auth_req).await?;
let mut ac_entries = access_container(&auth, app_id.clone(), auth_granted.clone()).await?;
let (videos_md, _) = unwrap!(ac_entries.remove("_videos"));
let (docs_md, _) = unwrap!(ac_entries.remove("_documents"));
create_file(&auth, videos_md.clone(), "video.mp4", vec![1; 10], true).await?;
create_file(&auth, docs_md.clone(), "test.doc", vec![2; 10], true).await?;
let auth = Authenticator::login(locator.clone(), password.clone(), || ()).await?;
try_revoke(&auth, &app_id).await?;
let _ = fetch_file(&auth, docs_md, "test.doc").await?;
let new_videos_md = get_container_from_authenticator_entry(&client, "_videos").await?;
let _ = fetch_file(&auth, new_videos_md, "video.mp4").await?;
let _ = fetch_file(&auth, videos_md, "video.mp4").await?;
let (auth_keys, _version) = client.list_auth_keys_and_version().await?;
assert!(!auth_keys.contains_key(&auth_granted.app_keys.public_key()));
let auth = Authenticator::login(locator, password, || ()).await?;
revoke(&auth, &app_id).await;
let app_id = app_id;
verify_app_is_revoked(&client, app_id, ac_entries).await?;
Ok(())
}
#[tokio::test]
async fn app_authentication_during_pending_revocation() -> Result<(), AuthError> {
let (auth, locator, password) = create_authenticator().await;
let auth_req = AuthReq {
app: rand_app(),
app_container: false,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let app_id = auth_req.app.id.clone();
let _ = register_app(&auth, &auth_req).await?;
simulate_revocation_failure(&locator, &password, iter::once(&app_id)).await;
match register_app(&auth, &auth_req).await {
Err(AuthError::PendingRevocation) => (),
x => panic!("Unexpected: {:?}", x),
}
revoke(&auth, &app_id).await;
let _ = register_app(&auth, &auth_req).await?;
Ok(())
}
#[tokio::test]
#[allow(unused_variables, unused_assignments)]
async fn concurrent_revocation_of_single_app() -> Result<(), AuthError> {
let mut rng = StdRng::from_entropy();
let concurrency = 2;
let (auth, locator, password) = create_authenticator().await;
let client = auth.client.clone();
let mut containers_req = HashMap::new();
let _ = containers_req.insert(
"_documents".to_owned(),
btree_set![
Permission::Read,
Permission::Insert,
Permission::Update,
Permission::Delete,
],
);
let (app_id_0, auth_granted_0) =
register_rand_app(&auth, true, containers_req.clone()).await?;
let (app_id_1, _) = register_rand_app(&auth, true, containers_req).await?;
let ac_entries_0 = access_container(&auth, app_id_0.clone(), auth_granted_0).await?;
let info = get_container_from_authenticator_entry(&client, "_documents").await?;
create_file(&auth, info, "shared.txt", vec![0; 10], true).await?;
for app_id in &[&app_id_0, &app_id_1] {
let info = get_container_from_authenticator_entry(&client, &app_container_name(app_id))
.await?;
create_file(&auth, info, "private.txt", vec![0; 10], true).await?;
}
let barrier = Arc::new(Barrier::new(concurrency));
let sync = Synchronizer::new(&mut rng);
let mut success = false;
let local = LocalSet::new();
for _ in 0..concurrency {
let locator = locator.clone();
let password = password.clone();
let app_id = app_id_0.clone();
let barrier = Arc::clone(&barrier);
let sync = sync.clone();
let _ = local.spawn_local(async move {
let auth = Authenticator::login_with_hook(
locator,
password,
|| (),
move |routing| sync.hook(routing),
)
.await?;
let _ = barrier;
match try_revoke(&auth, &app_id).await {
Ok(_) | Err(AuthError::IpcError(IpcError::UnknownApp)) => success = true,
_ => {}
}
Ok::<_, AuthError>(())
});
}
local.await;
if !success {
try_revoke(&auth, &app_id_0).await?;
}
verify_app_is_revoked(&client, app_id_0, ac_entries_0).await?;
let expected_permissions = AppPermissions {
get_balance: true,
transfer_coins: true,
perform_mutations: true,
};
verify_app_is_authenticated(&client, app_id_1, expected_permissions).await?;
Ok(())
}
#[tokio::test]
async fn concurrent_revocation_of_multiple_apps() -> Result<(), AuthError> {
let mut rng = StdRng::from_entropy();
let (auth, locator, password) = create_authenticator().await;
let client = auth.client.clone();
let mut containers_req = HashMap::new();
let _ = containers_req.insert(
"_documents".to_owned(),
btree_set![
Permission::Read,
Permission::Insert,
Permission::Update,
Permission::Delete,
],
);
let (app_id_0, auth_granted_0) =
register_rand_app(&auth, true, containers_req.clone()).await?;
let (app_id_1, auth_granted_1) =
register_rand_app(&auth, true, containers_req.clone()).await?;
let (app_id_2, _) = register_rand_app(&auth, true, containers_req).await?;
let ac_entries_0 = access_container(&auth, app_id_0.clone(), auth_granted_0).await?;
let ac_entries_1 = access_container(&auth, app_id_1.clone(), auth_granted_1).await?;
let info = get_container_from_authenticator_entry(&client, "_documents").await?;
create_file(&auth, info, "shared.txt", vec![0; 10], true).await?;
for app_id in &[&app_id_0, &app_id_1, &app_id_2] {
let info = get_container_from_authenticator_entry(&client, &app_container_name(app_id))
.await?;
create_file(&auth, info, "private.txt", vec![0; 10], true).await?;
}
let apps_to_revoke = [app_id_0.clone(), app_id_1.clone()];
let barrier = Arc::new(Barrier::new(apps_to_revoke.len()));
let sync = Synchronizer::new(&mut rng);
let local = LocalSet::new();
for app_id in apps_to_revoke.iter() {
let locator = locator.clone();
let password = password.clone();
let app_id = app_id.to_string();
let barrier = Arc::clone(&barrier);
let sync = sync.clone();
let _ = local.spawn_local(async move {
let auth = Authenticator::login_with_hook(
locator,
password,
|| (),
move |routing| sync.hook(routing),
)
.await?;
let _ = barrier;
if try_revoke(&auth, &app_id).await.is_err() {
match try_revoke(&auth, &app_id).await {
Ok(_) | Err(AuthError::IpcError(IpcError::UnknownApp)) => (),
Err(error) => panic!("Unexpected revocation failure: {:?}", error),
}
}
Ok::<_, AuthError>(())
});
}
local.await;
verify_app_is_revoked(&client, app_id_0, ac_entries_0).await?;
verify_app_is_revoked(&client, app_id_1, ac_entries_1).await?;
let expected_permissions = AppPermissions {
get_balance: true,
transfer_coins: true,
perform_mutations: true,
};
verify_app_is_authenticated(&client, app_id_2, expected_permissions).await?;
Ok(())
}
}
#[tokio::test]
async fn app_revocation_and_reauth() -> Result<(), AuthError> {
let authenticator = create_account_and_login().await;
let client = authenticator.client.clone();
let auth_req1 = AuthReq {
app: rand_app(),
app_container: false,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let app_id1 = auth_req1.app.id.clone();
let auth_granted1 = register_app(&authenticator, &auth_req1).await?;
let mut auth_req2 = AuthReq {
app: rand_app(),
app_container: true,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let app_id2 = auth_req2.app.id.clone();
let auth_granted2 = register_app(&authenticator, &auth_req2).await?;
let mut ac_entries =
access_container(&authenticator, app_id1.clone(), auth_granted1.clone()).await?;
let (videos_md1, _) = unwrap!(ac_entries.remove("_videos"));
create_file(
&authenticator,
videos_md1.clone(),
"1.mp4",
vec![1; 10],
true,
)
.await?;
let mut ac_entries =
access_container(&authenticator, app_id2.clone(), auth_granted2.clone()).await?;
let (videos_md2, _) = unwrap!(ac_entries.remove("_videos"));
create_file(
&authenticator,
videos_md2.clone(),
"2.mp4",
vec![1; 10],
true,
)
.await?;
let app_container_name = app_container_name(&app_id2);
let (app_container_md, _) = unwrap!(ac_entries.remove(&app_container_name));
create_file(&authenticator, app_container_md, "3.mp4", vec![1; 10], true).await?;
assert_eq!(
count_mdata_entries(&authenticator, videos_md1.clone()).await?,
2
);
let _ = fetch_file(&authenticator, videos_md1.clone(), "1.mp4").await?;
let _ = fetch_file(&authenticator, videos_md1.clone(), "2.mp4").await?;
let _ = fetch_file(&authenticator, videos_md2.clone(), "1.mp4").await?;
let _ = fetch_file(&authenticator, videos_md2.clone(), "2.mp4").await?;
revoke(&authenticator, &app_id1).await;
assert_eq!(count_mdata_entries(&authenticator, videos_md1).await?, 2);
let ac = try_access_container(&authenticator, app_id1.clone(), auth_granted1.clone()).await?;
assert!(ac.is_none());
let (name, tag) = (videos_md2.name(), videos_md2.type_tag());
let perms = client
.list_mdata_permissions(MDataAddress::Seq { name, tag })
.await?;
assert!(!perms.contains_key(&auth_granted1.app_keys.public_key()));
assert!(perms.contains_key(&auth_granted2.app_keys.public_key()));
let (app_id1_clone, app_id2_clone) = (app_id1.clone(), app_id2.clone());
verify_app_is_revoked(&client, app_id1_clone, ac_entries).await?;
verify_app_is_authenticated(&client, app_id2_clone, Default::default()).await?;
let mut ac_entries =
access_container(&authenticator, app_id2.clone(), auth_granted2.clone()).await?;
let (videos_md2, _) = unwrap!(ac_entries.remove("_videos"));
let _ = fetch_file(&authenticator, videos_md2.clone(), "1.mp4").await?;
let _ = fetch_file(&authenticator, videos_md2.clone(), "2.mp4").await?;
let auth_granted1 = register_app(&authenticator, &auth_req1).await?;
let mut ac_entries = access_container(&authenticator, app_id1.clone(), auth_granted1).await?;
let (videos_md1, _) = unwrap!(ac_entries.remove("_videos"));
let _ = fetch_file(&authenticator, videos_md1.clone(), "1.mp4").await?;
let _ = fetch_file(&authenticator, videos_md1.clone(), "2.mp4").await?;
let _ = fetch_file(&authenticator, videos_md2.clone(), "1.mp4").await?;
let _ = fetch_file(&authenticator, videos_md2, "2.mp4").await?;
revoke(&authenticator, &app_id1).await;
assert_eq!(count_mdata_entries(&authenticator, videos_md1).await?, 2);
let (app_id1_clone, app_id2_clone) = (app_id1.clone(), app_id2.clone());
verify_app_is_revoked(&client, app_id1_clone, ac_entries).await?;
verify_app_is_authenticated(&client, app_id2_clone, Default::default()).await?;
let mut ac_entries = access_container(&authenticator, app_id2.clone(), auth_granted2).await?;
let (videos_md2, _) = unwrap!(ac_entries.remove("_videos"));
let _ = fetch_file(&authenticator, videos_md2.clone(), "1.mp4").await?;
let _ = fetch_file(&authenticator, videos_md2, "2.mp4").await?;
revoke(&authenticator, &app_id2).await;
let (app_id1_clone, app_id2_clone) = (app_id1, app_id2.clone());
verify_app_is_revoked(&client, app_id1_clone, ac_entries.clone()).await?;
verify_app_is_revoked(&client, app_id2_clone, ac_entries).await?;
let new_app_permissions = AppPermissions {
get_balance: true,
perform_mutations: false,
transfer_coins: false,
};
auth_req2.app_permissions = new_app_permissions;
let auth_granted2 = register_app(&authenticator, &auth_req2).await?;
let mut ac_entries = access_container(&authenticator, app_id2.clone(), auth_granted2).await?;
let (app_container_md, _) = unwrap!(ac_entries.remove(&app_container_name));
assert_eq!(
count_mdata_entries(&authenticator, app_container_md.clone()).await?,
1
);
let _ = fetch_file(&authenticator, app_container_md, "3.mp4").await?;
let app_id2_clone = app_id2.clone();
verify_app_is_authenticated(&client, app_id2_clone, new_app_permissions).await?;
revoke(&authenticator, &app_id2).await;
verify_app_is_revoked(&client, app_id2, ac_entries).await?;
Ok(())
}
#[tokio::test]
#[ignore]
async fn revocation_symmetric_decipher_failure() -> Result<(), AuthError> {
let authenticator = create_account_and_login().await;
let client = authenticator.client.clone();
let mut corrupt_containers = HashMap::new();
let _ = corrupt_containers.insert(
"_downloads".to_owned(),
btree_set![Permission::Read, Permission::Insert],
);
let auth_req1 = AuthReq {
app: rand_app(),
app_container: false,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let app_id1 = auth_req1.app.id.clone();
debug!("Registering app 1 with ID {}...", app_id1);
let auth_granted1 = register_app(&authenticator, &auth_req1).await?;
let auth_req2 = AuthReq {
app: rand_app(),
app_container: true,
app_permissions: Default::default(),
containers: corrupt_containers,
};
let app_id2 = auth_req2.app.id.clone();
debug!("Registering app 2 with ID {}...", app_id2);
let auth_granted2 = register_app(&authenticator, &auth_req2).await?;
let auth_req3 = AuthReq {
app: rand_app(),
app_container: false,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let app_id3 = auth_req3.app.id.clone();
debug!("Registering app 3 with ID {}...", app_id3);
let _auth_granted3 = register_app(&authenticator, &auth_req3).await?;
let mut ac_entries = access_container(&authenticator, app_id2.clone(), auth_granted2).await?;
let (downloads_md, _) = unwrap!(ac_entries.remove("_downloads"));
create_file(&authenticator, downloads_md, "video.mp4", vec![1; 10], true).await?;
{
let app_id1 = app_id1.clone();
let app_id2 = app_id2.clone();
let app_id2_clone = app_id2;
let c2 = client.clone();
let c3 = client.clone();
let c4 = client.clone();
let c5 = client.clone();
let (version, queue) = get_app_revocation_queue(&client).await?;
let _ = push_to_app_revocation_queue(&c2, queue, config::next_version(version), &app_id1)
.await?;
let (version, queue) = get_app_revocation_queue(&c3).await?;
let _ =
push_to_app_revocation_queue(&c4, queue, config::next_version(version), &app_id2_clone)
.await?;
corrupt_container(&c5, "_downloads").await?;
}
match try_revoke(&authenticator, &app_id3).await {
Ok(()) => (),
Err(AuthError::CoreError(CoreError::SymmetricDecipherFailure)) => (),
Err(x) => panic!("An unexpected error occurred: {:?}", x),
}
let (_, queue) = unwrap!(get_app_revocation_queue(&client).await);
let ac = unwrap!(try_access_container(&authenticator, app_id1.clone(), auth_granted1).await);
assert!(ac.is_none());
assert!(!queue.contains(&app_id1));
assert!(!queue.contains(&app_id2));
assert!(!queue.contains(&app_id3));
Ok(())
}
#[tokio::test]
async fn flushing_empty_app_revocation_queue_does_not_mutate_network() -> Result<(), AuthError> {
let (auth, ..) = create_authenticator().await;
let client = auth.client.clone();
let balance_0 = unwrap!(client.get_balance(None).await);
revocation::flush_app_revocation_queue(&client).await?;
let balance_1 = client.get_balance(None).await?;
assert_eq!(balance_0, balance_1);
let auth_req = AuthReq {
app: rand_app(),
app_container: false,
app_permissions: Default::default(),
containers: create_containers_req(),
};
let _ = unwrap!(register_app(&auth, &auth_req).await);
let app_id = auth_req.app.id;
revoke(&auth, &app_id).await;
let balance_2 = client.get_balance(None).await?;
revocation::flush_app_revocation_queue(&client).await?;
let balance_3 = client.get_balance(None).await?;
assert_eq!(balance_2, balance_3);
Ok(())
}
#[tokio::test]
async fn revocation_with_unencrypted_container_entries() -> Result<(), AuthError> {
let (auth, ..) = create_authenticator().await;
let client = auth.client.clone();
let mut containers_req = HashMap::new();
let _ = containers_req.insert(
"_documents".to_owned(),
btree_set![Permission::Read, Permission::Insert,],
);
let (app_id, _) = unwrap!(register_rand_app(&auth, true, containers_req).await);
let shared_info = unwrap!(get_container_from_authenticator_entry(&client, "_documents").await);
let shared_info2 = shared_info.clone();
let shared_key = b"shared-key".to_vec();
let shared_content = b"shared-value".to_vec();
let shared_actions =
MDataSeqEntryActions::new().ins(shared_key.clone(), shared_content.clone(), 0);
let dedicated_info = unwrap!(
get_container_from_authenticator_entry(&client, &app_container_name(&app_id),).await
);
let dedicated_info2 = dedicated_info.clone();
let dedicated_key = b"dedicated-key".to_vec();
let dedicated_content = b"dedicated-value".to_vec();
let dedicated_actions =
MDataSeqEntryActions::new().ins(dedicated_key.clone(), dedicated_content.clone(), 0);
client
.mutate_seq_mdata_entries(shared_info.name(), shared_info.type_tag(), shared_actions)
.await?;
client
.mutate_seq_mdata_entries(
dedicated_info.name(),
dedicated_info.type_tag(),
dedicated_actions,
)
.await?;
revoke(&auth, &app_id).await;
let shared_value = client
.get_seq_mdata_value(shared_info2.name(), shared_info2.type_tag(), shared_key)
.await?;
let dedicated_value = client
.get_seq_mdata_value(
dedicated_info2.name(),
dedicated_info2.type_tag(),
dedicated_key,
)
.await?;
assert_eq!(shared_value.data, shared_content);
assert_eq!(dedicated_value.data, dedicated_content);
Ok(())
}
async fn count_mdata_entries(
authenticator: &Authenticator,
info: MDataInfo,
) -> Result<usize, AuthError> {
let entries = authenticator
.client
.list_seq_mdata_entries(info.name(), info.type_tag())
.await?;
Ok(entries.len())
}