blob: 206ec19c955c1d6d4aad97a6736451ece1165e6e [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{anyhow, format_err, Error};
use fidl::endpoints::{create_endpoints, create_request_stream};
use fidl_fuchsia_auth::{
AuthenticationContextProviderMarker, AuthenticationContextProviderRequest,
};
use fidl_fuchsia_identity_account::{
AccountManagerMarker, AccountManagerProxy, AccountProxy, Error as ApiError, Lifetime, Scenario,
ThreatScenario,
};
use fidl_fuchsia_stash::StoreMarker;
use fuchsia_async::{self as fasync, DurationExt, TimeoutExt};
use fuchsia_component::client::{launch, App};
use fuchsia_component::fuchsia_single_component_package_url;
use fuchsia_component::server::{NestedEnvironment, ServiceFs};
use fuchsia_zircon as zx;
use futures::future::join;
use futures::prelude::*;
use lazy_static::lazy_static;
use std::ops::Deref;
/// Type alias for the LocalAccountId FIDL type
type LocalAccountId = u64;
const ALWAYS_SUCCEED_AUTH_MECHANISM_ID: &str =
"fuchsia-pkg://fuchsia.com/dev_authenticator#meta/dev_authenticator_always_succeed.cmx";
const ALWAYS_FAIL_AUTHENTICATION_AUTH_MECHANISM_ID: &str = concat!(
"fuchsia-pkg://fuchsia.com/dev_authenticator",
"#meta/dev_authenticator_always_fail_authentication.cmx"
);
lazy_static! {
/// URL for account manager.
static ref ACCOUNT_MANAGER_URL: String =
String::from(fuchsia_single_component_package_url!("account_manager"));
/// Arguments passed to account manager started in test environment.
static ref ACCOUNT_MANAGER_ARGS: Vec<String> = vec![
"--dev-auth-providers".to_string(),
"--dev-auth-mechanisms".to_string(),
];
static ref TEST_SCENARIO: Scenario = Scenario {
include_test: false,
threat_scenario: ThreatScenario::BasicAttacker,
};
/// Maximum time between a lock request and when the account is locked
static ref LOCK_REQUEST_DURATION: zx::Duration = zx::Duration::from_seconds(5);
}
/// Calls provision_new_account on the supplied account_manager, returning an error on any
/// non-OK responses, or the account ID on success.
async fn provision_new_account(
account_manager: &AccountManagerProxy,
lifetime: Lifetime,
auth_mechanism_id: Option<&str>,
) -> Result<LocalAccountId, Error> {
account_manager
.provision_new_account(lifetime, auth_mechanism_id)
.await?
.map_err(|error| format_err!("ProvisionNewAccount returned error: {:?}", error))
}
/// Calls provision_from_auth_provider on the supplied account_manager using "dev_auth_provider",
/// mocks out the UI context, returning an error on any non-OK responses, or the account ID on
/// success. This requires the account manager to run the dev_auth_provider. See the
/// `account_manager` package for more information on how that is configured.
async fn provision_account_from_dev_auth_provider(
account_manager: &AccountManagerProxy,
) -> Result<LocalAccountId, Error> {
let (acp_client_end, mut acp_request_stream) =
create_request_stream::<AuthenticationContextProviderMarker>()
.expect("failed opening channel");
// This async function mocks out the auth context provider channel, although it supplies no data
// for the test.
let serve_fn = async move {
let request = acp_request_stream
.try_next()
.await
.expect("AuthenticationContextProvider failed receiving message");
match request {
Some(AuthenticationContextProviderRequest::GetAuthenticationUiContext { .. }) => Ok(()),
None => Err(format_err!("AuthenticationContextProvider channel closed unexpectedly")),
}
};
let (serve_result, provision_result) = join(
serve_fn,
account_manager.provision_from_auth_provider(
acp_client_end,
"dev_auth_provider",
Lifetime::Persistent,
None,
),
)
.await;
serve_result?;
provision_result?
.map_err(|error| format_err!("ProvisionFromAuthProvider returned error: {:?}", error))
}
/// A proxy to an account manager running in a nested environment.
struct NestedAccountManagerProxy {
/// Proxy to account manager.
account_manager_proxy: AccountManagerProxy,
/// Application object for account manager. Needs to be kept in scope to
/// keep the nested environment alive.
_app: App,
/// The nested environment account manager is running in. Needs to be kept
/// in scope to keep the nested environment alive.
_nested_envronment: NestedEnvironment,
}
impl Deref for NestedAccountManagerProxy {
type Target = AccountManagerProxy;
fn deref(&self) -> &AccountManagerProxy {
&self.account_manager_proxy
}
}
/// Start account manager in an isolated environment and return a proxy to it. An optional
/// environment label can be provided in order to test state preservation across component
/// termination and restart. If env is None a randomized environment label will be picked
/// to provide isolation across tests.
/// NOTE: Do not reuse environment labels across tests. A NestedAccountManagerProxy should be
/// destroyed before a new one referencing the same enviroment is created.
fn create_account_manager(env: Option<String>) -> Result<NestedAccountManagerProxy, Error> {
let mut service_fs = ServiceFs::new();
service_fs.add_proxy_service::<StoreMarker, _>();
let nested_environment = match env {
None => service_fs.create_salted_nested_environment("account_test_env")?,
Some(label) => service_fs.create_nested_environment(&label)?,
};
let app = launch(
nested_environment.launcher(),
ACCOUNT_MANAGER_URL.clone(),
Some(ACCOUNT_MANAGER_ARGS.clone()),
)?;
fasync::Task::spawn(service_fs.collect()).detach();
let account_manager_proxy = app.connect_to_service::<AccountManagerMarker>()?;
Ok(NestedAccountManagerProxy {
account_manager_proxy,
_app: app,
_nested_envronment: nested_environment,
})
}
/// Locks an account and waits for the channel to close.
async fn lock_and_check(account: &AccountProxy) -> Result<(), Error> {
account.lock().await?.map_err(|err| anyhow!("Lock failed: {:?}", err))?;
account
.take_event_stream()
.for_each(|_| async move {}) // Drain
.map(|_| Ok(())) // Completed drain results in ok
.on_timeout(LOCK_REQUEST_DURATION.after_now(), || Err(anyhow!("Locking timeout exceeded")))
.await
}
// TODO(jsankey): Work with ComponentFramework and cramertj@ to develop a nice Rust equivalent of
// the C++ TestWithEnvironment fixture to provide isolated environments for each test case. We
// are currently creating a new environment for account manager to run in, but the tests
// themselves run in a single environment.
#[fuchsia_async::run_singlethreaded(test)]
async fn test_provision_new_account() -> Result<(), Error> {
let account_manager = create_account_manager(None)?;
// Verify we initially have no accounts.
assert_eq!(account_manager.get_account_ids().await?, vec![]);
// Provision a new account.
let account_1 = provision_new_account(&account_manager, Lifetime::Persistent, None).await?;
assert_eq!(account_manager.get_account_ids().await?, vec![account_1]);
// Provision a second new account and verify it has a different ID.
let account_2 = provision_new_account(&account_manager, Lifetime::Persistent, None).await?;
assert_ne!(account_1, account_2);
// Provision account with an auth mechanism
let account_3 = account_manager
.provision_new_account(Lifetime::Persistent, Some(ALWAYS_SUCCEED_AUTH_MECHANISM_ID))
.await?
.unwrap();
let account_ids = account_manager.get_account_ids().await?;
assert_eq!(account_ids.len(), 3);
assert!(account_ids.contains(&account_1));
assert!(account_ids.contains(&account_2));
assert!(account_ids.contains(&account_3));
// Auth state is not yet supported
assert_eq!(
account_manager.get_account_auth_states(&mut TEST_SCENARIO.clone()).await?,
Err(ApiError::UnsupportedOperation)
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_provision_then_lock_then_unlock_account() -> Result<(), Error> {
let account_manager = create_account_manager(None)?;
// Provision account with an auth mechanism that passes authentication
let account_id = provision_new_account(
&account_manager,
Lifetime::Persistent,
Some(ALWAYS_SUCCEED_AUTH_MECHANISM_ID),
)
.await?;
let (acp_client_end, _) = create_endpoints()?;
let (account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_id, acp_client_end, account_server_end).await?,
Ok(())
);
let account_proxy = account_client_end.into_proxy()?;
// Lock the account and ensure that it's locked
lock_and_check(&account_proxy).await?;
// Unlock the account and re-acquire a channel
let (acp_client_end, _) = create_endpoints()?;
let (account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_id, acp_client_end, account_server_end).await?,
Ok(())
);
let account_proxy = account_client_end.into_proxy()?;
assert!(account_proxy.get_account_name().await.is_ok());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_unlock_account() -> Result<(), Error> {
let account_manager =
create_account_manager(Some("test_successful_authentication".to_string()))?;
// Provision account with an auth mechanism that passes authentication
let account_id = provision_new_account(
&account_manager,
Lifetime::Persistent,
Some(ALWAYS_SUCCEED_AUTH_MECHANISM_ID),
)
.await?;
// Restart the account manager, now the account should be locked
std::mem::drop(account_manager);
let account_manager =
create_account_manager(Some("test_successful_authentication".to_string()))?;
// Unlock the account and acquire a channel to it
let (acp_client_end, _) = create_endpoints()?;
let (account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_id, acp_client_end, account_server_end).await?,
Ok(())
);
let account_proxy = account_client_end.into_proxy()?;
assert!(account_proxy.get_account_name().await.is_ok());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_provision_then_lock_then_unlock_fail_authentication() -> Result<(), Error> {
let account_manager = create_account_manager(None)?;
// Provision account with an auth mechanism that fails authentication
let account_id = provision_new_account(
&account_manager,
Lifetime::Persistent,
Some(ALWAYS_FAIL_AUTHENTICATION_AUTH_MECHANISM_ID),
)
.await?;
let (acp_client_end, _) = create_endpoints()?;
let (account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_id, acp_client_end, account_server_end).await?,
Ok(())
);
let account_proxy = account_client_end.into_proxy()?;
// Lock the account and ensure that it's locked
lock_and_check(&account_proxy).await?;
// Attempting to unlock the account fails with an authentication error
let (acp_client_end, _) = create_endpoints()?;
let (_, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_id, acp_client_end, account_server_end).await?,
Err(ApiError::FailedAuthentication)
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_unlock_account_fail_authentication() -> Result<(), Error> {
let account_manager = create_account_manager(Some("test_fail_authentication".to_string()))?;
// Provision account with an auth mechanism that fails authentication
let account_id = provision_new_account(
&account_manager,
Lifetime::Persistent,
Some(ALWAYS_FAIL_AUTHENTICATION_AUTH_MECHANISM_ID),
)
.await?;
// Restart the account manager, now the account should be locked
std::mem::drop(account_manager);
let account_manager = create_account_manager(Some("test_fail_authentication".to_string()))?;
// Attempting to unlock the account fails with an authentication error
let (acp_client_end, _) = create_endpoints()?;
let (_, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_id, acp_client_end, account_server_end).await?,
Err(ApiError::FailedAuthentication)
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_provision_new_account_from_auth_provider() -> Result<(), Error> {
let account_manager = create_account_manager(None)?;
// Verify we initially have no accounts.
assert_eq!(account_manager.get_account_ids().await?, vec![]);
// Provision a new account.
let account_1 = provision_account_from_dev_auth_provider(&account_manager).await?;
assert_eq!(account_manager.get_account_ids().await?, vec![account_1]);
// Provision a second new account and verify it has a different ID.
let account_2 = provision_account_from_dev_auth_provider(&account_manager).await?;
assert_ne!(account_1, account_2);
let account_ids = account_manager.get_account_ids().await?;
assert_eq!(account_ids.len(), 2);
assert!(account_ids.contains(&account_1));
assert!(account_ids.contains(&account_2));
// Auth state is not yet supported
assert_eq!(
account_manager.get_account_auth_states(&mut TEST_SCENARIO.clone()).await?,
Err(ApiError::UnsupportedOperation)
);
Ok(())
}
// This represents two nearly identical tests, one with ephemeral and one with persistent accounts
async fn get_account_and_persona_helper(lifetime: Lifetime) -> Result<(), Error> {
let account_manager = create_account_manager(None)?;
assert_eq!(account_manager.get_account_ids().await?, vec![]);
let account = provision_new_account(&account_manager, lifetime, None).await?;
// Connect a channel to the newly created account and verify it's usable.
let (acp_client_end, _) = create_endpoints()?;
let (account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account, acp_client_end, account_server_end).await?,
Ok(())
);
let account_proxy = account_client_end.into_proxy()?;
let account_auth_state = account_proxy.get_auth_state(&mut TEST_SCENARIO.clone()).await?;
assert_eq!(account_auth_state, Err(ApiError::UnsupportedOperation));
// Cannot lock account not protected by an auth mechanism
assert_eq!(account_proxy.lock().await?, Err(ApiError::FailedPrecondition));
assert_eq!(account_proxy.get_lifetime().await?, lifetime);
// Connect a channel to the account's default persona and verify it's usable.
let (persona_client_end, persona_server_end) = create_endpoints()?;
assert!(account_proxy.get_default_persona(persona_server_end).await?.is_ok());
let persona_proxy = persona_client_end.into_proxy()?;
let persona_auth_state = persona_proxy.get_auth_state(&mut TEST_SCENARIO.clone()).await?;
assert_eq!(persona_auth_state, Err(ApiError::UnsupportedOperation));
assert_eq!(persona_proxy.get_lifetime().await?, lifetime);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_persistent_account_and_persona() -> Result<(), Error> {
get_account_and_persona_helper(Lifetime::Persistent).await
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_ephemeral_account_and_persona() -> Result<(), Error> {
get_account_and_persona_helper(Lifetime::Ephemeral).await
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_account_deletion() -> Result<(), Error> {
let account_manager = create_account_manager(None)?;
assert_eq!(account_manager.get_account_ids().await?, vec![]);
let account_1 = provision_new_account(&account_manager, Lifetime::Persistent, None).await?;
let account_2 = provision_new_account(&account_manager, Lifetime::Persistent, None).await?;
let existing_accounts = account_manager.get_account_ids().await?;
assert!(existing_accounts.contains(&account_1));
assert!(existing_accounts.contains(&account_2));
assert_eq!(existing_accounts.len(), 2);
// Delete an account and verify it is removed.
assert_eq!(account_manager.remove_account(account_1, true).await?, Ok(()));
assert_eq!(account_manager.get_account_ids().await?, vec![account_2]);
// Connecting to the deleted account should fail.
let (acp_client_end, _acp_server_end) = create_endpoints()?;
let (_account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_1, acp_client_end, account_server_end).await?,
Err(ApiError::NotFound)
);
Ok(())
}
/// Ensure that an account manager created in a specific environment picks up the state of
/// previous instances that ran in that environment. Also check that some basic operations work on
/// accounts created in that previous lifetime.
#[fuchsia_async::run_singlethreaded(test)]
async fn test_lifecycle() -> Result<(), Error> {
let account_manager = create_account_manager(Some("test_account_deletion".to_string()))?;
assert_eq!(account_manager.get_account_ids().await?, vec![]);
let account_1 = provision_new_account(&account_manager, Lifetime::Persistent, None).await?;
let account_2 = provision_new_account(&account_manager, Lifetime::Persistent, None).await?;
let account_3 = provision_new_account(&account_manager, Lifetime::Ephemeral, None).await?;
let existing_accounts = account_manager.get_account_ids().await?;
assert_eq!(existing_accounts.len(), 3);
// Kill and restart account manager in the same environment
std::mem::drop(account_manager);
let account_manager = create_account_manager(Some("test_account_deletion".to_string()))?;
let existing_accounts = account_manager.get_account_ids().await?;
assert_eq!(existing_accounts.len(), 2); // The ephemeral account was dropped
// Make sure we can't retrieve the ephemeral account
let (acp_client_end, _) = create_endpoints()?;
let (_account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_3, acp_client_end, account_server_end).await?,
Err(ApiError::NotFound)
);
// Retrieve a persistent account that was created in the earlier lifetime
let (acp_client_end, _) = create_endpoints()?;
let (_account_client_end, account_server_end) = create_endpoints()?;
assert_eq!(
account_manager.get_account(account_1, acp_client_end, account_server_end).await?,
Ok(())
);
// Delete an account and verify it is removed.
assert_eq!(account_manager.remove_account(account_2, true).await?, Ok(()));
assert_eq!(account_manager.get_account_ids().await?, vec![account_1]);
Ok(())
}