diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs b/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs index f3305e986fe..128b50cbdf0 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/cli_state.rs @@ -138,7 +138,7 @@ impl CliState { /// Delete the local database and log files pub async fn delete(&self) -> Result<()> { self.database.drop_tables().await?; - Ok(Self::delete_at(&self.dir)?) + self.delete_local_data() } /// Delete the local data on disk: sqlite database file and log files @@ -278,9 +278,8 @@ impl CliState { // Delete nodes logs let _ = std::fs::remove_dir_all(Self::make_nodes_dir_path(root_path)); // Delete the nodes database, keep the application database - let _ = match Self::make_database_configuration(root_path)? { - DatabaseConfiguration::Sqlite(path) => std::fs::remove_file(path)?, - DatabaseConfiguration::Postgres { .. } => (), + if let Some(path) = Self::make_database_configuration(root_path)?.path() { + std::fs::remove_file(path)? }; Ok(()) } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs index 77597bd1ef9..cb3f9817ca3 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/identities.rs @@ -66,8 +66,8 @@ impl CliState { ) -> Result { let vault = self.get_named_vault(vault_name).await?; - // Check that the vault is an KMS vault - if !vault.is_kms() { + // Check that the vault is an AWS KMS vault + if !vault.use_aws_kms() { return Err(Error::new( Origin::Api, Kind::Misuse, diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs index 8e5f95067a0..66cb236a225 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository.rs @@ -1,6 +1,4 @@ -use std::path::Path; - -use crate::cli_state::NamedVault; +use crate::cli_state::{NamedVault, VaultType}; use ockam_core::async_trait; use ockam_core::Result; @@ -9,20 +7,20 @@ use ockam_core::Result; #[async_trait] pub trait VaultsRepository: Send + Sync + 'static { /// Store a new vault path with an associated name - async fn store_vault(&self, name: &str, path: &Path, is_kms: bool) -> Result; + async fn store_vault(&self, name: &str, vault_type: VaultType) -> Result; /// Update a vault path - async fn update_vault(&self, name: &str, path: &Path) -> Result<()>; + async fn update_vault(&self, name: &str, vault_type: VaultType) -> Result<()>; /// Delete a vault given its name async fn delete_named_vault(&self, name: &str) -> Result<()>; + /// Return the database vault if it has been created + async fn get_database_vault(&self) -> Result>; + /// Return a vault by name async fn get_named_vault(&self, name: &str) -> Result>; - /// Return a vault by path - async fn get_named_vault_with_path(&self, path: &Path) -> Result>; - /// Return all vaults async fn get_named_vaults(&self) -> Result>; } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs index b9ecf114f6f..dc787813f12 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/vaults_repository_sql.rs @@ -1,13 +1,13 @@ -use std::path::{Path, PathBuf}; -use std::str::FromStr; +use std::path::PathBuf; use sqlx::*; -use crate::cli_state::{NamedVault, VaultsRepository}; use ockam::{FromSqlxError, SqlxDatabase, ToVoid}; use ockam_core::async_trait; use ockam_core::Result; -use ockam_node::database::{Boolean, ToSqlxType}; +use ockam_node::database::{Boolean, Nullable, ToSqlxType}; + +use crate::cli_state::{NamedVault, UseAwsKms, VaultType, VaultsRepository}; #[derive(Clone)] pub struct VaultsSqlxDatabase { @@ -28,32 +28,47 @@ impl VaultsSqlxDatabase { #[async_trait] impl VaultsRepository for VaultsSqlxDatabase { - async fn store_vault(&self, name: &str, path: &Path, is_kms: bool) -> Result { - let query = query("INSERT INTO vault VALUES ($1, $2, $4, $4)") - .bind(name) - .bind(path.to_sql()) - .bind(true) - .bind(is_kms); - query.execute(&*self.database.pool).await.void()?; - - Ok(NamedVault::new(name, path.into(), is_kms)) + async fn store_vault(&self, name: &str, vault_type: VaultType) -> Result { + let mut transaction = self.database.begin().await.into_core()?; + + let query1 = + query_scalar("SELECT EXISTS(SELECT 1 FROM vault WHERE is_default = $1)").bind(true); + let default_exists: Boolean = query1.fetch_one(&mut *transaction).await.into_core()?; + let default_exists = default_exists.to_bool(); + + let query = query( + r#" + INSERT INTO + vault (name, path, is_default, is_kms) + VALUES ($1, $2, $3, $4) + ON CONFLICT (name) + DO UPDATE SET path = $2, is_default = $3, is_kms = $4"#, + ) + .bind(name) + .bind(vault_type.path().map(|p| p.to_sql())) + .bind(!default_exists) + .bind(vault_type.use_aws_kms()); + query.execute(&mut *transaction).await.void()?; + + transaction.commit().await.void()?; + Ok(NamedVault::new(name, vault_type, !default_exists)) } - async fn update_vault(&self, name: &str, path: &Path) -> Result<()> { - let query = query("UPDATE vault SET path = $1 WHERE name = $2") - .bind(path.to_sql()) + async fn update_vault(&self, name: &str, vault_type: VaultType) -> Result<()> { + let query = query("UPDATE vault SET path = $1, is_kms = $2 WHERE name = $3") + .bind(vault_type.path().map(|p| p.to_sql())) + .bind(vault_type.use_aws_kms()) .bind(name); query.execute(&*self.database.pool).await.void() } - /// Delete a vault by name async fn delete_named_vault(&self, name: &str) -> Result<()> { let query = query("DELETE FROM vault WHERE name = $1").bind(name); query.execute(&*self.database.pool).await.void() } - async fn get_named_vault(&self, name: &str) -> Result> { - let query = query_as("SELECT name, path, is_kms FROM vault WHERE name = $1").bind(name); + async fn get_database_vault(&self) -> Result> { + let query = query_as("SELECT name, path, is_default, is_kms FROM vault WHERE path is NULL"); let row: Option = query .fetch_optional(&*self.database.pool) .await @@ -61,9 +76,9 @@ impl VaultsRepository for VaultsSqlxDatabase { row.map(|r| r.named_vault()).transpose() } - async fn get_named_vault_with_path(&self, path: &Path) -> Result> { + async fn get_named_vault(&self, name: &str) -> Result> { let query = - query_as("SELECT name, path, is_kms FROM vault WHERE path = $1").bind(path.to_sql()); + query_as("SELECT name, path, is_default, is_kms FROM vault WHERE name = $1").bind(name); let row: Option = query .fetch_optional(&*self.database.pool) .await @@ -72,7 +87,7 @@ impl VaultsRepository for VaultsSqlxDatabase { } async fn get_named_vaults(&self) -> Result> { - let query = query_as("SELECT name, path, is_kms FROM vault"); + let query = query_as("SELECT name, path, is_default, is_kms FROM vault"); let rows: Vec = query.fetch_all(&*self.database.pool).await.into_core()?; rows.iter().map(|r| r.named_vault()).collect() } @@ -83,7 +98,8 @@ impl VaultsRepository for VaultsSqlxDatabase { #[derive(FromRow)] pub(crate) struct VaultRow { name: String, - path: String, + path: Nullable, + is_default: Boolean, is_kms: Boolean, } @@ -91,10 +107,24 @@ impl VaultRow { pub(crate) fn named_vault(&self) -> Result { Ok(NamedVault::new( &self.name, - PathBuf::from_str(self.path.as_str()).unwrap(), - self.is_kms.to_bool(), + self.vault_type(), + self.is_default(), )) } + + pub(crate) fn vault_type(&self) -> VaultType { + match self.path.to_option() { + None => VaultType::database(UseAwsKms::from(self.is_kms.to_bool())), + Some(p) => VaultType::local_file( + PathBuf::from(p).as_path(), + UseAwsKms::from(self.is_kms.to_bool()), + ), + } + } + + pub(crate) fn is_default(&self) -> bool { + self.is_default.to_bool() + } } #[cfg(test)] @@ -109,39 +139,32 @@ mod test { let repository: Arc = Arc::new(VaultsSqlxDatabase::new(db)); // A vault can be defined with a path and stored under a specific name - let named_vault1 = repository - .store_vault("vault1", Path::new("path"), false) - .await?; - let expected = NamedVault::new("vault1", Path::new("path").into(), false); + let vault_type = VaultType::local_file("path", UseAwsKms::No); + let named_vault1 = repository.store_vault("vault1", vault_type.clone()).await?; + let expected = NamedVault::new("vault1", vault_type.clone(), true); assert_eq!(named_vault1, expected); - // A vault with the same name can not be created twice - let result = repository - .store_vault("vault1", Path::new("path"), false) - .await; - assert!(result.is_err()); - // The vault can then be retrieved with its name let result = repository.get_named_vault("vault1").await?; assert_eq!(result, Some(named_vault1.clone())); - // The vault can then be retrieved with its path - let result = repository - .get_named_vault_with_path(Path::new("path")) - .await?; - assert_eq!(result, Some(named_vault1.clone())); + // Another vault can be created. + // It is not the default vault + let vault_type = VaultType::local_file("path2", UseAwsKms::No); + let named_vault2 = repository.store_vault("vault2", vault_type.clone()).await?; + let expected = NamedVault::new("vault2", vault_type.clone(), false); + // it is not the default vault + assert_eq!(named_vault2, expected); - // The vault can be set at another path + // The first vault can be set at another path + let vault_type = VaultType::local_file("path2", UseAwsKms::No); repository - .update_vault("vault1", Path::new("path2")) + .update_vault("vault1", vault_type.clone()) .await?; let result = repository.get_named_vault("vault1").await?; - assert_eq!( - result, - Some(NamedVault::new("vault1", Path::new("path2").into(), false)) - ); + assert_eq!(result, Some(NamedVault::new("vault1", vault_type, true))); - // The vault can also be deleted + // The first vault can be deleted repository.delete_named_vault("vault1").await?; let result = repository.get_named_vault("vault1").await?; assert_eq!(result, None); @@ -155,11 +178,10 @@ mod test { with_dbs(|db| async move { let repository: Arc = Arc::new(VaultsSqlxDatabase::new(db)); - // A KMS vault can be created by setting the kms flag to true - let kms = repository - .store_vault("kms", Path::new("path"), true) - .await?; - let expected = NamedVault::new("kms", Path::new("path").into(), true); + // It is possible to create a vault storing its signing keys in an AWS KMS + let vault_type = VaultType::database(UseAwsKms::Yes); + let kms = repository.store_vault("kms", vault_type.clone()).await?; + let expected = NamedVault::new("kms", vault_type, true); assert_eq!(kms, expected); Ok(()) }) diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs index 75459e95a6e..fb598d78594 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/vaults.rs @@ -28,26 +28,51 @@ impl CliState { /// If the path is not specified then: /// - if this is the first vault then secrets are persisted in the main database /// - if this is a new vault then secrets are persisted in $OCKAM_HOME/vault_name - #[instrument(skip_all, fields(vault_name = vault_name.clone(), path = path.clone().map_or("n/a".to_string(), |p| p.to_string_lossy().to_string())))] + #[instrument(skip_all, fields(vault_name = vault_name.clone()))] pub async fn create_named_vault( &self, - vault_name: &Option, - path: &Option, + vault_name: Option, + path: Option, + use_aws_kms: UseAwsKms, ) -> Result { - self.create_a_vault(vault_name, path, false).await - } + let vaults_repository = self.vaults_repository(); - /// Create a KMS vault with a given name - /// If the path is not specified then: - /// - if this is the first vault then secrets are persisted in the main database - /// - if this is a new vault then secrets are persisted in $OCKAM_HOME/vault_name - #[instrument(skip_all, fields(vault_name = vault_name.clone(), path = path.clone().map_or("n/a".to_string(), |p| p.to_string_lossy().to_string())))] - pub async fn create_kms_vault( - &self, - vault_name: &Option, - path: &Option, - ) -> Result { - self.create_a_vault(vault_name, path, true).await + // determine the vault name to use if not given by the user + let vault_name = match vault_name { + Some(vault_name) => vault_name.clone(), + None => self.make_vault_name().await?, + }; + + // verify that a vault with that name does not exist + if vaults_repository + .get_named_vault(&vault_name) + .await? + .is_some() + { + return Err(CliStateError::AlreadyExists { + resource: "vault".to_string(), + name: vault_name.to_string(), + }); + } + + // Determine if the vault needs to be created at a specific path + // or if data can be stored in the main database directly + match path { + None => match self.vaults_repository().get_database_vault().await? { + None => Ok(vaults_repository + .store_vault(&vault_name, VaultType::database(use_aws_kms)) + .await?), + Some(_) => { + let path = self.make_vault_path(&vault_name); + Ok(self + .create_local_vault(vault_name, &path, use_aws_kms) + .await?) + } + }, + Some(path) => Ok(self + .create_local_vault(vault_name, &path, use_aws_kms) + .await?), + } } /// Delete an existing vault @@ -78,14 +103,14 @@ impl CliState { let vault = repository.get_named_vault(vault_name).await?; if let Some(vault) = vault { repository.delete_named_vault(vault_name).await?; - - // if the vault is stored in a separate file remove that file - if Some(vault.path.as_path()) != self.database_configuration()?.path() { - let _ = std::fs::remove_file(vault.path); - } else { - // otherwise delete the tables used by the database vault - self.purpose_keys_repository().delete_all().await?; - self.secrets_repository().delete_all().await?; + match vault.vault_type { + VaultType::DatabaseVault { .. } => { + self.purpose_keys_repository().delete_all().await?; + self.secrets_repository().delete_all().await?; + } + VaultType::LocalFileVault { path, .. } => { + let _ = std::fs::remove_file(path); + } } } Ok(()) @@ -143,7 +168,6 @@ impl CliState { #[instrument(skip_all, fields(vault_name = vault_name))] pub async fn get_or_create_named_vault(&self, vault_name: &str) -> Result { let vaults_repository = self.vaults_repository(); - let is_default = vault_name == DEFAULT_VAULT_NAME; if let Ok(Some(existing_vault)) = vaults_repository.get_named_vault(vault_name).await { return Ok(existing_vault); @@ -152,17 +176,39 @@ impl CliState { self.notify_message(fmt_log!( "This Identity needs a Vault to store its secrets." )); - self.notify_message(fmt_log!( - "There is no default Vault on this machine, creating one..." - )); - let named_vault = self - .create_a_vault(&Some(vault_name.to_string()), &None, false) - .await?; - self.notify_message(fmt_ok!( - "Created a new Vault named {} on your disk.", - color_primary(vault_name) - )); - if is_default { + let named_vault = if self + .vaults_repository() + .get_database_vault() + .await? + .is_none() + { + self.notify_message(fmt_log!( + "There is no default Vault on this machine, creating one..." + )); + let vault = self + .create_database_vault(vault_name.to_string(), UseAwsKms::No) + .await?; + self.notify_message(fmt_ok!( + "Created a new Vault named {}.", + color_primary(vault_name) + )); + vault + } else { + let vault = self + .create_local_vault( + vault_name.to_string(), + &self.make_vault_path(vault_name), + UseAwsKms::No, + ) + .await?; + self.notify_message(fmt_ok!( + "Created a new Vault named {} on your disk.", + color_primary(vault_name) + )); + vault + }; + + if named_vault.is_default() { self.notify_message(fmt_ok!( "Marked this new Vault as your default Vault, on this machine.\n" )); @@ -210,38 +256,52 @@ impl CliState { pub async fn move_vault(&self, vault_name: &str, path: &Path) -> Result<()> { let repository = self.vaults_repository(); let vault = self.get_named_vault(vault_name).await?; - if self.is_database_path(vault.path().as_path()) { - return Err(ockam_core::Error::new(Origin::Api, Kind::Invalid, format!("The vault at path {:?} cannot be moved to {path:?} because this is the default vault", vault.path())))?; - }; - - // copy the file to the new location - std::fs::copy(vault.path(), path)?; - // update the path in the database - repository.update_vault(vault_name, path).await?; - // remove the old file - std::fs::remove_file(vault.path())?; + match vault.vault_type { + VaultType::DatabaseVault { .. } => Err(ockam_core::Error::new( + Origin::Api, + Kind::Invalid, + format!( + "The vault {} cannot be moved to {path:?} because this is the default vault", + vault.name() + ), + ))?, + VaultType::LocalFileVault { + path: old_path, + use_aws_kms, + } => { + // copy the file to the new location + std::fs::copy(&old_path, path)?; + // update the path in the database + repository + .update_vault(vault_name, VaultType::local_file(path, use_aws_kms)) + .await?; + // remove the old file + std::fs::remove_file(old_path)?; + } + } Ok(()) } - /// Move a vault file to another location if the vault is not the default vault - /// contained in the main database - #[instrument(skip_all, fields(vault_name = named_vault.name, path = named_vault.path.to_string_lossy().to_string()))] + /// Make a concrete vault based on the NamedVault metadata + #[instrument(skip_all, fields(vault_name = named_vault.name))] pub async fn make_vault(&self, named_vault: NamedVault) -> Result { - let db = if Some(named_vault.path.as_path()) == self.database_ref().path() { - self.database() - } else { + let db = match named_vault.vault_type { + VaultType::DatabaseVault { .. } => self.database(), + VaultType::LocalFileVault { ref path, .. } => // TODO: Avoid creating multiple dbs with the same file - SqlxDatabase::create_sqlite(named_vault.path.as_path()).await? + { + SqlxDatabase::create_sqlite(path.as_path()).await? + } }; - let mut vault = Vault::create_with_database(db); - if named_vault.is_kms { + if named_vault.vault_type.use_aws_kms() { + let mut vault = Vault::create_with_database(db); let aws_vault = Arc::new(AwsSigningVault::create().await?); vault.identity_vault = aws_vault.clone(); vault.credential_vault = aws_vault; Ok(vault) } else { - Ok(vault) + Ok(Vault::create_with_database(db)) } } } @@ -258,66 +318,57 @@ impl CliState { /// Private functions impl CliState { - /// Create a vault with the given name and indicate if it is going to be used as a KMS vault - /// If the vault with the same name already exists then an error is returned - /// If there is already a file at the provided path, then an error is returned - #[instrument(skip_all, fields(vault_name = vault_name))] - async fn create_a_vault( + /// Create the database vault if it doesn't exist already + async fn create_database_vault( &self, - vault_name: &Option, - path: &Option, - is_kms: bool, + vault_name: String, + use_aws_kms: UseAwsKms, ) -> Result { - let vaults_repository = self.vaults_repository(); - - // determine the vault name to use if not given by the user - let vault_name = match vault_name { - Some(vault_name) => vault_name.clone(), - None => self.make_vault_name().await?, - }; - - // verify that a vault with that name does not exist - if vaults_repository - .get_named_vault(&vault_name) - .await? - .is_some() - { - return Err(CliStateError::AlreadyExists { - resource: "vault".to_string(), - name: vault_name.to_string(), - }); + match self.vaults_repository().get_database_vault().await? { + None => Ok(self + .vaults_repository() + .store_vault(&vault_name, VaultType::database(use_aws_kms)) + .await?), + Some(vault) => Err(CliStateError::AlreadyExists { + resource: "database vault".to_string(), + name: vault.name().to_string(), + }), } + } - // determine the vault path - // if the vault is the first vault we store the data directly in the main database - // otherwise we open a new file with the vault name - let path = match path { - Some(path) => path.clone(), - None => self.make_vault_path(&vault_name).await?, - }; - + /// Create a vault store in a local file if the path has not been taken already + async fn create_local_vault( + &self, + vault_name: String, + path: &PathBuf, + use_aws_kms: UseAwsKms, + ) -> Result { // check if the new file can be created - let path_taken = self.get_named_vault_with_path(&path).await?.is_some(); + let path_taken = self + .get_named_vaults() + .await? + .iter() + .any(|v| v.path() == Some(path.as_path())); if path_taken { - return Err(CliStateError::AlreadyExists { + Err(CliStateError::AlreadyExists { resource: "vault path".to_string(), name: format!("{path:?}"), - }); + })?; } else { // create a new file if we need to store the vault data outside of the main database - if !self.is_database_path(path.as_path()) { - // similar to File::create_new which is unstable for now - OpenOptions::new() - .read(true) - .write(true) - .create_new(true) - .open(&path)?; - } + // similar to File::create_new which is unstable for now + OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(path)?; }; - - // store the vault metadata - Ok(vaults_repository - .store_vault(&vault_name, &path, is_kms) + Ok(self + .vaults_repository() + .store_vault( + &vault_name, + VaultType::local_file(path.as_path(), use_aws_kms), + ) .await?) } @@ -337,49 +388,100 @@ impl CliState { } /// Decide which path to use for a vault path: - /// - if no vault has been using the main database, use it /// - otherwise return a new path alongside the database $OCKAM_HOME/vault-{vault_name} - /// - async fn make_vault_path(&self, vault_name: &str) -> Result { - let vaults_repository = self.vaults_repository(); - // is there already a vault using the main database? - let is_database_path_available = vaults_repository - .get_named_vaults() - .await? - .iter() - .all(|v| !self.is_database_path(v.path().as_path())); - if is_database_path_available { - self.database_configuration()? - .path() - .map(|p| p.to_path_buf()) - .ok_or(CliStateError::EmptyPath) - } else { - Ok(self.dir().join(format!("vault-{vault_name}"))) - } - } - - async fn get_named_vault_with_path(&self, path: &Path) -> Result> { - Ok(self - .vaults_repository() - .get_named_vault_with_path(path) - .await?) + fn make_vault_path(&self, vault_name: &str) -> PathBuf { + self.dir().join(format!("vault-{vault_name}")) } } #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] pub struct NamedVault { name: String, - path: PathBuf, - is_kms: bool, + vault_type: VaultType, + is_default: bool, +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub enum VaultType { + DatabaseVault { + use_aws_kms: UseAwsKms, + }, + LocalFileVault { + path: PathBuf, + use_aws_kms: UseAwsKms, + }, +} + +impl Display for VaultType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "Type: {}", + match &self { + VaultType::DatabaseVault { .. } => "INTERNAL", + VaultType::LocalFileVault { .. } => "EXTERNAL", + } + )?; + if self.use_aws_kms() { + writeln!(f, "Uses AWS KMS: true",)?; + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub enum UseAwsKms { + Yes, + No, +} + +impl UseAwsKms { + pub fn from(b: bool) -> Self { + if b { + UseAwsKms::Yes + } else { + UseAwsKms::No + } + } +} + +impl VaultType { + pub fn database(use_aws_kms: UseAwsKms) -> Self { + VaultType::DatabaseVault { use_aws_kms } + } + + pub fn local_file(path: impl Into, use_aws_kms: UseAwsKms) -> Self { + VaultType::LocalFileVault { + path: path.into(), + use_aws_kms, + } + } + + pub fn path(&self) -> Option<&Path> { + match self { + VaultType::DatabaseVault { .. } => None, + VaultType::LocalFileVault { path, .. } => Some(path.as_path()), + } + } + + pub fn use_aws_kms(&self) -> bool { + match self { + VaultType::DatabaseVault { use_aws_kms } => use_aws_kms == &UseAwsKms::Yes, + VaultType::LocalFileVault { + path: _, + use_aws_kms, + } => use_aws_kms == &UseAwsKms::Yes, + } + } } impl NamedVault { /// Create a new named vault - pub fn new(name: &str, path: PathBuf, is_kms: bool) -> Self { + pub fn new(name: &str, vault_type: VaultType, is_default: bool) -> Self { Self { name: name.to_string(), - path, - is_kms, + vault_type, + is_default, } } @@ -388,33 +490,38 @@ impl NamedVault { self.name.clone() } - /// Return the vault path - pub fn path(&self) -> PathBuf { - self.path.clone() + /// Return the vault type + pub fn vault_type(&self) -> VaultType { + self.vault_type.clone() } - /// Return the vault path as a String - pub fn path_as_string(&self) -> String { - self.path.clone().to_string_lossy().to_string() + /// Return true if this is the default vault + pub fn is_default(&self) -> bool { + self.is_default + } + + /// Return true if an AWS KMS is used to store signing keys + pub fn use_aws_kms(&self) -> bool { + self.vault_type.use_aws_kms() + } + + /// Return the vault path if the vault data is stored in a local file + pub fn path(&self) -> Option<&Path> { + self.vault_type.path() } - /// Return true if this vault is a KMS vault - pub fn is_kms(&self) -> bool { - self.is_kms + /// Return the vault path as a String + pub fn path_as_string(&self) -> Option { + self.vault_type + .path() + .map(|p| p.to_string_lossy().to_string()) } } impl Display for NamedVault { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Name: {}", self.name)?; - writeln!( - f, - "Type: {}", - match self.is_kms { - true => "AWS KMS", - false => "OCKAM", - } - )?; + writeln!(f, "{}", self.vault_type)?; Ok(()) } } @@ -422,15 +529,18 @@ impl Display for NamedVault { impl Output for NamedVault { fn item(&self) -> crate::Result { let mut output = String::new(); - writeln!(output, "Name: {}", self.name())?; + writeln!(output, "Name: {}", self.name)?; writeln!( output, "Type: {}", - match self.is_kms() { - true => "AWS KMS", - false => "OCKAM", + match &self.vault_type { + VaultType::DatabaseVault { .. } => "INTERNAL", + VaultType::LocalFileVault { .. } => "EXTERNAL", } )?; + if self.vault_type.use_aws_kms() { + writeln!(output, "Uses AWS KMS: true",)?; + } Ok(output) } } @@ -451,6 +561,7 @@ mod tests { let cli = CliState::test().await?; // create a vault + // since this is the first one, the data is stored in the database let named_vault1 = cli.get_or_create_named_vault("vault1").await?; let result = cli.get_named_vault("vault1").await?; @@ -458,19 +569,12 @@ mod tests { // another vault cannot be created with the same name let result = cli - .create_named_vault(&Some("vault1".to_string()), &None) + .create_named_vault(Some("vault1".to_string()), None, UseAwsKms::No) .await .ok(); assert_eq!(result, None); - // another vault cannot be created with the same path - let result = cli - .create_named_vault(&None, &Some(named_vault1.path())) - .await - .ok(); - assert_eq!(result, None); - - // the first created vault is the default one if it is the only one + // the first created vault is the default one let result = cli.get_or_create_default_named_vault().await?; assert_eq!(result, named_vault1.clone()); @@ -486,6 +590,19 @@ mod tests { let named_vault2 = cli.get_or_create_named_vault("vault2").await?; + // that vault is using a local file + assert!(named_vault2.path().is_some()); + // another vault cannot be created with the same path + let result = cli + .create_named_vault( + Some("another name".to_string()), + named_vault2.path().map(|p| p.to_path_buf()), + UseAwsKms::No, + ) + .await + .ok(); + assert_eq!(result, None); + let result = cli.get_named_vaults().await?; assert_eq!(result, vec![named_vault1.clone(), named_vault2.clone()]); @@ -539,7 +656,7 @@ mod tests { // if we create a second vault, it can be returned by name let vault2 = cli - .create_named_vault(&Some("vault-2".to_string()), &None) + .create_named_vault(Some("vault-2".to_string()), None, UseAwsKms::No) .await?; let result = cli.get_named_vault_or_default(&Some(vault2.name())).await?; assert_eq!(result, vault2); @@ -578,8 +695,8 @@ mod tests { }; let vault = cli.get_named_vault("vault2").await?; - assert_eq!(vault.path(), new_vault_path); - assert!(vault.path().exists()); + assert_eq!(vault.path(), Some(new_vault_path.as_path())); + assert!(new_vault_path.exists()); Ok(()) } @@ -589,33 +706,36 @@ mod tests { let cli = CliState::test().await?; // the first vault is stored in the main database with the name 'default' - let result = cli.create_named_vault(&None, &None).await?; + let result = cli.create_named_vault(None, None, UseAwsKms::No).await?; assert_eq!(result.name(), DEFAULT_VAULT_NAME.to_string()); - assert!(cli.is_database_path(result.path().as_path())); + assert_eq!(result.vault_type(), VaultType::database(UseAwsKms::No)); // the second vault is stored in a separate file, with a random name // that name is used to create the file name - let result = cli.create_named_vault(&None, &None).await?; + let result = cli.create_named_vault(None, None, UseAwsKms::No).await?; + assert!(result.path().is_some()); assert!(result .path_as_string() + .unwrap() .ends_with(&format!("vault-{}", result.name()))); // a third vault with a name is also stored in a separate file let result = cli - .create_named_vault(&Some("secrets".to_string()), &None) + .create_named_vault(Some("secrets".to_string()), None, UseAwsKms::No) .await?; assert_eq!(result.name(), "secrets".to_string()); - assert!(result.path_as_string().contains("vault-secrets")); + assert!(result.path().is_some()); + assert!(result.path_as_string().unwrap().contains("vault-secrets")); // if we reset, we can check that the first vault gets the user defined name // instead of default cli.reset().await?; let cli = CliState::test().await?; let result = cli - .create_named_vault(&Some("secrets".to_string()), &None) + .create_named_vault(Some("secrets".to_string()), None, UseAwsKms::No) .await?; assert_eq!(result.name(), "secrets".to_string()); - assert!(cli.is_database_path(result.path().as_path())); + assert_eq!(result.vault_type(), VaultType::database(UseAwsKms::No)); Ok(()) } @@ -632,10 +752,14 @@ mod tests { .join(random_name()); let result = cli - .create_named_vault(&Some("secrets".to_string()), &Some(vault_path.clone())) + .create_named_vault( + Some("secrets".to_string()), + Some(vault_path.clone()), + UseAwsKms::No, + ) .await?; assert_eq!(result.name(), "secrets".to_string()); - assert_eq!(result.path(), vault_path); + assert_eq!(result.path(), Some(vault_path.as_path())); Ok(()) } @@ -645,7 +769,7 @@ mod tests { let cli = CliState::test().await?; // create a vault and populate the tables used by the vault - let vault = cli.create_named_vault(&None, &None).await?; + let vault = cli.create_named_vault(None, None, UseAwsKms::No).await?; let purpose_keys_repository = cli.purpose_keys_repository(); let identity = cli.create_identity_with_name("name").await?; diff --git a/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs b/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs index bd390c53324..348d62f1900 100644 --- a/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs +++ b/implementations/rust/ockam/ockam_api/src/ui/output/utils.rs @@ -47,6 +47,15 @@ pub fn colorize_connection_status(status: ConnectionStatus) -> CString { } } +pub fn indent(indent: impl Into, text: impl Into) -> String { + let indent: String = indent.into(); + text.into() + .split('\n') + .map(|line| format!("{indent}{line}")) + .collect::>() + .join("\n") +} + #[cfg(test)] mod tests { use super::*; @@ -57,4 +66,10 @@ mod tests { let result = comma_separated(&data); assert_eq!(result, "a, b, c"); } + + #[test] + fn test_indent() { + let result = indent("---", "line1\nthen line2\n and finally line3"); + assert_eq!(result, "---line1\n---then line2\n--- and finally line3"); + } } diff --git a/implementations/rust/ockam/ockam_command/src/vault/create.rs b/implementations/rust/ockam/ockam_command/src/vault/create.rs index 927b225fa8e..74c75432a4c 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/create.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/create.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use async_trait::async_trait; use clap::Args; use colorful::Colorful; +use ockam_api::cli_state::UseAwsKms; use ockam_api::{fmt_info, fmt_ok}; use ockam_node::Context; @@ -39,19 +40,17 @@ impl Command for CreateCommand { "This is the first vault to be created in this environment. It will be set as the default vault" ))?; } - let vault = if self.aws_kms { - opts.state.create_kms_vault(&self.name, &self.path).await? - } else { - opts.state - .create_named_vault(&self.name, &self.path) - .await? - }; + + let vault = opts + .state + .create_named_vault(self.name, self.path, UseAwsKms::from(self.aws_kms)) + .await?; opts.terminal .stdout() .plain(fmt_ok!("Vault created with name '{}'!", vault.name())) .machine(vault.name()) - .json(serde_json::json!({ "name": &self.name })) + .json(serde_json::json!({ "name": &vault.name() })) .write_line()?; Ok(()) } diff --git a/implementations/rust/ockam/ockam_command/src/vault/util.rs b/implementations/rust/ockam/ockam_command/src/vault/util.rs index fb600d5bb18..e9cc64d5da5 100644 --- a/implementations/rust/ockam/ockam_command/src/vault/util.rs +++ b/implementations/rust/ockam/ockam_command/src/vault/util.rs @@ -2,8 +2,9 @@ use colorful::Colorful; use indoc::formatdoc; use ockam_api::cli_state::vaults::NamedVault; +use ockam_api::cli_state::{UseAwsKms, VaultType}; use ockam_api::colors::OckamColor; -use ockam_api::output::Output; +use ockam_api::output::{indent, Output}; #[derive(serde::Serialize)] pub struct VaultOutput { @@ -27,48 +28,82 @@ impl Output for VaultOutput { Ok(formatdoc!( r#" Vault: - Name: {name} - Type: {vault_type} - Path: {vault_path} + {vault} "#, - name = self - .vault - .name() - .to_string() - .color(OckamColor::PrimaryResource.color()), - vault_type = match self.vault.is_kms() { - true => "AWS KMS", - false => "OCKAM", - } - .to_string() - .color(OckamColor::PrimaryResource.color()), - vault_path = self - .vault - .path_as_string() - .color(OckamColor::PrimaryResource.color()), + vault = indent(" ", self.as_list_item()?) )) } fn as_list_item(&self) -> ockam_api::Result { - Ok(formatdoc!( - r#"Name: {name} + let name = self + .vault + .name() + .to_string() + .color(OckamColor::PrimaryResource.color()); + + let vault_type = if self.vault.path().is_some() { + "External" + } else { + "Internal" + } + .to_string() + .color(OckamColor::PrimaryResource.color()); + + let uses_aws_kms = if self.vault.use_aws_kms() { + "true" + } else { + "false" + } + .to_string() + .color(OckamColor::PrimaryResource.color()); + + Ok(match self.vault.vault_type() { + VaultType::DatabaseVault { + use_aws_kms: UseAwsKms::No, + } => formatdoc!( + r#"Name: {name} + Type: {vault_type}"#, + name = name, + vault_type = vault_type + ), + VaultType::DatabaseVault { + use_aws_kms: UseAwsKms::Yes, + } => formatdoc!( + r#"Name: {name} + Type: {vault_type} + Uses AWS KMS: {uses_aws_kms}"#, + name = name, + uses_aws_kms = uses_aws_kms + ), + VaultType::LocalFileVault { + path, + use_aws_kms: UseAwsKms::No, + } => formatdoc!( + r#"Name: {name} Type: {vault_type} Path: {vault_path}"#, - name = self - .vault - .name() - .to_string() - .color(OckamColor::PrimaryResource.color()), - vault_type = match self.vault.is_kms() { - true => "AWS KMS", - false => "OCKAM", - } - .to_string() - .color(OckamColor::PrimaryResource.color()), - vault_path = self - .vault - .path_as_string() - .color(OckamColor::PrimaryResource.color()), - )) + name = name, + vault_type = vault_type, + vault_path = path + .to_string_lossy() + .to_string() + .color(OckamColor::PrimaryResource.color()) + ), + VaultType::LocalFileVault { + path, + use_aws_kms: UseAwsKms::Yes, + } => formatdoc!( + r#"Name: {name} + Type: External + Path: {vault_path} + Uses AWS KMS: {uses_aws_kms}"#, + name = name, + vault_path = path + .to_string_lossy() + .to_string() + .color(OckamColor::PrimaryResource.color()), + uses_aws_kms = uses_aws_kms, + ), + }) } } diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/database_configuration.rs b/implementations/rust/ockam/ockam_node/src/storage/database/database_configuration.rs index cb54d94be82..187dba88967 100644 --- a/implementations/rust/ockam/ockam_node/src/storage/database/database_configuration.rs +++ b/implementations/rust/ockam/ockam_node/src/storage/database/database_configuration.rs @@ -21,7 +21,10 @@ pub const OCKAM_POSTGRES_PASSWORD: &str = "OCKAM_POSTGRES_PASSWORD"; #[derive(Clone, Debug, PartialEq, Eq)] pub enum DatabaseConfiguration { /// Configuration for a SQLite database - Sqlite(String), + Sqlite { + /// Database file path if the database is stored on disk + path: Option, + }, /// Configuration for a Postgres database Postgres { /// Database host name @@ -103,18 +106,20 @@ impl DatabaseConfiguration { /// Create a local sqlite configuration pub fn sqlite(path: &Path) -> DatabaseConfiguration { - DatabaseConfiguration::Sqlite(Self::create_sqlite_on_disk_connection_string(path)) + DatabaseConfiguration::Sqlite { + path: Some(path.to_path_buf()), + } } /// Create an in-memory sqlite configuration pub fn sqlite_in_memory() -> DatabaseConfiguration { - DatabaseConfiguration::Sqlite(Self::create_sqlite_in_memory_connection_string()) + DatabaseConfiguration::Sqlite { path: None } } /// Return the type of database that has been configured pub fn database_type(&self) -> DatabaseType { match self { - DatabaseConfiguration::Sqlite(_) => DatabaseType::Sqlite, + DatabaseConfiguration::Sqlite { .. } => DatabaseType::Sqlite, DatabaseConfiguration::Postgres { .. } => DatabaseType::Postgres, } } @@ -122,7 +127,12 @@ impl DatabaseConfiguration { /// Return the type of database that has been configured pub fn connection_string(&self) -> String { match self { - DatabaseConfiguration::Sqlite(path) => path.clone(), + DatabaseConfiguration::Sqlite { path: None } => { + Self::create_sqlite_in_memory_connection_string() + } + DatabaseConfiguration::Sqlite { path: Some(path) } => { + Self::create_sqlite_on_disk_connection_string(path) + } DatabaseConfiguration::Postgres { host, port, @@ -139,17 +149,13 @@ impl DatabaseConfiguration { /// Create a directory for the SQLite database file if necessary pub fn create_directory_if_necessary(&self) -> Result<()> { - match self { - DatabaseConfiguration::Sqlite(path) => match PathBuf::from(path).parent() { - Some(parent) => { - if !parent.exists() { - create_dir_all(parent) - .map_err(|e| Error::new(Origin::Api, Kind::Io, e.to_string()))? - } + if let DatabaseConfiguration::Sqlite { path: Some(path) } = self { + if let Some(parent) = path.parent() { + if !parent.exists() { + create_dir_all(parent) + .map_err(|e| Error::new(Origin::Api, Kind::Io, e.to_string()))? } - None => (), - }, - _ => (), + } } Ok(()) } @@ -162,7 +168,7 @@ impl DatabaseConfiguration { /// Return the database path if the database is a SQLite file. pub fn path(&self) -> Option { match self { - DatabaseConfiguration::Sqlite(path) => Some(PathBuf::from(path)), + DatabaseConfiguration::Sqlite { path } => path.clone(), _ => None, } } diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240613100000_create_database.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240613100000_create_database.sql index c8bc5bb36a5..dbbdced58cd 100644 --- a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240613100000_create_database.sql +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/postgres/20240613100000_create_database.sql @@ -85,7 +85,7 @@ CREATE UNIQUE INDEX purpose_key_index ON purpose_key (identifier, purpose); CREATE TABLE vault ( name TEXT PRIMARY KEY, -- User-specified name for a vault - path TEXT NOT NULL, -- Path where the vault is saved, This path can the current database path. In that case the vault data is stored in the *-secrets table below + path TEXT NULL, -- Path where the vault is saved, This path can the current database path. In that case the vault data is stored in the *-secrets table below is_default BOOLEAN, -- boolean indicating if this vault is the default one (0 means true) is_kms BOOLEAN -- boolean indicating if this vault is a KMS one (0 means true). In that case only key handles are stored in the database ); diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20240619100000_database_vault.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20240619100000_database_vault.sql new file mode 100644 index 00000000000..7a88865301b --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/sqlite/20240619100000_database_vault.sql @@ -0,0 +1,17 @@ +-- This migration allows the path column to be NULL +-- This case indicates that the vault 'name' has all its keys stored in the current database +CREATE TABLE new_vault +( + name TEXT PRIMARY KEY, -- User-specified name for a vault + path TEXT NULL, -- Path where the vault is saved + is_default INTEGER, -- boolean indicating if this vault is the default one (1 means true) + is_kms INTEGER -- boolean indicating if this signing keys are stored in an AWS KMS (1 means true) +); + +INSERT INTO new_vault (name, path, is_default, is_kms) +SELECT name, NULL, is_default, is_kms +FROM vault; + +DROP TABLE vault; + +ALTER TABLE new_vault RENAME TO vault; diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs index 90676ed40dd..cb96c29d5e3 100644 --- a/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs +++ b/implementations/rust/ockam/ockam_node/src/storage/database/sqlx_database.rs @@ -249,8 +249,8 @@ impl SqlxDatabase { /// Drop all the database tables pub async fn drop_tables(&self) -> Result<()> { - match self.configuration { - DatabaseConfiguration::Sqlite(_) => { + match self.configuration.database_type() { + DatabaseType::Sqlite => { let tables: Vec = sqlx::query("SELECT name FROM sqlite_master WHERE type='table';") .fetch_all(&*self.pool) @@ -266,7 +266,7 @@ impl SqlxDatabase { } Ok(()) } - DatabaseConfiguration::Postgres { .. } => sqlx::query( + DatabaseType::Postgres => sqlx::query( r#"DO $$ DECLARE r RECORD; @@ -297,10 +297,9 @@ where rethrow("SQLite on disk", f(db)).await?; // only run the postgres tests if the OCKAM_POSTGRES_* environment variables are set - match SqlxDatabase::create_new_postgres().await.ok() { - Some(db) => rethrow("Postgres local", f(db)).await?, - None => (), - } + if let Ok(db) = SqlxDatabase::create_new_postgres().await { + rethrow("Postgres local", f(db)).await? + }; Ok(()) } @@ -319,9 +318,8 @@ where rethrow("SQLite on disk", f(db)).await?; // only run the postgres tests if the OCKAM_POSTGRES_* environment variables are set - match SqlxDatabase::create_new_application_postgres().await.ok() { - Some(db) => rethrow("Postgres local", f(db)).await?, - None => (), + if let Ok(db) = SqlxDatabase::create_new_application_postgres().await { + rethrow("Postgres local", f(db)).await? } Ok(()) } @@ -418,16 +416,13 @@ pub mod tests { /// This is a sanity check to test that we can use Postgres as a database #[tokio::test] async fn test_create_postgres_database() -> Result<()> { - match DatabaseConfiguration::postgres()? { - Some(configuration) => { - let db = SqlxDatabase::create_no_migration(configuration.clone()).await?; - db.drop_tables().await?; + if let Some(configuration) = DatabaseConfiguration::postgres()? { + let db = SqlxDatabase::create_no_migration(configuration.clone()).await?; + db.drop_tables().await?; - let db = SqlxDatabase::create(configuration).await?; - let inserted = insert_identity(&db).await.unwrap(); - assert_eq!(inserted.rows_affected(), 1); - } - None => (), + let db = SqlxDatabase::create(configuration).await?; + let inserted = insert_identity(&db).await.unwrap(); + assert_eq!(inserted.rows_affected(), 1); } Ok(()) }