From 74a34bbe179d8ae1e000cf47e12bf81d20d52692 Mon Sep 17 00:00:00 2001 From: etorreborre Date: Wed, 22 Nov 2023 13:46:28 +0100 Subject: [PATCH] refactor(rust): comment and add tests --- .../ockam_api/src/cli_state/enrollments.rs | 278 ++---------------- .../src/cli_state/enrollments_repository.rs | 30 ++ .../cli_state/enrollments_repository_sql.rs | 252 ++++++++++++++++ .../rust/ockam/ockam_api/src/cli_state/mod.rs | 5 +- .../ockam/ockam_api/src/cli_state/nodes.rs | 43 ++- .../ockam/ockam_api/src/cli_state/projects.rs | 18 +- .../src/cli_state/projects_repository.rs | 24 +- .../src/cli_state/projects_repository_sql.rs | 32 +- .../ockam/ockam_api/src/cli_state/spaces.rs | 20 +- .../src/cli_state/spaces_repository.rs | 19 ++ .../src/cli_state/spaces_repository_sql.rs | 35 ++- .../cli_state/trust_contexts_repository.rs | 18 +- .../trust_contexts_repository_sql.rs | 74 +++-- .../src/cli_state/users_repository.rs | 26 ++ .../src/cli_state/users_repository_sql.rs | 25 +- .../src/nodes/nodes_repository_sql.rs | 118 ++++++-- .../rust/ockam/ockam_app_lib/src/state/mod.rs | 8 +- .../src/state/model_state_repository.rs | 15 + ...itory.rs => model_state_repository_sql.rs} | 29 +- .../storage/change_history_repository_sql.rs | 12 +- .../src/storage/secrets_repository_sql.rs | 15 +- 21 files changed, 687 insertions(+), 409 deletions(-) create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository.rs create mode 100644 implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository_sql.rs create mode 100644 implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs rename implementations/rust/ockam/ockam_app_lib/src/state/{repository.rs => model_state_repository_sql.rs} (87%) diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs index b0bd7f9280a..d32cdeb8aa8 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs @@ -1,14 +1,6 @@ -use std::str::FromStr; -use std::sync::Arc; - -use sqlx::sqlite::SqliteRow; -use sqlx::FromRow; -use sqlx::*; use time::OffsetDateTime; use ockam::identity::Identifier; -use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; -use ockam_core::async_trait; use crate::cli_state::CliState; use crate::cli_state::Result; @@ -18,23 +10,25 @@ impl CliState { let repository = self.enrollment_repository().await?; match name { - Some(name) => repository.is_identity_enrolled(name).await, - None => repository.is_default_identity_enrolled().await, + Some(name) => Ok(repository.is_identity_enrolled(name).await?), + None => Ok(repository.is_default_identity_enrolled().await?), } } pub async fn is_default_identity_enrolled(&self) -> Result { - self.enrollment_repository() + Ok(self + .enrollment_repository() .await? .is_default_identity_enrolled() - .await + .await?) } pub async fn set_identifier_as_enrolled(&self, identifier: &Identifier) -> Result<()> { - self.enrollment_repository() + Ok(self + .enrollment_repository() .await? .set_as_enrolled(identifier) - .await + .await?) } pub async fn set_node_as_enrolled(&self, node_name: &str) -> Result<()> { @@ -48,136 +42,12 @@ impl CliState { ) -> Result> { let repository = self.enrollment_repository().await?; match enrollment_status { - EnrollmentStatus::Enrolled => repository.get_enrolled_identities().await, - EnrollmentStatus::Any => repository.get_all_identities_enrollments().await, + EnrollmentStatus::Enrolled => Ok(repository.get_enrolled_identities().await?), + EnrollmentStatus::Any => Ok(repository.get_all_identities_enrollments().await?), } } } -#[async_trait] -pub trait EnrollmentsRepository: Send + Sync + 'static { - async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()>; - async fn get_enrolled_identities(&self) -> Result>; - async fn get_all_identities_enrollments(&self) -> Result>; - async fn is_default_identity_enrolled(&self) -> Result; - async fn is_identity_enrolled(&self, name: &str) -> Result; -} - -pub struct EnrollmentsSqlxDatabase { - database: Arc, -} - -impl EnrollmentsSqlxDatabase { - pub fn new(database: Arc) -> Self { - debug!("create a repository for enrollments"); - Self { database } - } - - /// Create a new in-memory database - pub async fn create() -> Result> { - Ok(Arc::new(Self::new( - SqlxDatabase::in_memory("enrollments").await?, - ))) - } -} - -#[async_trait] -impl EnrollmentsRepository for EnrollmentsSqlxDatabase { - async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()> { - let query = query("INSERT OR REPLACE INTO identity_enrollment VALUES (?, ?)") - .bind(identifier.to_sql()) - .bind(OffsetDateTime::now_utc().to_sql()); - Ok(query.execute(&self.database.pool).await.void()?) - } - - async fn get_enrolled_identities(&self) -> Result> { - let query = query_as( - r#" - SELECT - identity.identifier, named_identity.name, named_identity.is_default, - identity_enrollment.enrolled_at - FROM identity - INNER JOIN identity_enrollment ON - identity.identifier = identity_enrollment.identifier - INNER JOIN named_identity ON - identity.identifier = named_identity.identifier - "#, - ) - .bind(None as Option); - let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; - result - .into_iter() - .map(|r| r.identity_enrollment()) - .collect::>>() - } - - async fn get_all_identities_enrollments(&self) -> Result> { - let query = query_as( - r#" - SELECT - identity.identifier, named_identity.name, named_identity.is_default, - identity_enrollment.enrolled_at - FROM identity - LEFT JOIN identity_enrollment ON - identity.identifier = identity_enrollment.identifier - INNER JOIN named_identity ON - identity.identifier = named_identity.identifier - "#, - ); - let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; - result - .into_iter() - .map(|r| r.identity_enrollment()) - .collect::>>() - } - - async fn is_default_identity_enrolled(&self) -> Result { - let query = query( - r#" - SELECT - identity_enrollment.enrolled_at - FROM identity - INNER JOIN identity_enrollment ON - identity.identifier = identity_enrollment.identifier - INNER JOIN named_identity ON - identity.identifier = named_identity.identifier - WHERE - named_identity.is_default = ? - "#, - ) - .bind(true.to_sql()); - let result: Option = query - .fetch_optional(&self.database.pool) - .await - .into_core()?; - Ok(result.map(|_| true).unwrap_or(false)) - } - - async fn is_identity_enrolled(&self, name: &str) -> Result { - let query = query( - r#" - SELECT - identity_enrollment.enrolled_at - FROM identity - INNER JOIN identity_enrollment ON - identity.identifier = identity_enrollment.identifier - INNER JOIN named_identity ON - identity.identifier = named_identity.identifier - INNER JOIN named_identity ON - identity.identifier = named_identity.identifier - WHERE - named_identity.name = ? - "#, - ) - .bind(name.to_sql()); - let result: Option = query - .fetch_optional(&self.database.pool) - .await - .into_core()?; - Ok(result.map(|_| true).unwrap_or(false)) - } -} - pub enum EnrollmentStatus { Enrolled, Any, @@ -191,6 +61,19 @@ pub struct IdentityEnrollment { } impl IdentityEnrollment { + pub fn new( + identifier: Identifier, + name: Option, + is_default: bool, + enrolled_at: Option, + ) -> Self { + Self { + identifier, + name, + is_default, + enrolled_at, + } + } pub fn identifier(&self) -> Identifier { self.identifier.clone() } @@ -215,118 +98,3 @@ impl IdentityEnrollment { self.enrolled_at } } - -#[derive(FromRow)] -pub struct EnrollmentRow { - identifier: String, - name: Option, - is_default: bool, - enrolled_at: Option, -} - -impl EnrollmentRow { - fn identity_enrollment(&self) -> Result { - let identifier = Identifier::from_str(self.identifier.as_str())?; - Ok(IdentityEnrollment { - identifier, - name: self.name.clone(), - is_default: self.is_default, - enrolled_at: self.enrolled_at(), - }) - } - - fn enrolled_at(&self) -> Option { - self.enrolled_at - .map(|at| OffsetDateTime::from_unix_timestamp(at).unwrap_or(OffsetDateTime::now_utc())) - } -} - -#[cfg(test)] -mod tests { - use std::path::Path; - - use tempfile::NamedTempFile; - - use ockam::identity::{ChangeHistoryRepository, ChangeHistorySqlxDatabase, Identity}; - - use crate::identity::{IdentitiesRepository, IdentitiesSqlxDatabase}; - - use super::*; - - #[tokio::test] - async fn test_identities_enrollment_repository() -> Result<()> { - let db_file = NamedTempFile::new().unwrap(); - let identity1 = create_identity1(db_file.path(), "identity1").await?; - create_identity2(db_file.path(), "identity2").await?; - let repository = create_repository(db_file.path()).await?; - - // an identity can be enrolled - repository.set_as_enrolled(identity1.identifier()).await?; - - // retrieve the identities and their enrollment status - let result = repository.get_all_identities_enrollments().await?; - assert_eq!(result.len(), 2); - - // retrieve only the enrolled identities - let result = repository.get_enrolled_identities().await?; - assert_eq!(result.len(), 1); - - // the first identity has been set as the default one - let result = repository.is_default_identity_enrolled().await?; - assert!(result); - - Ok(()) - } - - /// HELPERS - async fn create_identity1(path: &Path, name: &str) -> Result { - let identity = Identity::create( - "81a201583ba20101025835a4028201815820530d1c2e9822433b679a66a60b9c2ed47c370cd0ce51cbe1a7ad847b5835a96303f4041a64dd4060051a77a94360028201815840042fff8f6c80603fb1cec4a3cf1ff169ee36889d3ed76184fe1dfbd4b692b02892df9525c61c2f1286b829586d13d5abf7d18973141f734d71c1840520d40a0e", - ) - .await?; - store_identity(path, name, identity).await - } - - async fn create_identity2(path: &Path, name: &str) -> Result { - let identity = Identity::create( - "81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805", - ) - .await?; - store_identity(path, name, identity).await - } - - async fn store_identity(path: &Path, name: &str, identity: Identity) -> Result { - let change_history_repository = create_change_history_repository(path).await?; - let identities_repository = create_identities_repository(path).await?; - change_history_repository - .store_change_history(&identity) - .await?; - - identities_repository - .store_named_identity(identity.identifier(), name, "vault") - .await?; - if name == "identity1" { - identities_repository - .set_as_default_by_identifier(identity.identifier()) - .await?; - } - Ok(identity) - } - - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(EnrollmentsSqlxDatabase::new(Arc::new(db)))) - } - - async fn create_change_history_repository( - path: &Path, - ) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(ChangeHistorySqlxDatabase::new(Arc::new(db)))) - } - - async fn create_identities_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(IdentitiesSqlxDatabase::new(Arc::new(db)))) - } -} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository.rs new file mode 100644 index 00000000000..75c9ca545c0 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository.rs @@ -0,0 +1,30 @@ +use crate::cli_state::enrollments::IdentityEnrollment; +use ockam::identity::Identifier; +use ockam_core::async_trait; +use ockam_core::Result; + +/// This trait stores the enrollment status for identities +/// If an identity has been enrolled it is possible to retrieve: +/// +/// - its name (if it has one) +/// - if this the default identity +/// - if an identity was enrolled and when the local node was informed +/// +/// +#[async_trait] +pub trait EnrollmentsRepository: Send + Sync + 'static { + /// Set the identifier as enrolled, and set a timestamp to record the information + async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()>; + + /// Get the list of enrolled identities + async fn get_enrolled_identities(&self) -> Result>; + + /// Get the enrollment statuses for all known identities + async fn get_all_identities_enrollments(&self) -> Result>; + + /// Return true if the default identity is enrolled + async fn is_default_identity_enrolled(&self) -> Result; + + /// Return true if the identity with the given name is enrolled + async fn is_identity_enrolled(&self, name: &str) -> Result; +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository_sql.rs new file mode 100644 index 00000000000..367fe2b0ab3 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments_repository_sql.rs @@ -0,0 +1,252 @@ +use std::str::FromStr; +use std::sync::Arc; + +use sqlx::sqlite::SqliteRow; +use sqlx::FromRow; +use sqlx::*; +use time::OffsetDateTime; + +use ockam::identity::Identifier; +use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use ockam_core::async_trait; +use ockam_core::Result; + +use crate::cli_state::enrollments::IdentityEnrollment; +use crate::cli_state::enrollments_repository::EnrollmentsRepository; + +pub struct EnrollmentsSqlxDatabase { + database: Arc, +} + +impl EnrollmentsSqlxDatabase { + pub fn new(database: Arc) -> Self { + debug!("create a repository for enrollments"); + Self { database } + } + + /// Create a new in-memory database + #[allow(unused)] + pub async fn create() -> Result> { + Ok(Arc::new(Self::new( + SqlxDatabase::in_memory("enrollments").await?, + ))) + } +} + +#[async_trait] +impl EnrollmentsRepository for EnrollmentsSqlxDatabase { + async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity_enrollment VALUES (?, ?)") + .bind(identifier.to_sql()) + .bind(OffsetDateTime::now_utc().to_sql()); + Ok(query.execute(&self.database.pool).await.void()?) + } + + async fn get_enrolled_identities(&self) -> Result> { + let query = query_as( + r#" + SELECT + identity.identifier, named_identity.name, named_identity.is_default, + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + "#, + ) + .bind(None as Option); + let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identity_enrollment()) + .collect::>>() + } + + async fn get_all_identities_enrollments(&self) -> Result> { + let query = query_as( + r#" + SELECT + identity.identifier, named_identity.name, named_identity.is_default, + identity_enrollment.enrolled_at + FROM identity + LEFT JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + "#, + ); + let result: Vec = query.fetch_all(&self.database.pool).await.into_core()?; + result + .into_iter() + .map(|r| r.identity_enrollment()) + .collect::>>() + } + + async fn is_default_identity_enrolled(&self) -> Result { + let query = query( + r#" + SELECT + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + WHERE + named_identity.is_default = ? + "#, + ) + .bind(true.to_sql()); + let result: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(result.map(|_| true).unwrap_or(false)) + } + + async fn is_identity_enrolled(&self, name: &str) -> Result { + let query = query( + r#" + SELECT + identity_enrollment.enrolled_at + FROM identity + INNER JOIN identity_enrollment ON + identity.identifier = identity_enrollment.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + INNER JOIN named_identity ON + identity.identifier = named_identity.identifier + WHERE + named_identity.name = ? + "#, + ) + .bind(name.to_sql()); + let result: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + Ok(result.map(|_| true).unwrap_or(false)) + } +} + +#[derive(FromRow)] +pub struct EnrollmentRow { + identifier: String, + name: Option, + is_default: bool, + enrolled_at: Option, +} + +impl EnrollmentRow { + fn identity_enrollment(&self) -> Result { + let identifier = Identifier::from_str(self.identifier.as_str())?; + Ok(IdentityEnrollment::new( + identifier, + self.name.clone(), + self.is_default, + self.enrolled_at(), + )) + } + + fn enrolled_at(&self) -> Option { + self.enrolled_at + .map(|at| OffsetDateTime::from_unix_timestamp(at).unwrap_or(OffsetDateTime::now_utc())) + } +} + +#[cfg(test)] +mod tests { + use ockam::identity::{ChangeHistoryRepository, ChangeHistorySqlxDatabase, Identity}; + + use crate::identity::{IdentitiesRepository, IdentitiesSqlxDatabase}; + + use super::*; + + #[tokio::test] + async fn test_identities_enrollment_repository() -> Result<()> { + let db = create_database().await?; + let repository = create_repository(db.clone()); + + // create some identities + let identity1 = create_identity1(db.clone(), "identity1").await?; + create_identity2(db.clone(), "identity2").await?; + + // an identity can be enrolled + repository.set_as_enrolled(identity1.identifier()).await?; + + // retrieve the identities and their enrollment status + let result = repository.get_all_identities_enrollments().await?; + assert_eq!(result.len(), 2); + + // retrieve only the enrolled identities + let result = repository.get_enrolled_identities().await?; + assert_eq!(result.len(), 1); + + // the first identity has been set as the default one when it has been created + // so we should retrieve this information via is_default_identity_enrolled + let result = repository.is_default_identity_enrolled().await?; + assert!(result); + + Ok(()) + } + + /// HELPERS + async fn create_identity1(db: Arc, name: &str) -> Result { + let identity = Identity::create( + "81a201583ba20101025835a4028201815820530d1c2e9822433b679a66a60b9c2ed47c370cd0ce51cbe1a7ad847b5835a96303f4041a64dd4060051a77a94360028201815840042fff8f6c80603fb1cec4a3cf1ff169ee36889d3ed76184fe1dfbd4b692b02892df9525c61c2f1286b829586d13d5abf7d18973141f734d71c1840520d40a0e", + ) + .await?; + store_identity(db, name, identity).await + } + + async fn create_identity2(db: Arc, name: &str) -> Result { + let identity = Identity::create( + "81a201583ba20101025835a4028201815820afbca9cf5d440147450f9f0d0a038a337b3fe5c17086163f2c54509558b62ef403f4041a64dd404a051a77a9434a0282018158407754214545cda6e7ff49136f67c9c7973ec309ca4087360a9f844aac961f8afe3f579a72c0c9530f3ff210f02b7c5f56e96ce12ee256b01d7628519800723805", + ) + .await?; + store_identity(db, name, identity).await + } + + async fn store_identity( + db: Arc, + name: &str, + identity: Identity, + ) -> Result { + let change_history_repository = create_change_history_repository(db.clone()).await?; + let identities_repository = create_identities_repository(db).await?; + change_history_repository + .store_change_history(&identity) + .await?; + + identities_repository + .store_named_identity(identity.identifier(), name, "vault") + .await?; + if name == "identity1" { + identities_repository + .set_as_default_by_identifier(identity.identifier()) + .await?; + } + Ok(identity) + } + + fn create_repository(db: Arc) -> Arc { + Arc::new(EnrollmentsSqlxDatabase::new(db)) + } + + async fn create_database() -> Result> { + Ok(SqlxDatabase::in_memory("enrollments-test").await?) + } + + async fn create_change_history_repository( + db: Arc, + ) -> Result> { + Ok(Arc::new(ChangeHistorySqlxDatabase::new(db))) + } + + async fn create_identities_repository( + db: Arc, + ) -> Result> { + Ok(Arc::new(IdentitiesSqlxDatabase::new(db))) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs b/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs index 3eb9ac10e51..d6ca374f138 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/mod.rs @@ -22,7 +22,8 @@ pub use spaces_repository_sql::*; pub use users::*; pub use crate::cli_state::credentials::*; -use crate::cli_state::enrollments::{EnrollmentsRepository, EnrollmentsSqlxDatabase}; +use crate::cli_state::enrollments_repository::EnrollmentsRepository; +use crate::cli_state::enrollments_repository_sql::EnrollmentsSqlxDatabase; pub use crate::cli_state::nodes::*; pub use crate::cli_state::projects::*; pub use crate::cli_state::spaces::*; @@ -40,6 +41,8 @@ use crate::nodes::{NodesRepository, NodesSqlxDatabase}; pub mod credentials; pub mod enrollments; +mod enrollments_repository; +mod enrollments_repository_sql; pub mod identities; pub mod nodes; pub mod policies; diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs index da79e4c4a0b..698246f66fc 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/nodes.rs @@ -239,10 +239,20 @@ impl CliState { return Ok(()); }; - self.nodes_repository() - .await? - .delete_node(node_name) - .await?; + // remove the node from the database + let repository = self.nodes_repository().await?; + let node_exists = repository.get_node(node_name).await.is_ok(); + repository.delete_node(node_name).await?; + + // set another node as the default node + if node_exists { + let other_nodes = repository.get_nodes().await?; + if let Some(other_node) = other_nodes.first() { + repository.set_default_node(&other_node.name()).await?; + } + } + + // remove the node directory let _ = std::fs::remove_dir_all(self.make_node_dir(node_name)); debug!(name=%node_name, "node deleted"); Ok(()) @@ -384,19 +394,34 @@ mod tests { let cli = CliState::test().await?; // a node can be created with just a name - let node_name = "node-1"; - let _ = cli.create_node(node_name).await?; - cli.remove_node(node_name).await?; + let node1 = "node-1"; + let node_info1 = cli.create_node(node1).await?; + + // the created node is set as the default node + let result = cli.get_default_node().await?; + assert_eq!(result, node_info1); + + // a node can also be removed + // first let's create a second node + let node2 = "node-2"; + let node_info2 = cli.create_node(node2).await?; - let result = cli.get_node(node_name).await.ok(); + // and remove node 1 + cli.remove_node(node1).await?; + + let result = cli.get_node(node1).await.ok(); assert_eq!( result, None, "the node information is not available anymore" ); assert!( - !cli.make_node_dir(node_name).exists(), + !cli.make_node_dir(node1).exists(), "the node directory must be deleted" ); + + // then node 2 should be the default node + let result = cli.get_default_node().await?; + assert_eq!(result, node_info2.set_as_default()); Ok(()) } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs index cebdc17996c..c73e70783e8 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs @@ -66,11 +66,19 @@ impl CliState { } pub async fn delete_project(&self, project_id: &str) -> Result<()> { - Ok(self - .projects_repository() - .await? - .delete_project(project_id) - .await?) + let repository = self.projects_repository().await?; + // delete the project + let project_exists = repository.get_project(project_id).await.is_ok(); + repository.delete_project(project_id).await?; + + // set another project as the default project + if project_exists { + let other_projects = repository.get_projects().await?; + if let Some(other_project) = other_projects.first() { + repository.set_default_project(&other_project.id()).await?; + } + } + Ok(()) } pub async fn get_default_project(&self) -> Result { diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs index b7e6d5fef88..62db3a85379 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository.rs @@ -1,14 +1,36 @@ -use crate::cloud::project::Project; use ockam_core::async_trait; use ockam_core::Result; +use crate::cloud::project::Project; + +/// This trait supports the storage of projects as retrieved from the Controller +/// +/// - in addition to the project data, we can set a project as the default project +/// - a project is identified by its id by default when getting it or setting it as the default +/// #[async_trait] pub trait ProjectsRepository: Send + Sync + 'static { + /// Store a project in the database + /// If the project has already been stored and is updated then we take care of + /// keeping it as the default project if it was before async fn store_project(&self, project: &Project) -> Result<()>; + + /// Return a project given its id async fn get_project(&self, project_id: &str) -> Result>; + + /// Return a project given its name async fn get_project_by_name(&self, name: &str) -> Result>; + + /// Return all the projects async fn get_projects(&self) -> Result>; + + /// Return the default project async fn get_default_project(&self) -> Result>; + + /// Set one project as the default project async fn set_default_project(&self, project_id: &str) -> Result<()>; + + /// Delete a project + /// Return true if the project could be deleted async fn delete_project(&self, project_id: &str) -> Result<()>; } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs index f6135409f8d..ec97fa2fba7 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects_repository_sql.rs @@ -18,6 +18,14 @@ use crate::minicbor_url::Url; use super::ProjectsRepository; +/// The ProjectsSqlxDatabase stores project information in several tables: +/// +/// - project +/// - user_project +/// - user_role +/// - okta_config +/// - confluent_config +/// #[derive(Clone)] pub struct ProjectsSqlxDatabase { database: Arc, @@ -253,10 +261,14 @@ impl ProjectsRepository for ProjectsSqlxDatabase { query("DELETE FROM confluent_config WHERE project_id=?").bind(project_id.to_sql()); query5.execute(&self.database.pool).await.void()?; - transaction.commit().await.void() + transaction.commit().await.void()?; + Ok(()) } } +// Database serialization / deserialization + +/// Low-level representation of a row in the projects table #[derive(sqlx::FromRow)] struct ProjectRow { project_id: String, @@ -311,6 +323,7 @@ impl ProjectRow { } } +/// Low-level representation of a row in the user_project table #[derive(sqlx::FromRow)] struct UserProjectRow { #[allow(unused)] @@ -318,6 +331,7 @@ struct UserProjectRow { user_email: String, } +/// Low-level representation of a row in the user_role table #[derive(sqlx::FromRow)] struct UserRoleRow { user_id: i64, @@ -343,6 +357,7 @@ impl UserRoleRow { } } +/// Low-level representation of a row in the okta_config table #[derive(sqlx::FromRow)] struct OktaConfigRow { #[allow(unused)] @@ -366,6 +381,7 @@ impl OktaConfigRow { } } +/// Low-level representation of a row in the confluent_config table #[derive(sqlx::FromRow)] struct ConfluentConfigRow { #[allow(unused)] @@ -383,16 +399,11 @@ impl ConfluentConfigRow { #[cfg(test)] mod test { - use std::path::Path; - - use tempfile::NamedTempFile; - use super::*; #[tokio::test] async fn test_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; // create and store 2 projects let project1 = create_project( @@ -416,7 +427,7 @@ mod test { repository.store_project(&project1).await?; repository.store_project(&project2).await?; - // retrieve them as a vector or by name + // retrieve them as a list or by name let result = repository.get_projects().await?; assert_eq!(result, vec![project1.clone(), project2.clone()]); @@ -449,9 +460,8 @@ mod test { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(ProjectsSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(ProjectsSqlxDatabase::create().await?) } fn create_project( diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs index c143b6be255..b5538422664 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces.rs @@ -62,11 +62,21 @@ impl CliState { } pub async fn delete_space(&self, space_id: &str) -> Result<()> { - Ok(self - .spaces_repository() - .await? - .delete_space(space_id) - .await?) + let repository = self.spaces_repository().await?; + // delete the space + let space_exists = repository.get_space(space_id).await.is_ok(); + repository.delete_space(space_id).await?; + + // set another space as the default space + if space_exists { + let other_space = repository.get_spaces().await?; + if let Some(other_space) = other_space.first() { + repository + .set_default_space(&other_space.space_id()) + .await?; + } + } + Ok(()) } pub async fn set_space_as_default(&self, space_id: &str) -> Result<()> { diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs index 92752cb2ec1..a5d7fcc0f29 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository.rs @@ -2,12 +2,31 @@ use crate::cloud::space::Space; use ockam_core::async_trait; use ockam_core::Result; +/// This trait supports the storage of spaces as retrieved from the Controller +/// +/// - in addition to the space data, we can set a space as the default space +/// - a space is identified by its id by default when getting it or setting it as the default +/// #[async_trait] pub trait SpacesRepository: Send + Sync + 'static { + /// Store a space async fn store_space(&self, space: &Space) -> Result<()>; + + /// Return a space for a given id + async fn get_space(&self, space_id: &str) -> Result>; + + /// Return a space for a given name async fn get_space_by_name(&self, name: &str) -> Result>; + + /// Return the list of all spaces async fn get_spaces(&self) -> Result>; + + /// Return the default space async fn get_default_space(&self) -> Result>; + + /// Set a space as the default one async fn set_default_space(&self, space_id: &str) -> Result<()>; + + /// Delete a space async fn delete_space(&self, space_id: &str) -> Result<()>; } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs index 08d71939ce6..273e95f5f09 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/spaces_repository_sql.rs @@ -1,7 +1,7 @@ -use sqlx::sqlite::SqliteRow; +use std::sync::Arc; +use sqlx::sqlite::SqliteRow; use sqlx::*; -use std::sync::Arc; use ockam_core::async_trait; use ockam_core::Result; @@ -62,6 +62,21 @@ impl SpacesRepository for SpacesSqlxDatabase { transaction.commit().await.void() } + async fn get_space(&self, space_id: &str) -> Result> { + let query = query("SELECT space_name FROM space WHERE space_id=$1").bind(space_id.to_sql()); + let row: Option = query + .fetch_optional(&self.database.pool) + .await + .into_core()?; + match row { + Some(r) => { + let space_name: String = r.get(0); + self.get_space_by_name(&space_name).await + } + None => Ok(None), + } + } + async fn get_space_by_name(&self, name: &str) -> Result> { let transaction = self.database.begin().await.into_core()?; @@ -149,6 +164,9 @@ impl SpacesRepository for SpacesSqlxDatabase { } } +// Database serialization / deserialization + +/// Low-level representation of a row in the space table #[derive(sqlx::FromRow)] struct SpaceRow { space_id: String, @@ -169,6 +187,7 @@ impl SpaceRow { } } +/// Low-level representation of a row in the user_space table #[derive(sqlx::FromRow)] struct UserSpaceRow { #[allow(unused)] @@ -178,16 +197,11 @@ struct UserSpaceRow { #[cfg(test)] mod test { - use std::path::Path; - - use tempfile::NamedTempFile; - use super::*; #[tokio::test] async fn test_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; // create and store 2 spaces let space1 = Space { @@ -241,8 +255,7 @@ mod test { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(SpacesSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(SpacesSqlxDatabase::create().await?) } } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs index 4e9a5750a93..bc28c762a47 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository.rs @@ -1,13 +1,29 @@ -use crate::cli_state::trust_contexts_repository_sql::NamedTrustContext; use ockam_core::async_trait; use ockam_core::Result; +use crate::cli_state::trust_contexts_repository_sql::NamedTrustContext; + +/// This trait supports the storage of trust context data: +/// +/// - one single trust context can be set as the default one +/// #[async_trait] pub trait TrustContextsRepository: Send + Sync + 'static { + /// Store trust context data associated with a specific trust context name async fn store_trust_context(&self, trust_context: &NamedTrustContext) -> Result<()>; + + /// Get the default named trust context async fn get_default_trust_context(&self) -> Result>; + + /// Set a trust context as the default one async fn set_default_trust_context(&self, name: &str) -> Result<()>; + + /// Get a named trust context by name async fn get_trust_context(&self, name: &str) -> Result>; + + /// Get all named trust contexts async fn get_trust_contexts(&self) -> Result>; + + /// Delete a trust context async fn delete_trust_context(&self, name: &str) -> Result<()>; } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs index a2910735d87..56fd6028178 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust_contexts_repository_sql.rs @@ -124,6 +124,13 @@ impl TrustContextsRepository for TrustContextsSqlxDatabase { } } +/// A NamedTrustContext collects all the data necessary to create a TrustContext +/// under a specific name: +/// +/// Either we can +/// - retrieve a fixed credential +/// - access an authority node to retrieve credentials +/// #[derive(Debug, Clone, PartialEq, Eq)] pub struct NamedTrustContext { name: String, @@ -171,6 +178,7 @@ impl NamedTrustContext { self.credential.clone() } + /// Return the identity of the trust context authority if configured pub async fn authority_identity(&self) -> Result> { match &self.authority_identity { Some(change_history) => Ok(Some( @@ -180,6 +188,7 @@ impl NamedTrustContext { } } + /// Return the identifier of the trust context authority if configured pub async fn authority_identifier(&self) -> Result> { Ok(self .authority_identity() @@ -187,6 +196,8 @@ impl NamedTrustContext { .map(|i| i.identifier().clone())) } + /// Make a TrustContext + /// This requires a transport and secure channels if we need to communicate with an Authority node pub async fn trust_context( &self, tcp_transport: &TcpTransport, @@ -243,6 +254,8 @@ impl NamedTrustContext { )) } + /// Return access data for an authority in order to be able to create + /// a RPC client to that authority and obtain credentials pub async fn authority(&self) -> Result> { match ( self.authority_identifier().await?, @@ -254,6 +267,29 @@ impl NamedTrustContext { } } +/// Configuration of an authority node +#[derive(Clone)] +pub struct Authority { + identifier: Identifier, + route: MultiAddr, +} + +impl Authority { + pub fn new(identifier: Identifier, route: MultiAddr) -> Self { + Self { identifier, route } + } + + pub fn identifier(&self) -> Identifier { + self.identifier.clone() + } + + pub fn route(&self) -> MultiAddr { + self.route.clone() + } +} + +// Database serialization / deserialization + #[derive(sqlx::FromRow)] struct NamedTrustContextRow { name: String, @@ -303,32 +339,9 @@ impl NamedTrustContextRow { } } -#[derive(Clone)] -pub struct Authority { - identifier: Identifier, - route: MultiAddr, -} - -impl Authority { - pub fn new(identifier: Identifier, route: MultiAddr) -> Self { - Self { identifier, route } - } - - pub fn identifier(&self) -> Identifier { - self.identifier.clone() - } - - pub fn route(&self) -> MultiAddr { - self.route.clone() - } -} - #[cfg(test)] mod test { use core::time::Duration; - use std::path::Path; - - use tempfile::NamedTempFile; use ockam::identity::models::CredentialSchemaIdentifier; use ockam::identity::utils::AttributesBuilder; @@ -338,8 +351,9 @@ mod test { #[tokio::test] async fn test_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; + + // create 2 trust contexts let identities = identities().await?; let issuer_identifier = identities.identities_creation().create_identity().await?; let issuer = identities.get_identity(&issuer_identifier).await?; @@ -350,24 +364,29 @@ mod test { repository.store_trust_context(&trust_context1).await?; repository.store_trust_context(&trust_context2).await?; + // get a trust context by name let result = repository.get_trust_context("trust-context-1").await?; assert_eq!(result, Some(trust_context1.clone())); + // get all the trust contexts let result = repository.get_trust_contexts().await?; assert_eq!(result, vec![trust_context1.clone(), trust_context2.clone()]); + // set the first trust context as the default trust context repository .set_default_trust_context("trust-context-1") .await?; let result = repository.get_default_trust_context().await?; assert_eq!(result, Some(trust_context1)); + // then set the second one repository .set_default_trust_context("trust-context-2") .await?; let result = repository.get_default_trust_context().await?; assert_eq!(result, Some(trust_context2.clone())); + // a trust context can be deleted repository.delete_trust_context("trust-context-1").await?; let result = repository.get_trust_contexts().await?; assert_eq!(result, vec![trust_context2]); @@ -375,9 +394,8 @@ mod test { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(TrustContextsSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(TrustContextsSqlxDatabase::create().await?) } async fn create_trust_context( diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs index 4d22a914523..42f50bc2a90 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository.rs @@ -2,12 +2,38 @@ use crate::cloud::enroll::auth0::UserInfo; use ockam_core::async_trait; use ockam_core::Result; +/// This traits allows user information to be stored locally. +/// User information is retrieved when a user has been authenticated. +/// It contains fields like: +/// +/// - name +/// - sub(ject) unique identifier +/// - email +/// - etc... +/// +/// Even if there is a sub field supposed to uniquely identify a user we currently use +/// the user email for this. +/// +/// A user can also be set as the default user via this repository. +/// #[async_trait] pub trait UsersRepository: Send + Sync + 'static { + /// Store (or update) some information + /// In case of an update, if the user was already the default user, it will stay the default user async fn store_user(&self, user: &UserInfo) -> Result<()>; + + /// Return the default user async fn get_default_user(&self) -> Result>; + + /// Set a user as the default one async fn set_default_user(&self, email: &str) -> Result<()>; + + /// Return a user given their email async fn get_user(&self, email: &str) -> Result>; + + /// Get the list of all users async fn get_users(&self) -> Result>; + + /// Delete a user given their email async fn delete_user(&self, email: &str) -> Result<()>; } diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs index 72803695d23..6c557c777c3 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/users_repository_sql.rs @@ -1,14 +1,16 @@ +use std::sync::Arc; + use sqlx::sqlite::SqliteRow; use sqlx::*; -use std::sync::Arc; - -use super::UsersRepository; -use crate::cloud::enroll::auth0::UserInfo; use ockam_core::async_trait; use ockam_core::Result; use ockam_node::database::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; +use crate::cloud::enroll::auth0::UserInfo; + +use super::UsersRepository; + #[derive(Clone)] pub struct UsersSqlxDatabase { database: Arc, @@ -89,6 +91,9 @@ impl UsersRepository for UsersSqlxDatabase { } } +// Database serialization / deserialization + +/// Low-level representation of a row in the user table #[derive(sqlx::FromRow)] struct UserRow { email: String, @@ -118,16 +123,11 @@ impl UserRow { #[cfg(test)] mod test { - use std::path::Path; - - use tempfile::NamedTempFile; - use super::*; #[tokio::test] async fn test_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; // create and store 2 users let user1 = UserInfo { @@ -175,8 +175,7 @@ mod test { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(UsersSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(UsersSqlxDatabase::create().await?) } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs index a72c23b45da..6b7e1583b3b 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/nodes_repository_sql.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use sqlx::sqlite::SqliteRow; use sqlx::*; +use sysinfo::{Pid, ProcessExt, ProcessStatus, System, SystemExt}; use ockam::identity::Identifier; use ockam::{FromSqlxError, SqlxDatabase, ToSqlxType, ToVoid}; @@ -10,26 +11,61 @@ use ockam_core::async_trait; use ockam_core::errcode::{Kind, Origin}; use ockam_core::Result; use ockam_multiaddr::MultiAddr; -use sysinfo::{Pid, ProcessExt, ProcessStatus, System, SystemExt}; use crate::config::lookup::InternetAddress; +/// This trait supports the storage of node data: +/// +/// - a node has a unique name +/// - a node is always associated to an identifier +/// - a node can be associated to a (single) project +/// - when a node is running we can persist its process id and its TCP listener address +/// - one of the nodes is always set as the default node +/// - a node can be set as an authority node. The purpose of this flag is to be able to display +/// the node status without being able to start a TCP connection since the TCP listener might not be accessible +/// #[async_trait] pub trait NodesRepository: Send + Sync + 'static { + /// Store or update the information about a node async fn store_node(&self, node_info: &NodeInfo) -> Result<()>; + + /// Get the list of all the nodes async fn get_nodes(&self) -> Result>; + + /// Get a node by name async fn get_node(&self, node_name: &str) -> Result>; + + /// Get a node by its associated identifier async fn get_node_by_identifier(&self, identifier: &Identifier) -> Result>; + + /// Get the node set as default async fn get_default_node(&self) -> Result>; + + /// Set a node set the default node async fn set_default_node(&self, node_name: &str) -> Result<()>; + + /// Return true if a node with the given name is the default node async fn is_default_node(&self, node_name: &str) -> Result; + + /// Delete a node given its name async fn delete_node(&self, node_name: &str) -> Result<()>; - async fn delete_default_node(&self) -> Result<()>; + + /// Set the TCP listener of a node async fn set_tcp_listener_address(&self, node_name: &str, address: &str) -> Result<()>; + + /// Get the TCP listener of a node async fn get_tcp_listener_address(&self, node_name: &str) -> Result>; + + /// Set the process id of a node async fn set_node_pid(&self, node_name: &str, pid: u32) -> Result<()>; + + /// Unset the process id of a node async fn set_no_node_pid(&self, node_name: &str) -> Result<()>; + + /// Associate a node to a project async fn set_node_project_name(&self, node_name: &str, project_name: &str) -> Result<()>; + + /// Return the name of the project associated to a node async fn get_node_project_name(&self, node_name: &str) -> Result>; } @@ -131,11 +167,6 @@ impl NodesRepository for NodesSqlxDatabase { query.execute(&self.database.pool).await.void() } - async fn delete_default_node(&self) -> Result<()> { - let query = query("DELETE FROM node WHERE is_default=?").bind(true.to_sql()); - query.execute(&self.database.pool).await.void() - } - async fn set_tcp_listener_address(&self, node_name: &str, address: &str) -> Result<()> { let query = query("UPDATE node SET tcp_listener_address = ? WHERE name = ?") .bind(address.to_sql()) @@ -181,11 +212,13 @@ impl NodesRepository for NodesSqlxDatabase { } } +/// This struct contains all the data associated to a node #[derive(Debug, PartialEq, Eq, Clone)] pub struct NodeInfo { name: String, identifier: Identifier, verbosity: u8, + // this is used when restarting the node to determine its logging level is_default: bool, is_authority: bool, tcp_listener_address: Option, @@ -228,6 +261,13 @@ impl NodeInfo { self.is_default } + /// Return a copy of this node with the is_default flag set to true + pub fn set_as_default(&self) -> Self { + let mut result = self.clone(); + result.is_default = true; + result + } + pub fn is_authority_node(&self) -> bool { self.is_authority } @@ -255,6 +295,7 @@ impl NodeInfo { self.pid } + /// Return true if there is a running process corresponding to the node process id pub fn is_running(&self) -> bool { if let Some(pid) = self.pid() { let mut sys = System::new(); @@ -273,6 +314,8 @@ impl NodeInfo { } } +// Database serialization / deserialization + #[derive(FromRow)] pub(crate) struct NodeRow { name: String, @@ -311,20 +354,16 @@ impl NodeRow { #[cfg(test)] mod test { - use std::path::Path; - - use tempfile::NamedTempFile; - use super::*; #[tokio::test] async fn test_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; let identifier = Identifier::from_str("I521f58f591f0dcc69c5a849fd7a93823474aef31").unwrap(); - let node_info = NodeInfo::new( - "node_name".to_string(), + // The information about a node can be stored + let node_info1 = NodeInfo::new( + "node1".to_string(), identifier.clone(), 0, false, @@ -333,24 +372,52 @@ mod test { Some(1234), ); - repository.store_node(&node_info).await?; - let result = repository.get_nodes().await?; - assert_eq!(result, vec![node_info.clone()]); + repository.store_node(&node_info1).await?; // get the node by name - let result = repository.get_node("node_name").await?; - assert_eq!(result, Some(node_info.clone())); + let result = repository.get_node("node1").await?; + assert_eq!(result, Some(node_info1.clone())); // get the node by identifier let result = repository.get_node_by_identifier(&identifier).await?; - assert_eq!(result, Some(node_info)); + assert_eq!(result, Some(node_info1.clone())); + + // the list of all the nodes can be retrieved + let node_info2 = NodeInfo::new( + "node2".to_string(), + identifier.clone(), + 0, + false, + false, + None, + Some(5678), + ); + + repository.store_node(&node_info2).await?; + let result = repository.get_nodes().await?; + assert_eq!(result, vec![node_info1.clone(), node_info2.clone()]); + + // a node can be set as the default + repository.set_default_node("node2").await?; + let result = repository.get_default_node().await?; + assert_eq!(result, Some(node_info2.set_as_default())); + + // a node can be deleted + repository.delete_node("node2").await?; + let result = repository.get_nodes().await?; + assert_eq!(result, vec![node_info1.clone()]); + + // in that case there is no more default node + let result = repository.get_default_node().await?; + assert!(result.is_none()); Ok(()) } #[tokio::test] async fn test_node_project() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; + + // a node can be associated to a project name repository .set_node_project_name("node_name", "project1") .await?; @@ -361,8 +428,7 @@ mod test { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(NodesSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(NodesSqlxDatabase::create().await?) } } diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs index 3f72467e16a..a97259aac3c 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs @@ -6,6 +6,7 @@ use tokio::sync::RwLock; use tracing::{error, info, trace, warn}; use tracing_appender::non_blocking::WorkerGuard; +pub use kind::StateKind; use ockam::identity::Identifier; use ockam::Context; use ockam::{NodeBuilder, TcpListenerOptions, TcpTransport}; @@ -31,16 +32,17 @@ use crate::incoming_services::IncomingServicesState; use crate::invitations::state::{InvitationState, ReceivedInvitationStatus}; use crate::scheduler::Scheduler; pub(crate) use crate::state::model::ModelState; -pub(crate) use crate::state::repository::{ModelStateRepository, ModelStateSqlxDatabase}; +use crate::state::model_state_repository::ModelStateRepository; +pub(crate) use crate::state::model_state_repository_sql::ModelStateSqlxDatabase; use crate::state::tasks::{ RefreshInletsTask, RefreshInvitationsTask, RefreshProjectsTask, RefreshRelayTask, }; use crate::{api, Result}; -pub use kind::StateKind; mod kind; mod model; -mod repository; +mod model_state_repository; +mod model_state_repository_sql; mod tasks; pub const NODE_NAME: &str = "ockam_app"; diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs new file mode 100644 index 00000000000..bbb0d79ff7a --- /dev/null +++ b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository.rs @@ -0,0 +1,15 @@ +use ockam_core::async_trait; + +use crate::state::model::ModelState; +use crate::Result; + +/// The ModelStateRepository is responsible for storing and loading +/// the persistent data managed by the desktop application. +#[async_trait] +pub trait ModelStateRepository: Send + Sync + 'static { + /// Store / update the full model state in the database + async fn store(&self, model_state: &ModelState) -> Result<()>; + + /// Load the model state from the database + async fn load(&self) -> Result; +} diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository_sql.rs similarity index 87% rename from implementations/rust/ockam/ockam_app_lib/src/state/repository.rs rename to implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository_sql.rs index d0f31f4020d..1415f897f0c 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/repository.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/model_state_repository_sql.rs @@ -14,18 +14,9 @@ use ockam_core::{async_trait, Address}; use crate::incoming_services::PersistentIncomingService; use crate::state::model::ModelState; +use crate::state::model_state_repository::ModelStateRepository; use crate::Result; -/// The ModelStateRepository is responsible for storing and loading -/// ModelState data (user information, shared services etc...) -/// The state must be stored everytime it is modified (see set_user_info in AppState for example) -/// so that it can be loaded again when the application starts up -#[async_trait] -pub trait ModelStateRepository: Send + Sync + 'static { - async fn store(&self, model_state: &ModelState) -> Result<()>; - async fn load(&self) -> Result; -} - #[derive(Clone)] pub struct ModelStateSqlxDatabase { database: Arc, @@ -101,6 +92,9 @@ impl ModelStateRepository for ModelStateSqlxDatabase { } } +// Database serialization / deserialization + +/// Low-level representation of a row in the tcp_outlet_status table #[derive(sqlx::FromRow)] struct TcpOutletStatusRow { alias: String, @@ -123,6 +117,7 @@ impl TcpOutletStatusRow { } } +/// Low-level representation of a row in the incoming_service table #[derive(sqlx::FromRow)] struct PersistentIncomingServiceRow { invitation_id: String, @@ -142,10 +137,6 @@ impl PersistentIncomingServiceRow { #[cfg(test)] mod tests { - use std::path::Path; - - use tempfile::NamedTempFile; - use ockam_api::nodes::models::portal::OutletStatus; use ockam_core::Address; @@ -153,8 +144,7 @@ mod tests { #[tokio::test] async fn store_and_load() -> Result<()> { - let db_file = NamedTempFile::new()?; - let repository = create_repository(db_file.path()).await?; + let repository = create_repository().await?; let mut state = ModelState::default(); repository.store(&state).await?; @@ -196,7 +186,7 @@ mod tests { assert_eq!(state, loaded); // Reload from DB scratch to emulate an app restart - let repository = create_repository(db_file.path()).await?; + let repository = create_repository().await?; let loaded = repository.load().await?; assert_eq!(state.tcp_outlets.len(), 5); assert_eq!(state.incoming_services.len(), 1); @@ -206,8 +196,7 @@ mod tests { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(ModelStateSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(ModelStateSqlxDatabase::create().await?) } } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs index 3aa40e4497e..d57b9141380 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/change_history_repository_sql.rs @@ -105,10 +105,6 @@ impl ChangeHistoryRow { #[cfg(test)] mod tests { - use std::path::Path; - - use tempfile::NamedTempFile; - use crate::Identity; use super::*; @@ -117,8 +113,7 @@ mod tests { async fn test_identities_repository() -> Result<()> { let identity1 = create_identity1().await?; let identity2 = create_identity2().await?; - let db_file = NamedTempFile::new().unwrap(); - let repository = create_repository(db_file.path()).await?; + let repository = create_repository().await?; // store and retrieve an identity repository.store_change_history(&identity1).await?; @@ -167,8 +162,7 @@ mod tests { .await } - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(ChangeHistorySqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(ChangeHistorySqlxDatabase::create().await?) } } diff --git a/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs index 3e25f05d851..36a4037d259 100644 --- a/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs +++ b/implementations/rust/ockam/ockam_vault/src/storage/secrets_repository_sql.rs @@ -246,16 +246,11 @@ impl X25519SecretRow { #[cfg(test)] mod test { - use std::path::Path; - - use tempfile::NamedTempFile; - use super::*; #[tokio::test] async fn test_signing_secrets_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; let handle1 = SigningSecretKeyHandle::ECDSASHA256CurveP256(HandleToSecret::new(vec![1, 2, 3])); @@ -288,8 +283,7 @@ mod test { #[tokio::test] async fn test_x25519_secrets_repository() -> Result<()> { - let file = NamedTempFile::new().unwrap(); - let repository = create_repository(file.path()).await?; + let repository = create_repository().await?; let handle1 = X25519SecretKeyHandle(HandleToSecret::new(vec![1, 2, 3])); let secret1 = X25519SecretKey::new([1; 32]); @@ -319,8 +313,7 @@ mod test { } /// HELPERS - async fn create_repository(path: &Path) -> Result> { - let db = SqlxDatabase::create(path).await?; - Ok(Arc::new(SecretsSqlxDatabase::new(Arc::new(db)))) + async fn create_repository() -> Result> { + Ok(SecretsSqlxDatabase::create().await?) } }