| // 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. |
| extern crate serde; |
| extern crate serde_json; |
| |
| use crate::account_handler::AccountHandler; |
| use crate::auth_provider_supplier::AuthProviderSupplier; |
| use crate::persona::{Persona, PersonaContext}; |
| use crate::TokenManager; |
| use account_common::{ |
| AccountManagerError, FidlLocalPersonaId, LocalAccountId, LocalPersonaId, ResultExt, |
| }; |
| use failure::Error; |
| use fidl::encoding::OutOfLine; |
| use fidl::endpoints::{ClientEnd, ServerEnd}; |
| use fidl_fuchsia_auth::{ |
| AuthChangeGranularity, AuthState, AuthenticationContextProviderProxy, ServiceProviderAccount, |
| }; |
| use fidl_fuchsia_auth_account::{ |
| AccountRequest, AccountRequestStream, AuthListenerMarker, PersonaMarker, Status, |
| }; |
| use fidl_fuchsia_auth_account_internal::AccountHandlerContextProxy; |
| use fuchsia_async as fasync; |
| use futures::prelude::*; |
| use log::{error, info, warn}; |
| use serde_derive::{Deserialize, Serialize}; |
| use std::fs::{self, File}; |
| use std::path::{Path, PathBuf}; |
| use std::sync::Arc; |
| |
| /// Name of account doc file (one per account), within the account's dir. |
| const ACCOUNT_DOC: &str = "account.json"; |
| |
| /// Name of temporary account doc file, within the account's dir. |
| const ACCOUNT_DOC_TMP: &str = "account.json.tmp"; |
| |
| /// The file name to use for a token manager database. The location is supplied |
| /// by `AccountHandlerContext.GetAccountPath()` |
| const TOKEN_DB: &str = "tokens.json"; |
| |
| /// The context that a particular request to an Account should be executed in, capturing |
| /// information that was supplied upon creation of the channel. |
| pub struct AccountContext { |
| /// An `AuthenticationContextProviderProxy` capable of generating new `AuthenticationUiContext` |
| /// channels. |
| pub auth_ui_context_provider: AuthenticationContextProviderProxy, |
| } |
| |
| /// Information about the Account that this AccountHandler instance is responsible for. |
| /// |
| /// This state is only available once the Handler has been initialized to a particular account via |
| /// the AccountHandlerControl channel. |
| pub struct Account { |
| /// A device-local identifier for this account. |
| id: LocalAccountId, |
| |
| /// A directory containing data about the Fuchsia account, managed exclusively by one instance |
| /// of this type. It should exist prior to constructing an Account object. |
| account_dir: PathBuf, |
| |
| /// The default persona for this account. |
| default_persona: Arc<Persona>, |
| // TODO(jsankey): Once the system and API surface can support more than a single persona, add |
| // additional state here to store these personae. This will most likely be a hashmap from |
| // LocalPersonaId to Persona struct, and changing default_persona from a struct to an ID. We |
| // will also need to store Arc<TokenManager> at the account level. |
| } |
| |
| impl Account { |
| /// A fixed string returned as the name of all accounts until account names are fully |
| /// implemented. |
| const DEFAULT_ACCOUNT_NAME: &'static str = "Unnamed account"; |
| |
| /// Manually construct an account object, shouldn't normally be called directly. |
| fn new( |
| account_id: LocalAccountId, |
| persona_id: LocalPersonaId, |
| account_dir: PathBuf, |
| context_proxy: AccountHandlerContextProxy, |
| ) -> Result<Account, AccountManagerError> { |
| let token_db_path = account_dir.join(TOKEN_DB); |
| let token_manager = Arc::new( |
| TokenManager::new(&token_db_path, AuthProviderSupplier::new(context_proxy)) |
| .account_manager_status(Status::UnknownError)?, |
| ); |
| Ok(Self { |
| id: account_id.clone(), |
| account_dir, |
| default_persona: Arc::new(Persona::new(persona_id, account_id, token_manager)), |
| }) |
| } |
| |
| /// Creates a new Fuchsia account and persist it on disk. |
| pub fn create( |
| account_id: LocalAccountId, |
| account_dir: PathBuf, |
| context_proxy: AccountHandlerContextProxy, |
| ) -> Result<Account, AccountManagerError> { |
| if StoredAccount::path(&account_dir).exists() { |
| info!("Attempting to create account twice with local id: {:?}", account_id); |
| return Err(AccountManagerError::new(Status::InvalidRequest)); |
| } |
| |
| let local_persona_id = LocalPersonaId::new(rand::random::<u64>()); |
| let stored_account = StoredAccount::new(local_persona_id.clone()); |
| match stored_account.save(&account_dir) { |
| Ok(_) => Self::new(account_id, local_persona_id, account_dir, context_proxy), |
| Err(err) => { |
| warn!("Failed to initialize new Account: {:?}", err); |
| Err(err) |
| } |
| } |
| } |
| |
| /// Loads an existing Fuchsia account from disk. |
| pub fn load( |
| account_id: LocalAccountId, |
| account_dir: PathBuf, |
| context_proxy: AccountHandlerContextProxy, |
| ) -> Result<Account, AccountManagerError> { |
| let stored_account = StoredAccount::load(&account_dir)?; |
| Self::new(account_id, stored_account.default_persona_id, account_dir, context_proxy) |
| } |
| |
| /// Removes the account from disk. |
| pub fn remove(&self) -> Result<(), AccountManagerError> { |
| let token_db_path = &self.account_dir.join(TOKEN_DB); |
| if token_db_path.exists() { |
| fs::remove_file(token_db_path).map_err(|err| { |
| warn!("Failed to delete token db: {:?}", err); |
| AccountManagerError::new(Status::IoError).with_cause(err) |
| })?; |
| } |
| fs::remove_file(StoredAccount::path(&self.account_dir)).map_err(|err| { |
| warn!("Failed to delete account doc: {:?}", err); |
| AccountManagerError::new(Status::IoError).with_cause(err) |
| }) |
| } |
| |
| /// A device-local identifier for this account |
| pub fn id(&self) -> &LocalAccountId { |
| &self.id |
| } |
| |
| /// Asynchronously handles the supplied stream of `AccountRequest` messages. |
| pub async fn handle_requests_from_stream<'a>( |
| &'a self, |
| context: &'a AccountContext, |
| mut stream: AccountRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(req) = await!(stream.try_next())? { |
| self.handle_request(context, req)?; |
| } |
| Ok(()) |
| } |
| |
| /// Dispatches an `AccountRequest` message to the appropriate handler method |
| /// based on its type. |
| pub fn handle_request( |
| &self, |
| context: &AccountContext, |
| req: AccountRequest, |
| ) -> Result<(), fidl::Error> { |
| match req { |
| AccountRequest::GetAccountName { responder } => { |
| let response = self.get_account_name(); |
| responder.send(&response)?; |
| } |
| AccountRequest::GetAuthState { responder } => { |
| let mut response = self.get_auth_state(); |
| responder.send(response.0, response.1.as_mut().map(OutOfLine))?; |
| } |
| AccountRequest::RegisterAuthListener { |
| listener, |
| initial_state, |
| granularity, |
| responder, |
| } => { |
| let response = self.register_auth_listener(listener, initial_state, granularity); |
| responder.send(response)?; |
| } |
| AccountRequest::GetPersonaIds { responder } => { |
| let mut response = self.get_persona_ids(); |
| responder.send(&mut response.iter_mut())?; |
| } |
| AccountRequest::GetDefaultPersona { persona, responder } => { |
| let mut response = self.get_default_persona(context, persona); |
| responder.send(response.0, response.1.as_mut().map(OutOfLine))?; |
| } |
| AccountRequest::GetPersona { id, persona, responder } => { |
| let response = self.get_persona(context, id.into(), persona); |
| responder.send(response)?; |
| } |
| AccountRequest::GetRecoveryAccount { responder } => { |
| let mut response = self.get_recovery_account(); |
| responder.send(response.0, response.1.as_mut().map(OutOfLine))?; |
| } |
| AccountRequest::SetRecoveryAccount { account, responder } => { |
| let response = self.set_recovery_account(account); |
| responder.send(response)?; |
| } |
| } |
| Ok(()) |
| } |
| |
| fn get_account_name(&self) -> String { |
| // TODO(dnordstrom, jsankey): Implement this method, initially by populating the name from |
| // an associated service provider account profile name or a randomly assigned string. |
| Self::DEFAULT_ACCOUNT_NAME.to_string() |
| } |
| |
| fn get_auth_state(&self) -> (Status, Option<AuthState>) { |
| // TODO(jsankey): Return real authentication state once authenticators exist to create it. |
| (Status::Ok, Some(AccountHandler::DEFAULT_AUTH_STATE)) |
| } |
| |
| fn register_auth_listener( |
| &self, |
| _listener: ClientEnd<AuthListenerMarker>, |
| _initial_state: bool, |
| _granularity: AuthChangeGranularity, |
| ) -> Status { |
| // TODO(jsankey): Implement this method. |
| warn!("RegisterAuthListener not yet implemented"); |
| Status::InternalError |
| } |
| |
| fn get_persona_ids(&self) -> Vec<FidlLocalPersonaId> { |
| vec![self.default_persona.id().clone().into()] |
| } |
| |
| fn get_default_persona( |
| &self, |
| context: &AccountContext, |
| persona_server_end: ServerEnd<PersonaMarker>, |
| ) -> (Status, Option<FidlLocalPersonaId>) { |
| let persona_clone = Arc::clone(&self.default_persona); |
| let persona_context = |
| PersonaContext { auth_ui_context_provider: context.auth_ui_context_provider.clone() }; |
| match persona_server_end.into_stream() { |
| Ok(stream) => { |
| fasync::spawn( |
| async move { |
| await!(persona_clone.handle_requests_from_stream(&persona_context, stream)) |
| .unwrap_or_else(|e| error!("Error handling Persona channel {:?}", e)) |
| }, |
| ); |
| (Status::Ok, Some(self.default_persona.id().clone().into())) |
| } |
| Err(e) => { |
| error!("Error opening Persona channel {:?}", e); |
| (Status::IoError, None) |
| } |
| } |
| } |
| |
| fn get_persona( |
| &self, |
| context: &AccountContext, |
| id: LocalPersonaId, |
| persona_server_end: ServerEnd<PersonaMarker>, |
| ) -> Status { |
| if &id == self.default_persona.id() { |
| self.get_default_persona(context, persona_server_end).0 |
| } else { |
| warn!("Requested persona does not exist {:?}", id); |
| Status::NotFound |
| } |
| } |
| |
| fn get_recovery_account(&self) -> (Status, Option<ServiceProviderAccount>) { |
| // TODO(jsankey): Implement this method. |
| warn!("GetRecoveryAccount not yet implemented"); |
| (Status::InternalError, None) |
| } |
| |
| fn set_recovery_account(&self, _account: ServiceProviderAccount) -> Status { |
| // TODO(jsankey): Implement this method. |
| warn!("SetRecoveryAccount not yet implemented"); |
| Status::InternalError |
| } |
| } |
| |
| /// Json-representation of Fuchsia account state, on disk. As this format evolves, |
| /// cautiousness is encouraged to ensure backwards compatibility. |
| #[derive(Serialize, Deserialize)] |
| struct StoredAccount { |
| /// Default persona id for this account |
| default_persona_id: LocalPersonaId, |
| } |
| |
| impl StoredAccount { |
| /// Create a new stored account. No side effects. |
| pub fn new(default_persona_id: LocalPersonaId) -> StoredAccount { |
| Self { default_persona_id } |
| } |
| |
| /// Load StoredAccount from disk |
| pub fn load(account_dir: &Path) -> Result<StoredAccount, AccountManagerError> { |
| let path = Self::path(account_dir); |
| if !path.exists() { |
| warn!("Failed to locate account doc: {:?}", path); |
| return Err(AccountManagerError::new(Status::NotFound)); |
| }; |
| let file = File::open(path).map_err(|err| { |
| warn!("Failed to read account doc: {:?}", err); |
| AccountManagerError::new(Status::IoError).with_cause(err) |
| })?; |
| serde_json::from_reader(file).map_err(|err| { |
| warn!("Failed to parse account doc: {:?}", err); |
| AccountManagerError::new(Status::InternalError).with_cause(err) |
| }) |
| } |
| |
| /// Convenience path to the doc file, given the account_dir |
| fn path(account_dir: &Path) -> PathBuf { |
| account_dir.join(ACCOUNT_DOC) |
| } |
| |
| /// Convenience path to the doc temp file, given the account_dir; used for safe writing |
| fn tmp_path(account_dir: &Path) -> PathBuf { |
| account_dir.join(ACCOUNT_DOC_TMP) |
| } |
| |
| /// Write StoredAccount to disk, ensuring the file is either written completely or not |
| /// modified. |
| pub fn save(&self, account_dir: &Path) -> Result<(), AccountManagerError> { |
| let path = Self::path(account_dir); |
| let tmp_path = Self::tmp_path(account_dir); |
| { |
| let tmp_file = File::create(&tmp_path).map_err(|err| { |
| warn!("Failed to create account tmp doc: {:?}", err); |
| AccountManagerError::new(Status::IoError).with_cause(err) |
| })?; |
| serde_json::to_writer(tmp_file, self).map_err(|err| { |
| warn!("Failed to write account doc: {:?}", err); |
| AccountManagerError::new(Status::IoError).with_cause(err) |
| })?; |
| } |
| fs::rename(&tmp_path, &path).map_err(|err| { |
| warn!("Failed to rename account doc: {:?}", err); |
| AccountManagerError::new(Status::IoError).with_cause(err) |
| }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::test_util::*; |
| use fidl::endpoints::create_endpoints; |
| use fidl_fuchsia_auth::AuthenticationContextProviderMarker; |
| use fidl_fuchsia_auth_account::{AccountMarker, AccountProxy}; |
| use fidl_fuchsia_auth_account_internal::AccountHandlerContextMarker; |
| use fuchsia_async as fasync; |
| |
| /// Type to hold the common state require during construction of test objects and execution |
| /// of a test, including an async executor and a temporary location in the filesystem. |
| struct Test { |
| executor: fasync::Executor, |
| location: TempLocation, |
| } |
| |
| impl Test { |
| fn new() -> Test { |
| Test { |
| executor: fasync::Executor::new().expect("Failed to create executor"), |
| location: TempLocation::new(), |
| } |
| } |
| |
| fn create_account(&self) -> Result<Account, AccountManagerError> { |
| let (account_handler_context_client_end, _) = |
| create_endpoints::<AccountHandlerContextMarker>().unwrap(); |
| Account::create( |
| TEST_ACCOUNT_ID.clone(), |
| self.location.path.clone(), |
| account_handler_context_client_end.into_proxy().unwrap(), |
| ) |
| } |
| |
| fn load_account(&self) -> Result<Account, AccountManagerError> { |
| let (account_handler_context_client_end, _) = |
| create_endpoints::<AccountHandlerContextMarker>().unwrap(); |
| Account::load( |
| TEST_ACCOUNT_ID.clone(), |
| self.location.path.clone(), |
| account_handler_context_client_end.into_proxy().unwrap(), |
| ) |
| } |
| |
| fn run<TestFn, Fut>(&mut self, test_object: Account, test_fn: TestFn) |
| where |
| TestFn: FnOnce(AccountProxy) -> Fut, |
| Fut: Future<Output = Result<(), Error>>, |
| { |
| let (account_client_end, account_server_end) = |
| create_endpoints::<AccountMarker>().unwrap(); |
| let account_proxy = account_client_end.into_proxy().unwrap(); |
| let request_stream = account_server_end.into_stream().unwrap(); |
| |
| let (ui_context_provider_client_end, _) = |
| create_endpoints::<AuthenticationContextProviderMarker>().unwrap(); |
| let context = AccountContext { |
| auth_ui_context_provider: ui_context_provider_client_end.into_proxy().unwrap(), |
| }; |
| |
| fasync::spawn( |
| async move { |
| await!(test_object.handle_requests_from_stream(&context, request_stream)) |
| .unwrap_or_else(|err| { |
| panic!("Fatal error handling test request: {:?}", err) |
| }) |
| }, |
| ); |
| |
| self.executor.run_singlethreaded(test_fn(account_proxy)).expect("Executor run failed.") |
| } |
| } |
| |
| #[test] |
| fn test_random_persona_id() { |
| let mut test = Test::new(); |
| // Generating two accounts with the same accountID should lead to two different persona IDs |
| let account_1 = test.create_account().unwrap(); |
| test.location = TempLocation::new(); |
| let account_2 = test.create_account().unwrap(); |
| assert_ne!(account_1.default_persona.id(), account_2.default_persona.id()); |
| } |
| |
| #[test] |
| fn test_get_account_name() { |
| let mut test = Test::new(); |
| test.run(test.create_account().unwrap(), async move |proxy| { |
| assert_eq!(await!(proxy.get_account_name())?, Account::DEFAULT_ACCOUNT_NAME); |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_create_and_load() { |
| let test = Test::new(); |
| let account_1 = test.create_account().unwrap(); // Persists the account on disk |
| let account_2 = test.load_account().unwrap(); // Reads from same location |
| // Since persona ids are random, we can check that loading worked successfully here |
| assert_eq!(account_1.default_persona.id(), account_2.default_persona.id()); |
| } |
| |
| #[test] |
| fn test_load_non_existing() { |
| let test = Test::new(); |
| assert!(test.load_account().is_err()); // Reads from uninitialized location |
| } |
| |
| #[test] |
| fn test_create_twice() { |
| let test = Test::new(); |
| assert!(test.create_account().is_ok()); |
| assert!(test.create_account().is_err()); // Tries to write to same dir |
| } |
| |
| #[test] |
| fn test_get_auth_state() { |
| let mut test = Test::new(); |
| test.run(test.create_account().unwrap(), async move |proxy| { |
| assert_eq!( |
| await!(proxy.get_auth_state())?, |
| (Status::Ok, Some(Box::new(AccountHandler::DEFAULT_AUTH_STATE))) |
| ); |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_register_auth_listener() { |
| let mut test = Test::new(); |
| test.run(test.create_account().unwrap(), async move |proxy| { |
| let (auth_listener_client_end, _) = create_endpoints().unwrap(); |
| assert_eq!( |
| await!(proxy.register_auth_listener( |
| auth_listener_client_end, |
| true, /* include initial state */ |
| &mut AuthChangeGranularity { summary_changes: true } |
| ))?, |
| Status::InternalError |
| ); |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_get_persona_ids() { |
| let mut test = Test::new(); |
| // Note: Persona ID is random. Record the persona_id before starting the test. |
| let account = test.create_account().unwrap(); |
| let persona_id = &account.default_persona.id().clone(); |
| |
| test.run(account, async move |proxy| { |
| let response = await!(proxy.get_persona_ids())?; |
| assert_eq!(response.len(), 1); |
| assert_eq!(&LocalPersonaId::new(response[0].id), persona_id); |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_get_default_persona() { |
| let mut test = Test::new(); |
| // Note: Persona ID is random. Record the persona_id before starting the test. |
| let account = test.create_account().unwrap(); |
| let persona_id = &account.default_persona.id().clone(); |
| |
| test.run(account, async move |account_proxy| { |
| let (persona_client_end, persona_server_end) = create_endpoints().unwrap(); |
| let response = await!(account_proxy.get_default_persona(persona_server_end))?; |
| assert_eq!(response.0, Status::Ok); |
| assert_eq!(&LocalPersonaId::from(*response.1.unwrap()), persona_id); |
| |
| // The persona channel should now be usable. |
| let persona_proxy = persona_client_end.into_proxy().unwrap(); |
| assert_eq!( |
| await!(persona_proxy.get_auth_state())?, |
| (Status::Ok, Some(Box::new(AccountHandler::DEFAULT_AUTH_STATE))) |
| ); |
| |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_get_persona_by_correct_id() { |
| let mut test = Test::new(); |
| let account = test.create_account().unwrap(); |
| let persona_id = account.default_persona.id().clone(); |
| |
| test.run(account, async move |account_proxy| { |
| let (persona_client_end, persona_server_end) = create_endpoints().unwrap(); |
| assert_eq!( |
| await!(account_proxy |
| .get_persona(&mut FidlLocalPersonaId::from(persona_id), persona_server_end))?, |
| Status::Ok |
| ); |
| |
| // The persona channel should now be usable. |
| let persona_proxy = persona_client_end.into_proxy().unwrap(); |
| assert_eq!( |
| await!(persona_proxy.get_auth_state())?, |
| (Status::Ok, Some(Box::new(AccountHandler::DEFAULT_AUTH_STATE))) |
| ); |
| |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_get_persona_by_incorrect_id() { |
| let mut test = Test::new(); |
| let account = test.create_account().unwrap(); |
| // Note: This fixed value has a 1 - 2^64 probability of not matching the randomly chosen |
| // one. |
| let wrong_id = LocalPersonaId::new(13); |
| |
| test.run(account, async move |proxy| { |
| let (_, persona_server_end) = create_endpoints().unwrap(); |
| assert_eq!( |
| await!(proxy.get_persona(&mut wrong_id.into(), persona_server_end))?, |
| Status::NotFound |
| ); |
| |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_set_recovery_account() { |
| let mut test = Test::new(); |
| let mut service_provider_account = ServiceProviderAccount { |
| identity_provider_domain: "google.com".to_string(), |
| user_profile_id: "test_obfuscated_gaia_id".to_string(), |
| }; |
| |
| test.run(test.create_account().unwrap(), async move |proxy| { |
| assert_eq!( |
| await!(proxy.set_recovery_account(&mut service_provider_account))?, |
| Status::InternalError |
| ); |
| Ok(()) |
| }); |
| } |
| |
| #[test] |
| fn test_get_recovery_account() { |
| let mut test = Test::new(); |
| let expectation = (Status::InternalError, None); |
| test.run(test.create_account().unwrap(), async move |proxy| { |
| assert_eq!(await!(proxy.get_recovery_account())?, expectation); |
| Ok(()) |
| }); |
| } |
| } |