blob: 8f4bea564875fe416fe7d6250805ef04085bbb0f [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 account_common::{AccountManagerError, FidlLocalAccountId, LocalAccountId, ResultExt};
use anyhow::{format_err, Error};
use fidl::endpoints::{create_endpoints, ClientEnd, ServerEnd};
use fidl_fuchsia_auth::{AppConfig, AuthenticationContextProviderMarker, Status as AuthStatus};
use fidl_fuchsia_auth::{AuthProviderConfig, UserProfileInfo};
use fidl_fuchsia_identity_account::{
AccountAuthState, AccountListenerMarker, AccountListenerOptions, AccountManagerRequest,
AccountManagerRequestStream, AccountMarker, Error as ApiError, Lifetime, Scenario,
};
use fuchsia_component::fuchsia_single_component_package_url;
use fuchsia_inspect::{Inspector, Property};
use futures::lock::Mutex;
use futures::prelude::*;
use lazy_static::lazy_static;
use log::{info, warn};
use std::path::PathBuf;
use std::sync::Arc;
use crate::account_event_emitter::{AccountEvent, AccountEventEmitter};
use crate::account_handler_connection::AccountHandlerConnection;
use crate::account_handler_context::AccountHandlerContext;
use crate::account_map::AccountMap;
use crate::inspect;
const SELF_URL: &str = fuchsia_single_component_package_url!("account_manager");
lazy_static! {
/// The Auth scopes used for authorization during service provider-based account provisioning.
/// An empty vector means that the auth provider should use its default scopes.
static ref APP_SCOPES: Vec<String> = Vec::default();
}
/// The core component of the account system for Fuchsia.
///
/// The AccountManager maintains the set of Fuchsia accounts that are provisioned on the device,
/// launches and configures AuthenticationProvider components to perform authentication via
/// service providers, and launches and delegates to AccountHandler component instances to
/// determine the detailed state and authentication for each account.
///
/// `AHC` An AccountHandlerConnection used to spawn new AccountHandler components.
pub struct AccountManager<AHC: AccountHandlerConnection> {
/// The account map maintains the state of all accounts as well as connections to their account
/// handlers.
account_map: Mutex<AccountMap<AHC>>,
/// Contains the client ends of all AccountListeners which are subscribed to account events.
event_emitter: AccountEventEmitter,
/// Helper for outputting auth_provider information via fuchsia_inspect. Must be retained
/// to avoid dropping the static properties it contains.
_auth_providers_inspect: inspect::AuthProviders,
}
impl<AHC: AccountHandlerConnection> AccountManager<AHC> {
/// Constructs a new AccountManager, loading existing set of accounts from `data_dir`, and an
/// auth provider configuration. The directory must exist at construction.
pub fn new(
data_dir: PathBuf,
auth_provider_config: &[AuthProviderConfig],
auth_mechanism_ids: &[String],
inspector: &Inspector,
) -> Result<AccountManager<AHC>, Error> {
let context =
Arc::new(AccountHandlerContext::new(auth_provider_config, auth_mechanism_ids));
let account_map = AccountMap::load(data_dir, Arc::clone(&context), inspector.root())?;
// Initialize the structs used to output state through the inspect system.
let auth_providers_inspect = inspect::AuthProviders::new(inspector.root());
let auth_provider_types: Vec<String> =
auth_provider_config.iter().map(|apc| apc.auth_provider_type.clone()).collect();
auth_providers_inspect.types.set(&auth_provider_types.join(","));
let event_emitter = AccountEventEmitter::new(inspector.root());
Ok(Self {
account_map: Mutex::new(account_map),
event_emitter,
_auth_providers_inspect: auth_providers_inspect,
})
}
/// Asynchronously handles the supplied stream of `AccountManagerRequest` messages.
pub async fn handle_requests_from_stream(
&self,
mut stream: AccountManagerRequestStream,
) -> Result<(), Error> {
while let Some(req) = stream.try_next().await? {
self.handle_request(req).await?;
}
Ok(())
}
/// Handles a single request to the AccountManager.
pub async fn handle_request(&self, req: AccountManagerRequest) -> Result<(), fidl::Error> {
match req {
AccountManagerRequest::GetAccountIds { responder } => {
let response = self.get_account_ids().await;
responder.send(&response)?;
}
AccountManagerRequest::GetAccountAuthStates { scenario, responder } => {
let mut response = self.get_account_auth_states(scenario).await;
responder.send(&mut response)?;
}
AccountManagerRequest::GetAccount { id, context_provider, account, responder } => {
let mut response = self.get_account(id.into(), context_provider, account).await;
responder.send(&mut response)?;
}
AccountManagerRequest::RegisterAccountListener { listener, options, responder } => {
let mut response = self.register_account_listener(listener, options).await;
responder.send(&mut response)?;
}
AccountManagerRequest::RemoveAccount { id, force, responder } => {
let mut response = self.remove_account(id.into(), force).await;
responder.send(&mut response)?;
}
AccountManagerRequest::ProvisionFromAuthProvider {
auth_context_provider,
auth_provider_type,
lifetime,
auth_mechanism_id,
responder,
} => {
let mut response = self
.provision_from_auth_provider(
auth_context_provider,
auth_provider_type,
lifetime,
auth_mechanism_id,
)
.await;
responder.send(&mut response)?;
}
AccountManagerRequest::ProvisionNewAccount {
lifetime,
auth_mechanism_id,
responder,
} => {
let mut response = self.provision_new_account(lifetime, auth_mechanism_id).await;
responder.send(&mut response)?;
}
AccountManagerRequest::GetAuthenticationMechanisms { responder } => {
responder.send(&mut Err(ApiError::UnsupportedOperation))?;
}
}
Ok(())
}
async fn get_account_ids(&self) -> Vec<FidlLocalAccountId> {
self.account_map.lock().await.get_account_ids().iter().map(|id| id.clone().into()).collect()
}
async fn get_account_auth_states(
&self,
_scenario: Scenario,
) -> Result<Vec<AccountAuthState>, ApiError> {
// TODO(jsankey): Collect authentication state from AccountHandler instances rather than
// returning a fixed value. This will involve opening account handler connections (in
// parallel) for all of the accounts where encryption keys for the account's data partition
// are available.
return Err(ApiError::UnsupportedOperation);
}
async fn get_account(
&self,
id: LocalAccountId,
auth_context_provider: ClientEnd<AuthenticationContextProviderMarker>,
account: ServerEnd<AccountMarker>,
) -> Result<(), ApiError> {
let mut account_map = self.account_map.lock().await;
let account_handler = account_map.get_handler(&id).await.map_err(|err| {
warn!("Failure getting account handler connection: {:?}", err);
err.api_error
})?;
account_handler.proxy().unlock_account().await.map_err(|_| ApiError::Resource)??;
account_handler.proxy().get_account(auth_context_provider, account).await.map_err(
|err| {
warn!("Failure calling get account: {:?}", err);
ApiError::Resource
},
)?
}
async fn register_account_listener(
&self,
listener: ClientEnd<AccountListenerMarker>,
options: AccountListenerOptions,
) -> Result<(), ApiError> {
match (&options.scenario, &options.granularity) {
(None, Some(_)) => return Err(ApiError::InvalidRequest),
(Some(_), _) => return Err(ApiError::UnsupportedOperation),
(None, None) => {}
};
let account_ids = self.account_map.lock().await.get_account_ids();
let proxy = listener.into_proxy().map_err(|err| {
warn!("Could not convert AccountListener client end to proxy {:?}", err);
ApiError::InvalidRequest
})?;
self.event_emitter.add_listener(proxy, options, &account_ids).await.map_err(|err| {
warn!("Could not instantiate AccountListener client {:?}", err);
ApiError::Unknown
})?;
info!("AccountListener established");
Ok(())
}
async fn remove_account(
&self,
account_id: LocalAccountId,
force: bool,
) -> Result<(), ApiError> {
let mut account_map = self.account_map.lock().await;
let account_handler = account_map.get_handler(&account_id).await.map_err(|err| {
warn!("Could not get account handler for account removal {:?}", err);
err.api_error
})?;
// TODO(fxbug.dev/43491): Make a conscious decision on what should happen when removing
// a locked account.
account_handler.proxy().unlock_account().await.map_err(|_| ApiError::Resource)??;
account_handler.proxy().remove_account(force).await.map_err(|_| ApiError::Resource)??;
account_handler.terminate().await;
// Emphemeral accounts were never included in the StoredAccountList and so it does not need
// to be modified when they are removed.
// TODO(fxbug.dev/39455): Handle irrecoverable, corrupt state.
account_map.remove_account(&account_id).await.map_err(|err| {
warn!("Could not remove account: {:?}", err);
// TODO(fxbug.dev/39829): Improve error mapping.
if err.api_error == ApiError::NotFound {
// We already checked for existence, so NotFound is unexpected
ApiError::Internal
} else {
err.api_error
}
})?;
let event = AccountEvent::AccountRemoved(account_id.clone());
self.event_emitter.publish(&event).await;
Ok(())
}
/// Creates a new account handler connection, then creates an account within it,
/// and finally returns the connection.
async fn create_account_internal(
&self,
lifetime: Lifetime,
auth_mechanism_id: Option<String>,
) -> Result<Arc<AHC>, ApiError> {
let account_handler =
self.account_map.lock().await.new_handler(lifetime).map_err(|err| {
warn!("Could not initialize account handler: {:?}", err);
err.api_error
})?;
account_handler
.proxy()
.create_account(auth_mechanism_id.as_ref().map(|x| &**x))
.await
.map_err(|err| {
warn!("Could not create account: {:?}", err);
ApiError::Resource
})??;
Ok(account_handler)
}
async fn provision_new_account(
&self,
lifetime: Lifetime,
auth_mechanism_id: Option<String>,
) -> Result<FidlLocalAccountId, ApiError> {
let account_handler = self.create_account_internal(lifetime, auth_mechanism_id).await?;
let account_id = account_handler.get_account_id();
// Persist the account both in memory and on disk
if let Err(err) = self.add_account(account_handler.clone()).await {
warn!("Failure adding account: {:?}", err);
account_handler.terminate().await;
Err(err.api_error)
} else {
info!("Adding new local account {:?}", account_id);
Ok(account_id.clone().into())
}
}
async fn provision_from_auth_provider(
&self,
auth_context_provider: ClientEnd<AuthenticationContextProviderMarker>,
auth_provider_type: String,
lifetime: Lifetime,
auth_mechanism_id: Option<String>,
) -> Result<FidlLocalAccountId, ApiError> {
let account_handler = self.create_account_internal(lifetime, auth_mechanism_id).await?;
let account_id = account_handler.get_account_id();
// Add a service provider to the account
let _user_profile = match Self::add_service_provider_account(
auth_context_provider,
auth_provider_type,
Arc::clone(&account_handler),
)
.await
{
Ok(user_profile) => user_profile,
Err(err) => {
// TODO(dnordstrom): Remove the newly created account handler as a cleanup.
warn!("Failure adding service provider account: {:?}", err);
account_handler.terminate().await;
return Err(err.api_error);
}
};
// Persist the account both in memory and on disk
if let Err(err) = self.add_account(Arc::clone(&account_handler)).await {
warn!("Failure adding service provider account: {:?}", err);
account_handler.terminate().await;
Err(err.api_error)
} else {
info!("Adding new account {:?}", &account_id);
Ok(account_id.clone().into())
}
}
// Attach a service provider account to this Fuchsia account
async fn add_service_provider_account(
auth_context_provider: ClientEnd<AuthenticationContextProviderMarker>,
auth_provider_type: String,
account_handler: Arc<AHC>,
) -> Result<UserProfileInfo, AccountManagerError> {
// Use account handler to get a channel to the account
let (account_client_end, account_server_end) =
create_endpoints().account_manager_error(ApiError::Resource)?;
account_handler.proxy().get_account(auth_context_provider, account_server_end).await??;
let account_proxy =
account_client_end.into_proxy().account_manager_error(ApiError::Resource)?;
// Use the account to get the persona
let (persona_client_end, persona_server_end) =
create_endpoints().account_manager_error(ApiError::Resource)?;
account_proxy.get_default_persona(persona_server_end).await??;
let persona_proxy =
persona_client_end.into_proxy().account_manager_error(ApiError::Resource)?;
// Use the persona to get the token manager
let (tm_client_end, tm_server_end) =
create_endpoints().account_manager_error(ApiError::Resource)?;
persona_proxy.get_token_manager(SELF_URL, tm_server_end).await??;
let tm_proxy = tm_client_end.into_proxy().account_manager_error(ApiError::Resource)?;
// Use the token manager to authorize
let mut app_config = AppConfig {
auth_provider_type,
client_id: None,
client_secret: None,
redirect_uri: None,
};
let fut = tm_proxy.authorize(
&mut app_config,
None, /* auth_ui_context */
&mut APP_SCOPES.iter().map(|x| &**x),
None, /* user_profile_id */
None, /* auth_code */
);
match fut.await? {
(AuthStatus::Ok, None) => Err(AccountManagerError::new(ApiError::Internal)
.with_cause(format_err!("Invalid response from token manager"))),
(AuthStatus::Ok, Some(user_profile)) => Ok(*user_profile),
(auth_status, _) => Err(AccountManagerError::from(auth_status)),
}
}
// Add the account to the AccountManager, including persistent state.
async fn add_account(&self, account_handler: Arc<AHC>) -> Result<(), AccountManagerError> {
let mut account_map = self.account_map.lock().await;
account_map.add_account(Arc::clone(&account_handler)).await.map_err(|err| {
warn!("Could not add account: {:?}", err);
// TODO(fxbug.dev/39829): Improve error mapping.
if err.api_error == ApiError::FailedPrecondition {
ApiError::Internal
} else {
err.api_error
}
})?;
let event = AccountEvent::AccountAdded(account_handler.get_account_id().clone());
self.event_emitter.publish(&event).await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::account_handler_connection::AccountHandlerConnectionImpl;
use crate::stored_account_list::{StoredAccountList, StoredAccountMetadata};
use fidl::endpoints::{create_request_stream, RequestStream};
use fidl_fuchsia_identity_account::{
AccountListenerRequest, AccountManagerProxy, AccountManagerRequestStream,
AuthChangeGranularity, InitialAccountState, Scenario, ThreatScenario,
};
use fuchsia_async as fasync;
use fuchsia_inspect::{assert_inspect_tree, Inspector};
use fuchsia_zircon as zx;
use futures::future::join;
use lazy_static::lazy_static;
use std::path::Path;
use tempfile::TempDir;
type TestAccountManager = AccountManager<AccountHandlerConnectionImpl>;
lazy_static! {
/// Configuration for a set of fake auth providers used for testing.
/// This can be populated later if needed.
static ref AUTH_PROVIDER_CONFIG: Vec<AuthProviderConfig> = vec![];
static ref AUTH_MECHANISM_IDS: Vec<String> = vec![];
static ref TEST_SCENARIO: Scenario =
Scenario { include_test: false, threat_scenario: ThreatScenario::BasicAttacker };
static ref TEST_GRANULARITY: AuthChangeGranularity = AuthChangeGranularity {
engagement_changes: true,
presence_changes: false,
summary_changes: true,
};
}
const FORCE_REMOVE_ON: bool = true;
fn request_stream_test<TestFn, Fut>(account_manager: TestAccountManager, test_fn: TestFn)
where
TestFn: FnOnce(AccountManagerProxy, Arc<TestAccountManager>) -> Fut,
Fut: Future<Output = Result<(), Error>>,
{
let mut executor = fasync::Executor::new().expect("Failed to create executor");
let (server_chan, client_chan) = zx::Channel::create().expect("Failed to create channel");
let proxy = AccountManagerProxy::new(fasync::Channel::from_channel(client_chan).unwrap());
let request_stream = AccountManagerRequestStream::from_channel(
fasync::Channel::from_channel(server_chan).unwrap(),
);
let account_manager_arc = Arc::new(account_manager);
let account_manager_clone = Arc::clone(&account_manager_arc);
// TODO(fxbug.dev/39745): Migrate off of fuchsia_async::spawn.
fasync::Task::spawn(async move {
account_manager_clone
.handle_requests_from_stream(request_stream)
.await
.unwrap_or_else(|err| panic!("Fatal error handling test request: {:?}", err))
})
.detach();
executor
.run_singlethreaded(test_fn(proxy, account_manager_arc))
.expect("Executor run failed.")
}
// Construct an account manager initialized with the supplied set of accounts.
fn create_accounts(
existing_ids: Vec<u64>,
data_dir: &Path,
inspector: &Inspector,
) -> TestAccountManager {
let stored_account_list = existing_ids
.iter()
.map(|&id| StoredAccountMetadata::new(LocalAccountId::new(id)))
.collect();
StoredAccountList::new(stored_account_list)
.save(data_dir)
.expect("Couldn't write account list");
read_accounts(data_dir, inspector)
}
// Contructs an account manager that reads its accounts from the supplied directory.
fn read_accounts(
data_dir: &Path,
inspector: &Inspector,
) -> AccountManager<AccountHandlerConnectionImpl> {
AccountManager::new(
data_dir.to_path_buf(),
&AUTH_PROVIDER_CONFIG,
&AUTH_MECHANISM_IDS,
inspector,
)
.unwrap()
}
/// Note: Many AccountManager methods launch instances of an AccountHandler. Since its
/// currently not convenient to mock out this component launching in Rust, we rely on the
/// hermetic component test to provide coverage for these areas and only cover the in-process
/// behavior with this unit-test.
#[test]
fn test_new() {
let inspector = Inspector::new();
let data_dir = TempDir::new().unwrap();
request_stream_test(
AccountManager::new(
data_dir.path().into(),
&AUTH_PROVIDER_CONFIG,
&AUTH_MECHANISM_IDS,
&inspector,
)
.unwrap(),
|proxy, _| async move {
assert_eq!(proxy.get_account_ids().await?.len(), 0);
Ok(())
},
);
}
#[test]
fn test_unsupported_scenario() {
let data_dir = TempDir::new().unwrap();
request_stream_test(
create_accounts(vec![], data_dir.path(), &Inspector::new()),
|proxy, _test_object| async move {
assert_eq!(proxy.get_account_ids().await?.len(), 0);
assert_eq!(
proxy.get_account_auth_states(&mut TEST_SCENARIO.clone()).await?,
Err(ApiError::UnsupportedOperation)
);
Ok(())
},
);
}
#[test]
fn test_initially_empty() {
let data_dir = TempDir::new().unwrap();
let inspector = Inspector::new();
request_stream_test(
create_accounts(vec![], data_dir.path(), &inspector),
|proxy, _test_object| async move {
assert_eq!(proxy.get_account_ids().await?.len(), 0);
assert_inspect_tree!(inspector, root: contains {
accounts: {
active: 0 as u64,
total: 0 as u64,
},
listeners: {
active: 0 as u64,
events: 0 as u64,
total_opened: 0 as u64,
},
});
Ok(())
},
);
}
#[test]
fn test_remove_missing_account() {
// Manually create an account manager with one account.
let data_dir = TempDir::new().unwrap();
let stored_account_list =
StoredAccountList::new(vec![StoredAccountMetadata::new(LocalAccountId::new(1))]);
stored_account_list.save(data_dir.path()).unwrap();
let inspector = Inspector::new();
request_stream_test(read_accounts(data_dir.path(), &inspector), |proxy, _test_object| {
async move {
// Try to delete a very different account from the one we added.
assert_eq!(
proxy.remove_account(LocalAccountId::new(42).into(), FORCE_REMOVE_ON).await?,
Err(ApiError::NotFound)
);
assert_inspect_tree!(inspector, root: contains {
accounts: {
total: 1 as u64,
active: 0 as u64,
},
});
Ok(())
}
});
}
/// Sets up an AccountListener which an init event.
#[test]
fn test_account_listener() {
let mut options = AccountListenerOptions {
initial_state: true,
add_account: true,
remove_account: true,
scenario: None,
granularity: None,
};
let data_dir = TempDir::new().unwrap();
let inspector = Inspector::new();
request_stream_test(
create_accounts(vec![1, 2], data_dir.path(), &inspector),
|proxy, _| {
async move {
let (client_end, mut stream) =
create_request_stream::<AccountListenerMarker>().unwrap();
let serve_fut = async move {
let request = stream.try_next().await.expect("stream error");
if let Some(AccountListenerRequest::OnInitialize {
account_states,
responder,
}) = request
{
assert_eq!(
account_states,
vec![
InitialAccountState { account_id: 1, auth_state: None },
InitialAccountState { account_id: 2, auth_state: None },
]
);
responder.send().unwrap();
} else {
panic!("Unexpected message received");
};
if let Some(_) = stream.try_next().await.expect("stream error") {
panic!("Unexpected message, channel should be closed");
}
};
let request_fut = async move {
// The registering itself triggers the init event.
assert_eq!(
proxy
.register_account_listener(client_end, &mut options)
.await
.unwrap(),
Ok(())
);
};
join(request_fut, serve_fut).await;
Ok(())
}
},
);
}
/// Registers an account listener with invalid request arguments.
#[test]
fn test_account_listener_invalid_requests() {
let mut options = AccountListenerOptions {
initial_state: true,
add_account: true,
remove_account: true,
scenario: None,
granularity: Some(Box::new(TEST_GRANULARITY.clone())),
};
let data_dir = TempDir::new().unwrap();
let inspector = Inspector::new();
request_stream_test(
create_accounts(vec![1, 2], data_dir.path(), &inspector),
|proxy, _| async move {
let (client_end, _) = create_request_stream::<AccountListenerMarker>().unwrap();
assert_eq!(
proxy.register_account_listener(client_end, &mut options).await?,
Err(ApiError::InvalidRequest)
);
Ok(())
},
);
}
/// Registers an account listener with a scenario, which is currently unsupported.
#[test]
fn test_account_listener_scenario() {
let mut options = AccountListenerOptions {
initial_state: true,
add_account: true,
remove_account: true,
scenario: Some(Box::new(TEST_SCENARIO.clone())),
granularity: None,
};
let data_dir = TempDir::new().unwrap();
let inspector = Inspector::new();
request_stream_test(
create_accounts(vec![1, 2], data_dir.path(), &inspector),
|proxy, _| async move {
let (client_end, _) = create_request_stream::<AccountListenerMarker>().unwrap();
assert_eq!(
proxy.register_account_listener(client_end, &mut options).await?,
Err(ApiError::UnsupportedOperation)
);
Ok(())
},
);
}
/// Registers an account listener with a scenario and a granularity, which is currently
/// unsupported.
#[test]
fn test_account_listener_scenario_granularity() {
let mut options = AccountListenerOptions {
initial_state: true,
add_account: true,
remove_account: true,
scenario: Some(Box::new(TEST_SCENARIO.clone())),
granularity: Some(Box::new(TEST_GRANULARITY.clone())),
};
let data_dir = TempDir::new().unwrap();
let inspector = Inspector::new();
request_stream_test(
create_accounts(vec![1, 2], data_dir.path(), &inspector),
|proxy, _| async move {
let (client_end, _) = create_request_stream::<AccountListenerMarker>().unwrap();
assert_eq!(
proxy.register_account_listener(client_end, &mut options).await?,
Err(ApiError::UnsupportedOperation)
);
Ok(())
},
);
}
}