diff --git a/Cargo.toml b/Cargo.toml index 2efd5beef49ef9b02ecbc6ab9f45e7dbd743a323..3f6a7ef07d640dc50d408e9cf418d879db54b049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ repository = "https://gitee.com/openeuler/signatrust" clap = { version = "4.0.22", features = ["derive", "env"] } config = "0.13.3" lazy_static = "1.4.0" -actix-web = { version = "4.3.0", features = ["openssl"]} +actix-web = { version = "4.4.0", features = ["openssl"]} actix-web-lab = "0.19.1" tonic = {version = "0.8.2", features = ["tls", "tls-roots", "transport", "channel"]} prost = "0.11.0" @@ -36,9 +36,9 @@ notify = { version = "6.0.0", default-features = false, features = ["macos_kqueu env_logger = "0.10.0" anyhow = "1.0.66" thiserror = "1.0.38" -sqlx = { version = "0.6.3", features = ["migrate", "mysql", "runtime-tokio-rustls", "chrono"] } +sqlx = { version = "0.7.1", features = ["migrate", "mysql", "runtime-tokio-rustls", "chrono"] } once_cell = "1.16.0" -reqwest = { version = "0.11.13", features=["json"]} +reqwest = { version = "0.11.20", features=["json"]} serde_json = "1.0.91" serde_urlencoded = "0.7.1" serde = "1.0.151" @@ -77,12 +77,14 @@ regex = "1" csrf= "0.4.1" data-encoding= "2.4.0" enum-iterator= "1.4.1" +sea-orm = { version = "0.12.2", features = [ "sqlx-mysql", "runtime-tokio-rustls", "macros", "with-chrono"] } [build-dependencies] tonic-build = "0.8.4" [dev-dependencies] -mockito = "1.0.2" +mockito = "1.1.0" +sea-orm = { version = "0.12.2", features = [ "mock"] } [[bin]] name = "client" diff --git a/scripts/initialize-user-and-keys.sh b/scripts/initialize-user-and-keys.sh index e65349bee3f71883d8ddb2cf60af05940d152fd4..98bf99e9b6fa612f6e1aec60122873ca55948e47 100755 --- a/scripts/initialize-user-and-keys.sh +++ b/scripts/initialize-user-and-keys.sh @@ -48,6 +48,11 @@ function create_default_openpgp_eddsa { RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-pgp-eddsa --description "used for test purpose only" --key-type pgp --email tommylikehu@gmail.com --param-key-type eddsa --param-pgp-email infra@openeuler.org --param-pgp-passphrase husheng1234 --digest-algorithm sha2_256 --visibility public } +function create_default_private_openpgp_rsa { + echo "start to create default openpgp keys identified with default-pgp" + RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-pgp-rsa --description "used for test purpose only" --key-type pgp --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 --param-pgp-email infra@openeuler.org --param-pgp-passphrase husheng1234 --digest-algorithm sha2_256 --visibility private +} + echo "Preparing basic keys for signatrust......" @@ -67,3 +72,4 @@ create_default_openpgp_rsa create_default_openpgp_eddsa +create_default_private_openpgp_rsa diff --git a/src/application/datakey.rs b/src/application/datakey.rs index 123d5433797d88943779b51f50b0e00dfc1c123f..7740ad146b455cc7cb300c6f0f5ba27f63d00f01 100644 --- a/src/application/datakey.rs +++ b/src/application/datakey.rs @@ -35,6 +35,8 @@ pub trait KeyService: Send + Sync{ async fn create(&self, user: UserIdentity, data: &mut DataKey) -> Result; async fn import(&self, data: &mut DataKey) -> Result; async fn get_by_name(&self, name: &str) -> Result; + + async fn check_name_exists(&self, name: &str) -> Result; async fn get_all(&self, key_type: Option, visibility: Visibility, user_id: i32) -> Result>; async fn get_one(&self, user: Option, id_or_name: String) -> Result; //get keys content @@ -212,6 +214,10 @@ where self.repository.get_by_name(name).await } + async fn check_name_exists(&self, name: &str) -> Result { + self.repository.check_name_exists(name).await + } + async fn get_all(&self, key_type: Option, visibility: Visibility, user_id: i32) -> Result> { self.repository.get_all_keys(key_type, visibility, user_id).await } diff --git a/src/domain/clusterkey/entity.rs b/src/domain/clusterkey/entity.rs index f038edf75120ba185087fd51223c8e3337ce32e8..564735b16391676413e67dfac3dd2d9abdc18c0c 100644 --- a/src/domain/clusterkey/entity.rs +++ b/src/domain/clusterkey/entity.rs @@ -22,7 +22,7 @@ use std::vec::Vec; use crate::domain::kms_provider::KMSProvider; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct ClusterKey { pub id: i32, pub data: Vec, @@ -89,7 +89,6 @@ impl Default for SecClusterKey { } } - impl SecClusterKey { pub async fn load(cluster_key: ClusterKey, kms_provider: &Box) -> Result where K: KMSProvider + ?Sized { diff --git a/src/domain/datakey/entity.rs b/src/domain/datakey/entity.rs index 79af46d6917787fa7a97715d4622512f141b7fd3..1f2f67e6a4c9f2dd19395f1a85c11df352c4e40b 100644 --- a/src/domain/datakey/entity.rs +++ b/src/domain/datakey/entity.rs @@ -223,7 +223,7 @@ impl Display for X509RevokeReason { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ParentKey { pub name: String, pub private_key: Vec, @@ -232,7 +232,7 @@ pub struct ParentKey { pub attributes: HashMap, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct RevokedKey { pub id: i32, pub key_id: i32, @@ -242,7 +242,7 @@ pub struct RevokedKey { pub serial_number: Option } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct DataKey { pub id: i32, pub name: String, diff --git a/src/domain/datakey/repository.rs b/src/domain/datakey/repository.rs index ae6fc34d943b061fae7c8f46965e0ba866ac5547..e4f083e52abbccb05200b88640ddfdb8d5c80804 100644 --- a/src/domain/datakey/repository.rs +++ b/src/domain/datakey/repository.rs @@ -27,6 +27,7 @@ pub trait Repository: Send + Sync { async fn get_all_keys(&self, key_type: Option, visibility: Visibility, user_id: i32) -> Result>; async fn get_by_id(&self, id: i32) -> Result; async fn get_by_name(&self, name: &str) -> Result; + async fn check_name_exists(&self, name: &str) -> Result; async fn update_state(&self, id: i32, state: KeyState) -> Result<()>; async fn update_key_data(&self, data_key: DataKey) -> Result<()>; async fn get_enabled_key_by_type_and_name(&self, key_type: String, name: String) -> Result; diff --git a/src/domain/token/entity.rs b/src/domain/token/entity.rs index d6087bca26bebef743883ecb4bae05d59e858ed3..6b65e2c12f9691f78de5db3551543dd4278443f0 100644 --- a/src/domain/token/entity.rs +++ b/src/domain/token/entity.rs @@ -21,7 +21,7 @@ use std::fmt::{Display, Formatter}; const TOKEN_EXPIRE_IN_DAYS: i64 = 180; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Token { pub id: i32, pub user_id: i32, diff --git a/src/domain/user/entity.rs b/src/domain/user/entity.rs index 0541e37954337a290315dce28d48ea45e2229275..ad72d75aded07ed8e9b2773260866685c9165648 100644 --- a/src/domain/user/entity.rs +++ b/src/domain/user/entity.rs @@ -19,7 +19,7 @@ use std::fmt::{Display, Formatter}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct User { pub id: i32, pub email: String diff --git a/src/infra/database/model/clusterkey/dto.rs b/src/infra/database/model/clusterkey/dto.rs index 659268ca21e9b629711e7c16deabf9fdd0bbbfbd..d16183f9deb245efb08f350685e449e95b839957 100644 --- a/src/infra/database/model/clusterkey/dto.rs +++ b/src/infra/database/model/clusterkey/dto.rs @@ -18,10 +18,13 @@ use crate::domain::clusterkey::entity::ClusterKey; use sqlx::types::chrono; -use sqlx::FromRow; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Debug, FromRow)] -pub(super) struct ClusterKeyDTO { +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "cluster_key")] +pub struct Model { + #[sea_orm(primary_key)] pub id: i32, pub data: Vec, pub algorithm: String, @@ -29,8 +32,13 @@ pub(super) struct ClusterKeyDTO { pub create_at: chrono::DateTime, } -impl From for ClusterKey { - fn from(dto: ClusterKeyDTO) -> Self { +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} + +impl From for ClusterKey { + fn from(dto: Model) -> Self { ClusterKey { id: dto.id, data: dto.data, @@ -41,44 +49,14 @@ impl From for ClusterKey { } } -impl From for ClusterKeyDTO { - fn from(cluster_key: ClusterKey) -> Self { - Self { - id: cluster_key.id, - data: cluster_key.data, - algorithm: cluster_key.algorithm, - identity: cluster_key.identity, - create_at: cluster_key.create_at, - } - } -} - #[cfg(test)] mod tests { use chrono::Utc; - use super::{ClusterKey,ClusterKeyDTO}; - - #[test] - fn test_cluster_key_dto_from_entity() { - let key = ClusterKey { - id: 1, - data: vec![1, 2, 3], - algorithm: "algo".to_string(), - identity: "id".to_string(), - create_at: Utc::now() - }; - let create_at = key.create_at.clone(); - let dto = ClusterKeyDTO::from(key); - assert_eq!(dto.id, 1); - assert_eq!(dto.data, vec![1, 2, 3]); - assert_eq!(dto.algorithm, "algo"); - assert_eq!(dto.identity, "id"); - assert_eq!(dto.create_at, create_at); - } + use super::{ClusterKey,Model}; #[test] fn test_cluster_key_entity_from_dto() { - let dto = ClusterKeyDTO { + let dto = Model { id: 1, data: vec![1, 2, 3], algorithm: "algo".to_string(), diff --git a/src/infra/database/model/clusterkey/repository.rs b/src/infra/database/model/clusterkey/repository.rs index e34357e162fc68af1c56858edc33ca64b871faa6..edf833427039a8367a60ca393c0a988fbbf8c41e 100644 --- a/src/infra/database/model/clusterkey/repository.rs +++ b/src/infra/database/model/clusterkey/repository.rs @@ -14,67 +14,226 @@ * */ -use super::dto::ClusterKeyDTO; -use crate::infra::database::pool::DbPool; +use super::dto::Entity as ClusterKeyDTO; +use crate::infra::database::model::clusterkey; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, ActiveValue::Set, QueryOrder}; use crate::domain::clusterkey::entity::ClusterKey; use crate::domain::clusterkey::repository::Repository; -use crate::util::error::Result; +use crate::util::error::{Result, Error}; use async_trait::async_trait; -use std::boxed::Box; +use sea_orm::sea_query::OnConflict; #[derive(Clone)] -pub struct ClusterKeyRepository { - db_pool: DbPool, +pub struct ClusterKeyRepository<'a> { + db_connection: &'a DatabaseConnection, } -impl ClusterKeyRepository { - pub fn new(db_pool: DbPool) -> Self { +impl<'a> ClusterKeyRepository<'a> { + pub fn new(db_connection: &'a DatabaseConnection) -> Self { Self { - db_pool, + db_connection, } } } #[async_trait] -impl Repository for ClusterKeyRepository { +impl<'a> Repository for ClusterKeyRepository<'a> { async fn create(&self, cluster_key: ClusterKey) -> Result<()> { - let dto = ClusterKeyDTO::from(cluster_key); - let _ : Option = sqlx::query_as("INSERT IGNORE INTO cluster_key(data, algorithm, identity, create_at) VALUES (?, ?, ?, ?)") - .bind(&dto.data) - .bind(&dto.algorithm) - .bind(&dto.identity) - .bind(dto.create_at) - .fetch_optional(&self.db_pool) - .await?; + let cluster_key = clusterkey::dto::ActiveModel { + data: Set(cluster_key.data), + algorithm: Set(cluster_key.algorithm), + identity: Set(cluster_key.identity), + create_at: Set(cluster_key.create_at), + ..Default::default() + }; + //TODO: https://github.com/SeaQL/sea-orm/issues/1790 + ClusterKeyDTO::insert(cluster_key).on_conflict(OnConflict::new() + .update_column(clusterkey::dto::Column::Id).to_owned() + ).exec(self.db_connection).await?; Ok(()) } async fn get_latest(&self, algorithm: &str) -> Result> { - let latest: Option = sqlx::query_as( - "SELECT * FROM cluster_key WHERE algorithm = ? ORDER BY id DESC LIMIT 1", - ) - .bind(algorithm) - .fetch_optional(&self.db_pool) - .await?; - match latest { - Some(l) => return Ok(Some(ClusterKey::from(l))), - None => Ok(None), + match ClusterKeyDTO::find().filter( + clusterkey::dto::Column::Algorithm.eq(algorithm) + ).order_by_desc(clusterkey::dto::Column::Id).one( + self.db_connection).await? { + None => { + Ok(None) + } + Some(cluster_key) => { + Ok(Some(ClusterKey::from(cluster_key))) + } } } async fn get_by_id(&self, id: i32) -> Result { - let selected: ClusterKeyDTO = sqlx::query_as("SELECT * FROM cluster_key WHERE id = ?") - .bind(id) - .fetch_one(&self.db_pool) - .await?; - Ok(ClusterKey::from(selected)) + match ClusterKeyDTO::find_by_id(id).one(self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(cluster_key) => { + Ok(ClusterKey::from(cluster_key)) + } + } } - async fn delete_by_id(&self, id: i32) -> Result<()> { - let _: Option = sqlx::query_as("DELETE FROM cluster_key where id = ?") - .bind(id) - .fetch_optional(&self.db_pool) - .await?; + let _ = ClusterKeyDTO::delete_by_id( + id).exec(self.db_connection).await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use crate::domain::clusterkey::entity::ClusterKey; + use crate::domain::clusterkey::repository::Repository; + use crate::infra::database::model::clusterkey::dto; + use crate::util::error::Result; + use crate::infra::database::model::clusterkey::repository::{ClusterKeyRepository}; + + #[tokio::test] + async fn test_cluster_key_repository_create_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 0, + data: vec![], + algorithm: "".to_string(), + identity: "".to_string(), + create_at: now.clone(), + }], + ]).append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let key_repository = ClusterKeyRepository::new(&db); + let key = ClusterKey{ + id: 0, + data: vec![], + algorithm: "fake_algorithm".to_string(), + identity: "123".to_string(), + create_at: now.clone(), + }; + assert_eq!(key_repository.create(key).await?, ()); + assert_eq!( + db.into_transaction_log(), + [ + //create + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"INSERT INTO `cluster_key` (`data`, `algorithm`, `identity`, `create_at`) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE `id` = VALUES(`id`)"#, + [vec![].into(), "fake_algorithm".into(), "123".into(), now.clone().into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_cluster_key_repository_delete_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + data: vec![], + algorithm: "fake_algorithm".to_string(), + identity: "123".to_string(), + create_at: now.clone(), + }], + ]).append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let key_repository = ClusterKeyRepository::new(&db); + assert_eq!(key_repository.delete_by_id(1).await?, ()); + assert_eq!( + db.into_transaction_log(), + [ + //delete + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"DELETE FROM `cluster_key` WHERE `cluster_key`.`id` = ?"#, + [1i32.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_cluster_key_repository_query_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + data: vec![], + algorithm: "fake_algorithm".to_string(), + identity: "123".to_string(), + create_at: now.clone(), + }], + vec![dto::Model { + id: 2, + data: vec![], + algorithm: "fake_algorithm".to_string(), + identity: "123".to_string(), + create_at: now.clone(), + }], + ], + ).into_connection(); + + let key_repository = ClusterKeyRepository::new(&db); + assert_eq!( + key_repository.get_latest("fake_algorithm").await?, + Some(ClusterKey::from(dto::Model { + id: 1, + data: vec![], + algorithm: "fake_algorithm".to_string(), + identity: "123".to_string(), + create_at: now.clone(), + })) + ); + assert_eq!( + key_repository.get_by_id(123).await?, + ClusterKey::from(dto::Model { + id: 2, + data: vec![], + algorithm: "fake_algorithm".to_string(), + identity: "123".to_string(), + create_at: now.clone(), + }) + ); + assert_eq!( + db.into_transaction_log(), + [ + //get_latest + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `cluster_key`.`id`, `cluster_key`.`data`, `cluster_key`.`algorithm`, `cluster_key`.`identity`, `cluster_key`.`create_at` FROM `cluster_key` WHERE `cluster_key`.`algorithm` = ? ORDER BY `cluster_key`.`id` DESC LIMIT ?"#, + ["fake_algorithm".into(), 1u64.into()] + ), + //get_by_id + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `cluster_key`.`id`, `cluster_key`.`data`, `cluster_key`.`algorithm`, `cluster_key`.`identity`, `cluster_key`.`create_at` FROM `cluster_key` WHERE `cluster_key`.`id` = ? LIMIT ?"#, + [123i32.into(), 1u64.into()] + ), + ] + ); + Ok(()) } } + diff --git a/src/infra/database/model/datakey/dto.rs b/src/infra/database/model/datakey/dto.rs index 289f00dc5a953154f103a509f4c478d05de8fccf..0954fef66c8e36554db714905c858d8ad7155da3 100644 --- a/src/infra/database/model/datakey/dto.rs +++ b/src/infra/database/model/datakey/dto.rs @@ -14,18 +14,24 @@ * */ -use crate::domain::datakey::entity::{DataKey, KeyState, Visibility, X509CRL}; +use crate::domain::datakey::entity::{DataKey, KeyState, Visibility}; use crate::domain::datakey::entity::KeyType; use crate::domain::datakey::traits::ExtendableAttributes; use crate::util::error::{Error}; use crate::util::key; use chrono::{DateTime, Utc}; -use sqlx::FromRow; use std::str::FromStr; +use sea_orm::ActiveValue::Set; -#[derive(Debug, FromRow)] -pub(super) struct DataKeyDTO { +use sea_orm::entity::prelude::*; +use sea_orm::{NotSet}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "data_key")] +pub struct Model { + #[sea_orm(primary_key)] pub id: i32, pub name: String, pub description: String, @@ -42,21 +48,41 @@ pub(super) struct DataKeyDTO { pub create_at: DateTime, pub expire_at: DateTime, pub key_state: String, - #[sqlx(default)] pub user_email: Option, - #[sqlx(default)] pub request_delete_users: Option, - #[sqlx(default)] pub request_revoke_users: Option, - #[sqlx(default)] pub x509_crl_update_at: Option> } -impl TryFrom for DataKey { +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_one = "super::super::user::dto::Entity")] + User, + #[sea_orm(has_one = "super::super::x509_crl_content::dto::Entity")] + CrlContent, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CrlContent.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + + + +impl TryFrom for DataKey { type Error = Error; - fn try_from(dto: DataKeyDTO) -> Result { + fn try_from(dto: Model) -> Result { Ok(DataKey { id: dto.id, name: dto.name.clone(), @@ -82,121 +108,49 @@ impl TryFrom for DataKey { } } -impl TryFrom for DataKeyDTO { +impl TryFrom for ActiveModel { type Error = Error; fn try_from(data_key: DataKey) -> Result { - Ok(DataKeyDTO { - id: data_key.id, - name: data_key.name.clone(), - description: data_key.description.clone(), - visibility: data_key.visibility.to_string(), - user: data_key.user, - attributes: data_key.serialize_attributes()?, - key_type: data_key.key_type.to_string(), - parent_id: data_key.parent_id, - fingerprint: data_key.fingerprint.clone(), - serial_number: data_key.serial_number, - private_key: key::encode_u8_to_hex_string( - &data_key.private_key + Ok(ActiveModel { + id: Set(data_key.id), + name: Set(data_key.name.clone()), + description: Set(data_key.description.clone()), + visibility: Set(data_key.visibility.to_string()), + user: Set(data_key.user), + attributes: Set(data_key.serialize_attributes()?), + key_type: Set(data_key.key_type.to_string()), + parent_id: Set(data_key.parent_id), + fingerprint: Set(data_key.fingerprint.clone()), + serial_number: Set(data_key.serial_number), + private_key: Set(key::encode_u8_to_hex_string( + &data_key.private_key) ), - public_key: key::encode_u8_to_hex_string( - &data_key.public_key + public_key: Set(key::encode_u8_to_hex_string( + &data_key.public_key) ), - certificate: key::encode_u8_to_hex_string( + certificate: Set(key::encode_u8_to_hex_string( &data_key.certificate - ), - create_at: data_key.create_at, - expire_at: data_key.expire_at, - key_state: data_key.key_state.to_string(), - user_email: None, - request_delete_users: None, - request_revoke_users: None, - x509_crl_update_at: None, - }) - } -} - -#[derive(Debug, FromRow)] -pub struct X509CRLDTO { - pub id: i32, - pub ca_id: i32, - pub data: String, - pub create_at: DateTime, - pub update_at: DateTime, -} - -impl TryFrom for X509CRL { - type Error = Error; - - fn try_from(value: X509CRLDTO) -> Result { - Ok(X509CRL { - id: value.id, - ca_id: value.ca_id, - data: key::decode_hex_string_to_u8(&value.data), - create_at: value.create_at, - update_at: value.update_at, - }) - } -} - -impl TryFrom for X509CRLDTO { - type Error = Error; - - fn try_from(value: X509CRL) -> Result { - Ok(X509CRLDTO { - id: value.id, - ca_id: value.ca_id, - data: key::encode_u8_to_hex_string(&value.data), - create_at: value.create_at, - update_at: value.update_at, + )), + create_at: Set(data_key.create_at), + expire_at: Set(data_key.expire_at), + key_state: Set(data_key.key_state.to_string()), + user_email: NotSet, + request_delete_users: NotSet, + request_revoke_users: NotSet, + x509_crl_update_at: NotSet, }) } } #[cfg(test)] mod tests { - use std::collections::HashMap; use super::*; use crate::domain::datakey::entity::{Visibility}; - #[test] - fn test_data_key_dto_from_entity() { - let key = DataKey{ - id: 0, - name: "Test Key".to_string(), - visibility: Visibility::Public, - description: "test key description".to_string(), - user: 0, - attributes: HashMap::new(), - key_type: KeyType::OpenPGP, - parent_id: None, - fingerprint: "".to_string(), - serial_number: None, - private_key: vec![1,2,3], - public_key: vec![4,5,6], - certificate: vec![7,8,9,10], - create_at: Utc::now(), - expire_at: Utc::now(), - key_state: KeyState::Disabled, - user_email: None, - request_delete_users: None, - request_revoke_users: None, - parent_key: None, - }; - let dto = DataKeyDTO::try_from(key).unwrap(); - assert_eq!(dto.id, 0); - assert_eq!(dto.name, "Test Key"); - assert_eq!(dto.visibility, Visibility::Public.to_string()); - assert_eq!(dto.key_state, KeyState::Disabled.to_string()); - assert_eq!(dto.private_key, "010203"); - assert_eq!(dto.public_key, "040506"); - assert_eq!(dto.certificate, "0708090A"); - } - #[test] fn test_data_key_entity_from_dto() { - let dto = DataKeyDTO { + let dto = Model { id: 1, name: "Test Key".to_string(), description: "".to_string(), diff --git a/src/infra/database/model/datakey/repository.rs b/src/infra/database/model/datakey/repository.rs index d135720062443834ee06cc6158cee3db0e4360fc..0aa8bf8a87d143bee2c74948a58f38e170e135c2 100644 --- a/src/infra/database/model/datakey/repository.rs +++ b/src/infra/database/model/datakey/repository.rs @@ -14,77 +14,96 @@ * */ -use super::dto::DataKeyDTO; -use crate::infra::database::pool::DbPool; +use super::dto as datakey_dto; +use super::super::user::dto as user_dto; +use super::super::request_delete::dto as request_dto; +use super::super::x509_crl_content::dto as crl_content_dto; +use super::super::x509_revoked_key::dto as revoked_key_dto; use crate::domain::datakey::entity::{DataKey, KeyState, KeyType, ParentKey, RevokedKey, Visibility, X509CRL, X509RevokeReason}; use crate::domain::datakey::repository::Repository; -use crate::util::error::{Result}; +use crate::util::error::{Error, Result}; use async_trait::async_trait; use chrono::Duration; +use sea_query::Expr; use chrono::Utc; -use sqlx::{MySql, Transaction}; -use crate::infra::database::model::datakey::dto::X509CRLDTO; -use crate::infra::database::model::request_delete::dto::{PendingOperationDTO, RequestType, RevokedKeyDTO}; -use crate::util::error; +use sea_orm::{Condition, Iterable, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, ActiveValue::Set, TransactionTrait, DatabaseTransaction, QuerySelect, JoinType, sea_query, RelationTrait, RelationBuilder, ExecResult, ConnectionTrait, Statement, DatabaseBackend, NotSet}; +use sea_orm::sea_query::{Alias, IntoCondition, OnConflict}; +use crate::infra::database::model::request_delete::dto::RequestType; +use crate::util::key::encode_u8_to_hex_string; const PUBLICKEY_PENDING_THRESHOLD: i32 = 3; const PRIVATEKEY_PENDING_THRESHOLD: i32 = 1; #[derive(Clone)] -pub struct DataKeyRepository { - db_pool: DbPool, +pub struct DataKeyRepository<'a> { + db_connection: &'a DatabaseConnection } -impl DataKeyRepository { - pub fn new(db_pool: DbPool) -> Self { +impl<'a> DataKeyRepository<'a> { + pub fn new(db_connection: &'a DatabaseConnection) -> Self { Self { - db_pool, + db_connection } } - async fn create_pending_operation(&self, pending_operation: PendingOperationDTO, tx: &mut Transaction<'_, MySql>) -> Result<()> { - let _ : Option = sqlx::query_as("INSERT IGNORE INTO pending_operation(user_id, key_id, user_email, create_at, request_type) VALUES (?, ?, ?, ?, ?)") - .bind(pending_operation.user_id) - .bind(pending_operation.key_id) - .bind(pending_operation.user_email) - .bind(pending_operation.create_at) - .bind(pending_operation.request_type.to_string()) - .fetch_optional(tx) - .await?; + async fn create_pending_operation(&self, pending_operation: request_dto::Model, tx: &mut DatabaseTransaction) -> Result<()> { + let operation = request_dto::ActiveModel { + user_id: Set(pending_operation.user_id), + key_id: Set(pending_operation.key_id), + request_type: Set(pending_operation.request_type), + user_email: Set(pending_operation.user_email), + create_at: Set(pending_operation.create_at), + ..Default::default() + }; + //TODO: https://github.com/SeaQL/sea-orm/issues/1790 + request_dto::Entity::insert(operation).on_conflict(OnConflict::new() + .update_column(request_dto::Column::Id).to_owned() + ).exec(tx).await?; Ok(()) } - async fn delete_pending_operation(&self, user_id: i32, id: i32, request_type: RequestType, tx: &mut Transaction<'_, MySql>) -> Result<()> { - let _ : Option = sqlx::query_as("DELETE FROM pending_operation WHERE user_id = ? AND key_id = ? and request_type = ?") - .bind(user_id) - .bind(id) - .bind(request_type.to_string()) - .fetch_optional(tx) + async fn delete_pending_operation(&self, user_id: i32, id: i32, request_type: request_dto::RequestType, tx: &mut DatabaseTransaction) -> Result<()> { + let _ = request_dto::Entity::delete_many().filter(Condition::all() + .add(request_dto::Column::UserId.eq(user_id)) + .add(request_dto::Column::RequestType.eq(request_type.to_string())) + .add(request_dto::Column::KeyId.eq(id))).exec(tx) .await?; Ok(()) } - async fn create_revoke_record(&self, key_id: i32, ca_id: i32, reason: X509RevokeReason, tx: &mut Transaction<'_, MySql>) -> Result<()> { - let revoked = RevokedKeyDTO::new(key_id, ca_id, reason); - let _ : Option = sqlx::query_as("INSERT IGNORE INTO x509_keys_revoked(ca_id, key_id, create_at, reason) VALUES (?, ?, ?, ?)") - .bind(revoked.ca_id) - .bind(revoked.key_id) - .bind(revoked.create_at) - .bind(revoked.reason) - .fetch_optional(tx) - .await?; + async fn create_revoke_record(&self, key_id: i32, ca_id: i32, reason: X509RevokeReason, tx: &mut DatabaseTransaction) -> Result<()> { + let revoked = revoked_key_dto::ActiveModel{ + id: Default::default(), + key_id: Set(key_id), + ca_id: Set(ca_id), + reason: Set(reason.to_string()), + create_at: Set(Utc::now()), + serial_number: NotSet, + }; + //TODO: https://github.com/SeaQL/sea-orm/issues/1790 + revoked_key_dto::Entity::insert(revoked).on_conflict(OnConflict::new() + .update_column(request_dto::Column::Id).to_owned() + ).exec(tx).await?; Ok(()) } - async fn delete_revoke_record(&self, key_id: i32, ca_id: i32, tx: &mut Transaction<'_, MySql>) -> Result<()> { - let _ : Option = sqlx::query_as("DELETE FROM x509_keys_revoked WHERE key_id = ? AND ca_id = ?") - .bind(key_id) - .bind(ca_id) - .fetch_optional(tx) + async fn delete_revoke_record(&self, key_id: i32, ca_id: i32, tx: &mut DatabaseTransaction) -> Result<()> { + let _ = revoked_key_dto::Entity::delete_many().filter(Condition::all() + .add(revoked_key_dto::Column::KeyId.eq(key_id)) + .add(revoked_key_dto::Column::CaId.eq(ca_id))).exec(tx) .await?; Ok(()) } + fn get_pending_operation_relation(&self, request_type: RequestType) -> RelationBuilder { + request_dto::Entity::belongs_to(datakey_dto::Entity).from( + request_dto::Column::KeyId).to( + datakey_dto::Column::Id).on_condition(move |left, _right| { + Expr::col((left, request_dto::Column::RequestType)).eq(request_type.clone().to_string()).into_condition() + } + ) + } + async fn _obtain_datakey_parent(&self, datakey: &mut DataKey) -> Result<()> { if let Some(parent) = datakey.parent_id { let result = self.get_by_id(parent).await; @@ -99,7 +118,7 @@ impl DataKeyRepository { }) } _ => { - return Err(error::Error::DatabaseError("unable to find parent key".to_string())); + return Err(Error::DatabaseError("unable to find parent key".to_string())); } } } @@ -108,402 +127,1306 @@ impl DataKeyRepository { } #[async_trait] -impl Repository for DataKeyRepository { +impl<'a> Repository for DataKeyRepository<'a> { async fn create(&self, data_key: DataKey) -> Result { - let dto = DataKeyDTO::try_from(data_key)?; - let record : u64 = sqlx::query("INSERT INTO data_key(name, description, user, attributes, key_type, fingerprint, private_key, public_key, certificate, create_at, expire_at, key_state, visibility, parent_id, serial_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") - .bind(&dto.name) - .bind(&dto.description) - .bind(dto.user) - .bind(dto.attributes) - .bind(dto.key_type) - .bind(dto.fingerprint) - .bind(dto.private_key) - .bind(dto.public_key) - .bind(dto.certificate) - .bind(dto.create_at) - .bind(dto.expire_at) - .bind(dto.key_state) - .bind(dto.visibility) - .bind(dto.parent_id) - .bind(dto.serial_number) - .execute(&self.db_pool) - .await?.last_insert_id(); - let mut datakey = self.get_by_id(record as i32).await?; + let dto = datakey_dto::ActiveModel::try_from(data_key)?; + let insert_result =datakey_dto::Entity::insert(dto).exec(self.db_connection).await?; + + let mut datakey = self.get_by_id(insert_result.last_insert_id).await?; //fetch parent key if 'parent_id' exists. if let Err(err) = self._obtain_datakey_parent(&mut datakey).await { warn!("failed to create datakey {} {}", datakey.name, err); - let _ = self.delete(record as i32).await; + let _ = self.delete(insert_result.last_insert_id).await; return Err(err); } - Ok(datakey) } async fn delete(&self, id: i32) -> Result<()> { - let _: Option = sqlx::query_as("DELETE FROM data_key WHERE id = ?") - .bind(id) - .fetch_optional(&self.db_pool) - .await?; + datakey_dto::Entity::delete_by_id(id).exec(self.db_connection).await?; Ok(()) } async fn get_all_keys(&self, key_type: Option, visibility: Visibility, user_id: i32) -> Result> { - let dtos: Vec = match key_type { - None => { - if visibility == Visibility::Public { - sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.key_state != ? AND D.visibility = ? \ - GROUP BY D.id") - .bind(KeyState::Deleted.to_string()) - .bind(visibility.to_string()) - .fetch_all(&self.db_pool) - .await? - } else { - sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.key_state != ? AND D.visibility = ? AND D.user = ? \ - GROUP BY D.id") - .bind(KeyState::Deleted.to_string()) - .bind(visibility.to_string()) - .bind(user_id) - .fetch_all(&self.db_pool) - .await? - } + let mut conditions = Condition::all().add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())).add( + datakey_dto::Column::Visibility.eq(visibility.to_string()) + ); + if let Some(k_type) = key_type { + conditions = conditions.add(datakey_dto::Column::KeyType.eq(k_type.to_string())) + } + if visibility == Visibility::Private { + conditions = conditions.add(datakey_dto::Column::User.eq(user_id)) + } + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).exprs( + [Expr::cust("user_table.email as user_email"), + Expr::cust("GROUP_CONCAT(request_delete_table.user_email) as request_delete_users"), + Expr::cust("GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users")]).join_as_rev( + JoinType::InnerJoin, user_dto::Relation::Datakey.def(), Alias::new("user_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Delete).into(), + Alias::new("request_delete_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Revoke).into(), + Alias::new("request_revoke_table")).group_by(datakey_dto::Column::Id).filter(conditions).all(self.db_connection).await { + Err(err) => { + warn!("failed to query database {:?}", err); + Err(Error::NotFoundError) } - Some(key_t) => { - if visibility == Visibility::Public { - sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.key_state != ? AND \ - D.key_type = ? AND D.visibility = ? \ - GROUP BY D.id") - .bind(KeyState::Deleted.to_string()) - .bind(key_t.to_string()) - .bind(visibility.to_string()) - .fetch_all(&self.db_pool) - .await? - } else { - sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.key_state != ? AND \ - D.key_type = ? AND D.visibility = ? AND D.user = ? \ - GROUP BY D.id") - .bind(KeyState::Deleted.to_string()) - .bind(key_t.to_string()) - .bind(visibility.to_string()) - .bind(user_id) - .fetch_all(&self.db_pool) - .await? - } + Ok(data_keys) => { + let mut results = vec![]; + for dto in data_keys.into_iter() { + results.push(DataKey::try_from(dto)?); + } + Ok(results) } - }; - let mut results = vec![]; - for dto in dtos.into_iter() { - results.push(DataKey::try_from(dto)?); } - Ok(results) } async fn get_keys_for_crl_update(&self, duration: Duration) -> Result> { let now = Utc::now(); - let dtos: Vec = sqlx::query_as( - "SELECT D.id, D.name, D.description, D.user, D.attributes, D.key_type, D.fingerprint, D.private_key, D.public_key, D.certificate, D.create_at, D.expire_at, D.key_state, D.visibility, D.parent_id, D.serial_number, R.update_at AS x509_crl_update_at \ - FROM data_key D \ - LEFT JOIN x509_crl_content R ON D.id = R.ca_id \ - WHERE (D.key_type = ? OR D.key_type = ?) AND D.key_state != ?") - .bind(KeyType::X509ICA.to_string()) - .bind(KeyType::X509CA.to_string()) - .bind(KeyState::Deleted.to_string()) - .fetch_all(&self.db_pool) - .await?; - let mut results = vec![]; - for dto in dtos.into_iter() { - if dto.x509_crl_update_at.is_none() { - results.push(DataKey::try_from(dto)?); - } else { - let update_at = dto.x509_crl_update_at.unwrap(); - if update_at + duration <= now { - results.push(DataKey::try_from(dto)?); + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).column_as( + Expr::col((Alias::new("crl_table"), crl_content_dto::Column::UpdateAt)), "x509_crl_update_at").join_as_rev( + JoinType::LeftJoin, crl_content_dto::Relation::Datakey.def(), Alias::new("crl_table")).filter( + Condition::all().add( + Condition::any().add(datakey_dto::Column::KeyType.eq(KeyType::X509CA.to_string()) + ).add(datakey_dto::Column::KeyType.eq(KeyType::X509ICA.to_string()))).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())) + ).all(self.db_connection).await { + Err(_) => { + Ok(vec![]) + } + Ok(keys) => { + let mut results = vec![]; + for dto in keys.into_iter() { + if dto.x509_crl_update_at.is_none() { + results.push(DataKey::try_from(dto)?); + } else { + let update_at = dto.x509_crl_update_at.unwrap(); + if update_at + duration <= now { + results.push(DataKey::try_from(dto)?); + } + } } + Ok(results) } } - Ok(results) } async fn get_revoked_serial_number_by_parent_id(&self, id: i32) -> Result> { - let dtos : Vec = sqlx::query_as( - "SELECT R.*, D.serial_number \ - FROM x509_keys_revoked R \ - INNER JOIN data_key D ON R.key_id = D.id \ - WHERE R.ca_id = ? AND D.key_state = ?") - .bind(id) - .bind(KeyState::Revoked.to_string()) - .fetch_all(&self.db_pool) - .await?; - let mut results = vec![]; - for dto in dtos.into_iter() { - results.push(RevokedKey::try_from(dto)?); + match revoked_key_dto::Entity::find().select_only().columns( + revoked_key_dto::Column::iter().filter(|col| + match col { + revoked_key_dto::Column::SerialNumber => false, + _ => true, + })).column_as( + Expr::col((Alias::new("datakey_table"), datakey_dto::Column::SerialNumber)), + "serial_number").join_as_rev( + JoinType::InnerJoin, datakey_dto::Entity::belongs_to(revoked_key_dto::Entity).from( + datakey_dto::Column::Id).to( + revoked_key_dto::Column::KeyId).on_condition( + move |left, right| { + Condition::all().add( + Expr::col((left, datakey_dto::Column::KeyState)).eq(KeyState::Revoked.to_string())).add( + Expr::col((right, revoked_key_dto::Column::CaId)).eq(id) + ).into_condition() + } + ).into(), + Alias::new("datakey_table")).all(self.db_connection).await { + Err(err) => { + warn!("failed to query database {:?}", err); + Err(Error::NotFoundError) + } + Ok(revoked_keys) => { + let mut results = vec![]; + for dto in revoked_keys.into_iter() { + results.push(RevokedKey::try_from(dto)?); + } + Ok(results) + } } - Ok(results) } async fn get_by_id(&self, id: i32) -> Result { - let dto: DataKeyDTO = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.id = ? AND D.key_state != ? \ - GROUP BY D.id") - .bind(id) - .bind(KeyState::Deleted.to_string()) - .fetch_one(&self.db_pool) - .await?; - Ok(DataKey::try_from(dto)?) + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).exprs( + [Expr::cust("user_table.email as user_email"), + Expr::cust("GROUP_CONCAT(request_delete_table.user_email) as request_delete_users"), + Expr::cust("GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users")]).join_as_rev( + JoinType::InnerJoin, user_dto::Relation::Datakey.def(), Alias::new("user_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Delete).into(), + Alias::new("request_delete_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Revoke).into(), + Alias::new("request_revoke_table")).group_by(datakey_dto::Column::Id).filter( + Condition::all().add( + datakey_dto::Column::Id.eq(id)).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())) + ).one(self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(datakey) => { + Ok(DataKey::try_from(datakey)?) + } + } } async fn get_by_parent_id(&self, parent_id: i32) -> Result> { - let dtos: Vec = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.parent_id = ? AND D.key_state != ? \ - GROUP BY D.id") - .bind(parent_id) - .bind(KeyState::Deleted.to_string()) - .fetch_all(&self.db_pool) - .await?; - let mut results = vec![]; - for dto in dtos.into_iter() { - results.push(DataKey::try_from(dto)?); + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).exprs( + [Expr::cust("user_table.email as user_email"), + Expr::cust("GROUP_CONCAT(request_delete_table.user_email) as request_delete_users"), + Expr::cust("GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users")]).join_as_rev( + JoinType::InnerJoin, user_dto::Relation::Datakey.def(), Alias::new("user_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Delete).into(), + Alias::new("request_delete_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Revoke).into(), + Alias::new("request_revoke_table")).group_by(datakey_dto::Column::Id).filter( + Condition::all().add( + datakey_dto::Column::ParentId.eq(parent_id)).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())) + ).all(self.db_connection).await { + Err(err) => { + warn!("failed to query database {:?}", err); + Err(Error::NotFoundError) + } + Ok(datakeys) => { + let mut results = vec![]; + for dto in datakeys.into_iter() { + results.push(DataKey::try_from(dto)?); + } + Ok(results) + } + } + } + + async fn check_name_exists(&self, name: &str) -> Result{ + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).filter(Condition::all().add( + datakey_dto::Column::Name.eq(name)).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string()))).one(self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(datakey) => { + Ok(DataKey::try_from(datakey)?) + } } - Ok(results) } async fn get_by_name(&self, name: &str) -> Result { - let dto: DataKeyDTO = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.name = ? AND D.key_state != ? \ - GROUP BY D.id") - .bind(name) - .bind(KeyState::Deleted.to_string()) - .fetch_one(&self.db_pool) - .await?; - Ok(DataKey::try_from(dto)?) + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).exprs( + [Expr::cust("user_table.email as user_email"), + Expr::cust("GROUP_CONCAT(request_delete_table.user_email) as request_delete_users"), + Expr::cust("GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users")]).join_as_rev( + JoinType::InnerJoin, user_dto::Relation::Datakey.def(), Alias::new("user_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Delete).into(), + Alias::new("request_delete_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Revoke).into(), + Alias::new("request_revoke_table")).group_by(datakey_dto::Column::Id).filter( + Condition::all().add( + datakey_dto::Column::Name.eq(name)).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())) + ).one(self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(datakey) => { + Ok(DataKey::try_from(datakey)?) + } + } } async fn update_state(&self, id: i32, state: KeyState) -> Result<()> { //Note: if the key in deleted status, it cannot be updated to other states - let _: Option = sqlx::query_as("UPDATE data_key SET key_state = ? WHERE id = ? AND key_state != ?") - .bind(state.to_string()) - .bind(id) - .bind(KeyState::Deleted.to_string()) - .fetch_optional(&self.db_pool) - .await?; + let _ = datakey_dto::Entity::update_many().col_expr( + datakey_dto::Column::KeyState, Expr::value(state.to_string()) + ).filter(Condition::all().add( + datakey_dto::Column::Id.eq(id)).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())) + ).exec(self.db_connection).await?; Ok(()) } async fn update_key_data(&self, data_key: DataKey) -> Result<()> { //Note: if the key in deleted status, it cannot be updated to other states - let dto = DataKeyDTO::try_from(data_key)?; - let _: Option = sqlx::query_as("UPDATE data_key SET serial_number = ?, fingerprint = ?, private_key = ?, public_key = ?, certificate = ? WHERE id = ? AND key_state != ?") - .bind(dto.serial_number) - .bind(dto.fingerprint) - .bind(dto.private_key) - .bind(dto.public_key) - .bind(dto.certificate) - .bind(dto.id) - .bind(KeyState::Deleted.to_string()) - .fetch_optional(&self.db_pool) - .await?; + let _ = datakey_dto::Entity::update_many().col_expr( + datakey_dto::Column::SerialNumber, Expr::value(data_key.serial_number) + ).col_expr( + datakey_dto::Column::Fingerprint, Expr::value(data_key.fingerprint) + ).col_expr( + datakey_dto::Column::PrivateKey, Expr::value(encode_u8_to_hex_string(&data_key.private_key)) + ).col_expr( + datakey_dto::Column::PublicKey, Expr::value(encode_u8_to_hex_string(&data_key.public_key)) + ).col_expr( + datakey_dto::Column::Certificate, Expr::value(encode_u8_to_hex_string(&data_key.certificate)) + ).filter(Condition::all().add( + datakey_dto::Column::Id.eq(data_key.id)).add( + datakey_dto::Column::KeyState.ne(KeyState::Deleted.to_string())) + ).exec(self.db_connection).await?; Ok(()) } async fn get_enabled_key_by_type_and_name(&self, key_type: String, name: String) -> Result { - let dto: DataKeyDTO = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ - GROUP_CONCAT(K.user_email) as request_revoke_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ - LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ - WHERE D.name = ? AND D.key_type = ? AND D.key_state = ? \ - GROUP BY D.id") - .bind(name) - .bind(key_type) - .bind(KeyState::Enabled.to_string()) - .fetch_one(&self.db_pool) - .await?; - let mut datakey = DataKey::try_from(dto)?; - self._obtain_datakey_parent(&mut datakey).await?; - return Ok(datakey) + match datakey_dto::Entity::find().select_only().columns( + datakey_dto::Column::iter().filter(|col| + match col { + datakey_dto::Column::UserEmail | datakey_dto::Column::RequestDeleteUsers | datakey_dto::Column::RequestRevokeUsers | datakey_dto::Column::X509CrlUpdateAt => false, + _ => true, + })).exprs( + [Expr::cust("user_table.email as user_email"), + Expr::cust("GROUP_CONCAT(request_delete_table.user_email) as request_delete_users"), + Expr::cust("GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users")]).join_as_rev( + JoinType::InnerJoin, user_dto::Relation::Datakey.def(), Alias::new("user_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Delete).into(), + Alias::new("request_delete_table")).join_as_rev( + JoinType::LeftJoin, self.get_pending_operation_relation(RequestType::Revoke).into(), + Alias::new("request_revoke_table")).group_by(datakey_dto::Column::Id).filter( + Condition::all().add( + datakey_dto::Column::Name.eq(name)).add( + datakey_dto::Column::KeyType.eq(key_type)).add( + datakey_dto::Column::KeyState.eq(KeyState::Enabled.to_string())) + ).one(self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(datakey) => { + Ok(DataKey::try_from(datakey)?) + } + } } async fn request_delete_key(&self, user_id: i32, user_email: String, id: i32, public_key: bool) -> Result<()> { - let mut tx = self.db_pool.begin().await?; + let mut txn = self.db_connection.begin().await?; let threshold = if public_key { PUBLICKEY_PENDING_THRESHOLD } else { PRIVATEKEY_PENDING_THRESHOLD }; //1. update key state to pending delete if needed. - let _: Option = sqlx::query_as( - "UPDATE data_key SET key_state = ? \ - WHERE id = ?") - .bind(KeyState::PendingDelete.to_string()) - .bind(id) - .fetch_optional(&mut tx) - .await?; + let _ = datakey_dto::Entity::update_many().col_expr( + datakey_dto::Column::KeyState, Expr::value(KeyState::PendingDelete.to_string()) + ).filter(datakey_dto::Column::Id.eq(id)).exec(&txn).await?; //2. add request delete record - let pending_delete = PendingOperationDTO::new_for_delete(id, user_id, user_email); - self.create_pending_operation(pending_delete, &mut tx).await?; + let pending_delete = request_dto::Model::new_for_delete(id, user_id, user_email); + self.create_pending_operation(pending_delete, &mut txn).await?; //3. delete datakey if pending delete count >= threshold - let _: Option = sqlx::query_as( + let _: ExecResult = txn.execute(Statement::from_sql_and_values( + DatabaseBackend::MySql, "UPDATE data_key SET key_state = ? \ WHERE id = ? AND ( \ - SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) >= ?") - .bind(KeyState::Deleted.to_string()) - .bind(id) - .bind(id) - .bind(threshold) - .fetch_optional(&mut tx) - .await?; - tx.commit().await?; + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) >= ?", + [KeyState::Deleted.to_string().into(), id.into(), id.into(), threshold.into()], + )).await?; + txn.commit().await?; Ok(()) } async fn request_revoke_key(&self, user_id: i32, user_email: String, id: i32, parent_id: i32, reason: X509RevokeReason, public_key: bool) -> Result<()> { - let mut tx = self.db_pool.begin().await?; + let mut txn = self.db_connection.begin().await?; let threshold = if public_key { PUBLICKEY_PENDING_THRESHOLD } else { PRIVATEKEY_PENDING_THRESHOLD }; //1. update key state to pending delete if needed. - let _: Option = sqlx::query_as( - "UPDATE data_key SET key_state = ? \ - WHERE id = ?") - .bind(KeyState::PendingRevoke.to_string()) - .bind(id) - .fetch_optional(&mut tx) - .await?; + let _ = datakey_dto::Entity::update_many().col_expr( + datakey_dto::Column::KeyState, Expr::value(KeyState::PendingDelete.to_string()) + ).filter(datakey_dto::Column::Id.eq(id)).exec(&txn).await?; //2. add request revoke pending record - let pending_revoke = PendingOperationDTO::new_for_revoke(id, user_id, user_email); - self.create_pending_operation(pending_revoke, &mut tx).await?; + let pending_revoke = request_dto::Model::new_for_revoke(id, user_id, user_email); + self.create_pending_operation(pending_revoke, &mut txn).await?; //3. add revoked record - self.create_revoke_record(id, parent_id, reason, &mut tx).await?; + self.create_revoke_record(id, parent_id, reason, &mut txn).await?; //4. mark datakey revoked if pending revoke count >= threshold - let _: Option = sqlx::query_as( + let _: ExecResult = txn.execute(Statement::from_sql_and_values( + DatabaseBackend::MySql, "UPDATE data_key SET key_state = ? \ WHERE id = ? AND ( \ - SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) >= ?") - .bind(KeyState::Revoked.to_string()) - .bind(id) - .bind(id) - .bind(threshold) - .fetch_optional(&mut tx) - .await?; - tx.commit().await?; + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) >= ?", + [KeyState::Revoked.to_string().into(), id.into(), id.into(), threshold.into()], + )).await?; + txn.commit().await?; Ok(()) } async fn cancel_delete_key(&self, user_id: i32, id: i32) -> Result<()> { - let mut tx = self.db_pool.begin().await?; + let mut txn = self.db_connection.begin().await?; //1. delete pending delete record - self.delete_pending_operation(user_id, id, RequestType::Delete, &mut tx).await?; + self.delete_pending_operation( + user_id, id, RequestType::Delete, &mut txn).await?; //2. update status if there is not any pending delete record. - let _: Option = sqlx::query_as( + let _: ExecResult = txn.execute(Statement::from_sql_and_values( + DatabaseBackend::MySql, "UPDATE data_key SET key_state = ? \ WHERE id = ? AND ( \ - SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) = ?") - .bind(KeyState::Disabled.to_string()) - .bind(id) - .bind(id) - .bind(0) - .fetch_optional(&mut tx) + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) = ?", + [KeyState::Disabled.to_string().into(), id.into(), id.into(), 0i32.into()], + )) .await?; - tx.commit().await?; + txn.commit().await?; Ok(()) } async fn cancel_revoke_key(&self, user_id: i32, id: i32, parent_id: i32) -> Result<()> { - let mut tx = self.db_pool.begin().await?; + let mut txn = self.db_connection.begin().await?; //1. delete pending delete record - self.delete_pending_operation(user_id, id, RequestType::Revoke, &mut tx).await?; + self.delete_pending_operation(user_id, id, RequestType::Revoke, &mut txn).await?; //2. delete revoked record - self.delete_revoke_record(id, parent_id, &mut tx).await?; + self.delete_revoke_record(id, parent_id, &mut txn).await?; //3. update status if there is not any pending delete record. - let _: Option = sqlx::query_as( - "UPDATE data_key SET key_state = ? \ - WHERE id = ? AND ( \ - SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) = ?") - .bind(KeyState::Disabled.to_string()) - .bind(id) - .bind(id) - .bind(0) - .fetch_optional(&mut tx) + let _: ExecResult = txn.execute(Statement::from_sql_and_values( + DatabaseBackend::MySql, + "UPDATE data_key SET key_state = ? \ + WHERE id = ? AND ( \ + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) = ?", + [KeyState::Disabled.to_string().into(), id.into(), id.into(), 0i32.into()], + )) .await?; - tx.commit().await?; + txn.commit().await?; Ok(()) } async fn get_x509_crl_by_ca_id(&self, id: i32) -> Result { - let dto: X509CRLDTO = sqlx::query_as( - "SELECT * from x509_crl_content WHERE ca_id = ?") - .bind(id) - .fetch_one(&self.db_pool) - .await?; - Ok( X509CRL::try_from(dto)?) + match crl_content_dto::Entity::find().filter( + crl_content_dto::Column::CaId.eq(id) + ).one( + self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(content) => { + Ok(X509CRL::try_from(content)?) + } + } } async fn upsert_x509_crl(&self, crl: X509CRL) -> Result<()> { - let dto = X509CRLDTO::try_from(crl)?; - match self.get_x509_crl_by_ca_id(dto.ca_id).await { + let ca_id = crl.ca_id.clone(); + let crl_model = crl_content_dto::ActiveModel { + id: Set(crl.id), + ca_id: Set(crl.ca_id), + data: Set(encode_u8_to_hex_string(&crl.data)), + create_at: Set(crl.create_at), + update_at: Set(crl.update_at) + }; + match self.get_x509_crl_by_ca_id(ca_id).await { Ok(_) => { - sqlx::query( - "UPDATE x509_crl_content SET data = ?, create_at = ?, update_at = ? WHERE ca_id = ?") - .bind(dto.data) - .bind(dto.create_at) - .bind(dto.update_at) - .bind(dto.ca_id) - .execute(&self.db_pool) - .await?; + //update crl content with new version + crl_content_dto::Entity::update(crl_model.clone()).filter( + crl_content_dto::Column::CaId.eq(ca_id)).exec(self.db_connection).await?; } Err(_) => { - sqlx::query( - "INSERT INTO x509_crl_content(ca_id, data, create_at, update_at) VALUES (?, ?, ?, ?)") - .bind(dto.ca_id) - .bind(dto.data) - .bind(dto.create_at) - .bind(dto.update_at) - .execute(&self.db_pool) - .await?; + crl_content_dto::Entity::insert(crl_model).exec(self.db_connection).await?; } - } + }; Ok(()) } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction, TransactionTrait}; + use crate::domain::datakey::entity::{DataKey, KeyState, KeyType, ParentKey, RevokedKey, Visibility}; + use super::super::super::x509_revoked_key::dto as revoked_key_dto; + use chrono::{Duration}; + use super::super::super::request_delete::dto as request_dto; + use crate::domain::datakey::repository::Repository; + use crate::infra::database::model::datakey::dto; + use crate::util::error::Result; + use crate::infra::database::model::datakey::repository::{DataKeyRepository}; + use crate::infra::database::model::request_delete::dto::RequestType; + + + #[tokio::test] + async fn test_datakey_repository_get_all_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + //get public + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + //get private + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + //get private with type + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let datakey = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!( + datakey_repository.get_all_keys(None, Visibility::Public, 1).await?, vec![datakey.clone()] + ); + assert_eq!( + datakey_repository.get_all_keys(None, Visibility::Private, 1).await?, vec![datakey.clone()] + ); + assert_eq!( + datakey_repository.get_all_keys(Some(KeyType::OpenPGP), Visibility::Private, 1).await?, vec![datakey] + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`key_state` <> ? AND `data_key`.`visibility` = ? GROUP BY `data_key`.`id`"#, + ["delete".into(), "revoke".into(), "deleted".into(), "public".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`key_state` <> ? AND `data_key`.`visibility` = ? AND `data_key`.`user` = ? GROUP BY `data_key`.`id`"#, + ["delete".into(), "revoke".into(), "deleted".into(), "private".into(), 1i32.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`key_state` <> ? AND `data_key`.`visibility` = ? AND `data_key`.`key_type` = ? AND `data_key`.`user` = ? GROUP BY `data_key`.`id`"#, + ["delete".into(), "revoke".into(), "deleted".into(), "private".into(), "pgp".into(), 1i32.into()] + ), + ] + ); + + Ok(()) + } + #[tokio::test] + async fn test_datakey_repository_get_by_id_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let user = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!( + datakey_repository.get_by_id(1).await?, user + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`id` = ? AND `data_key`.`key_state` <> ? GROUP BY `data_key`.`id` LIMIT ?"#, + ["delete".into(), "revoke".into(), 1i32.into(), "deleted".into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_update_key_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + }, + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let datakey = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "456".to_string(), + serial_number: Some("123".to_string()), + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!(datakey_repository.update_state(1, KeyState::Enabled).await?,()); + assert_eq!(datakey_repository.update_key_data( datakey).await?,()); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"UPDATE `data_key` SET `key_state` = ? WHERE `data_key`.`id` = ? AND `data_key`.`key_state` <> ?"#, + [KeyState::Enabled.to_string().into(), 1i32.into(), "deleted".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"UPDATE `data_key` SET `serial_number` = ?, `fingerprint` = ?, `private_key` = ?, `public_key` = ?, `certificate` = ? WHERE `data_key`.`id` = ? AND `data_key`.`key_state` <> ?"#, + ["123".into(), "456".into(), "0708090A".into(), "040506".into(), "010203".into(), 1i32.into(), "deleted".into()] + ) + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_get_keys_for_crl_update_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "456".to_string(), + serial_number: Some("123".to_string()), + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let datakey = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "456".to_string(), + serial_number: Some("123".to_string()), + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + let duration = Duration::days(1); + assert_eq!(datakey_repository.get_keys_for_crl_update(duration).await?, vec![datakey]); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, `crl_table`.`update_at` AS `x509_crl_update_at` FROM `data_key` LEFT JOIN `x509_crl_content` AS `crl_table` ON `crl_table`.`ca_id` = `data_key`.`id` WHERE (`data_key`.`key_type` = ? OR `data_key`.`key_type` = ?) AND `data_key`.`key_state` <> ?"#, + [KeyType::X509CA.to_string().into(), KeyType::X509ICA.to_string().into(), "deleted".into()] + ) + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_get_revoked_serial_number_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![revoked_key_dto::Model { + id: 1, + key_id: 1, + ca_id: 1, + serial_number: Some("123".to_string()), + create_at: now.clone(), + reason: "unspecified".to_string(), + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let revoked_key = revoked_key_dto::Model{ + id: 1, + key_id: 1, + ca_id: 1, + serial_number: Some("123".to_string()), + create_at: now.clone(), + reason: "unspecified".to_string(), + }; + assert_eq!(datakey_repository.get_revoked_serial_number_by_parent_id(1).await?, + vec![RevokedKey::try_from(revoked_key)?]); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `x509_keys_revoked`.`id`, `x509_keys_revoked`.`key_id`, `x509_keys_revoked`.`ca_id`, `x509_keys_revoked`.`reason`, `x509_keys_revoked`.`create_at`, `datakey_table`.`serial_number` AS `serial_number` FROM `x509_keys_revoked` INNER JOIN `data_key` AS `datakey_table` ON `datakey_table`.`id` = `x509_keys_revoked`.`key_id` AND (`datakey_table`.`key_state` = ? AND `x509_keys_revoked`.`ca_id` = ?)"#, + [KeyState::Revoked.to_string().into(), 1i32.into()] + ) + ] + ); + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_check_name_exists_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let user = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!( + datakey_repository.check_name_exists("Test Key").await?, user + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state` FROM `data_key` WHERE `data_key`.`name` = ? AND `data_key`.`key_state` <> ? LIMIT ?"#, + ["Test Key".into(), "deleted".into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_get_by_name_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let user = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!( + datakey_repository.get_by_name("Test Key").await?, user + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`name` = ? AND `data_key`.`key_state` <> ? GROUP BY `data_key`.`id` LIMIT ?"#, + ["delete".into(), "revoke".into(), "Test Key".into(), "deleted".into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_delete_datakey_sql_statement() -> Result<()> { + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_exec_results([ + MockExecResult { + last_insert_id: 0, + rows_affected: 0, + } + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + assert_eq!( + datakey_repository.delete(1).await?, () + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"DELETE FROM `data_key` WHERE `data_key`.`id` = ?"#, + [1i32.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_get_enabled_key_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let user = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!( + datakey_repository.get_enabled_key_by_type_and_name("openpgp".to_string(), "fake_name".to_string()).await?, user + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`name` = ? AND `data_key`.`key_type` = ? AND `data_key`.`key_state` = ? GROUP BY `data_key`.`id` LIMIT ?"#, + ["delete".into(), "revoke".into(), "fake_name".into(), "openpgp".into(), "enabled".into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_create_datakey_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: Some(2), + fingerprint: "".to_string(), + serial_number: Some("123".to_string()), + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + vec![dto::Model { + id: 2, + name: "Test Parent Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: Some("123".to_string()), + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).append_exec_results( + [ MockExecResult { + last_insert_id: 1, + rows_affected: 1, + }] + ).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let datakey = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: Some(2), + fingerprint: "".to_string(), + serial_number: Some("123".to_string()), + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: Some(ParentKey{ + name: "Test Parent Key".to_string(), + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + attributes: HashMap::new(), + }), + }; + assert_eq!( + datakey_repository.create(datakey.clone()).await?, datakey + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"INSERT INTO `data_key` (`id`, `name`, `description`, `visibility`, `user`, `attributes`, `key_type`, `parent_id`, `fingerprint`, `serial_number`, `private_key`, `public_key`, `certificate`, `create_at`, `expire_at`, `key_state`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, + [1i32.into(), "Test Key".into(), "".into(), "public".into(), 0i32.into(), "{}".into(), "pgp".into(), 2i32.into(), "".into(), "123".into(), "0708090A".into(), "040506".into(), "010203".into(), now.clone().into(), now.clone().into(), "disabled".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`id` = ? AND `data_key`.`key_state` <> ? GROUP BY `data_key`.`id` LIMIT ?"#, + ["delete".into(), "revoke".into(), 1i32.into(), "deleted".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`id` = ? AND `data_key`.`key_state` <> ? GROUP BY `data_key`.`id` LIMIT ?"#, + ["delete".into(), "revoke".into(), 2i32.into(), "deleted".into(), 1u64.into()] + ) + ] + ); + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_get_keys_by_parent_id_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }, dto::Model { + id: 2, + name: "Test Key2".to_string(), + description: "".to_string(), + visibility: Visibility::Public.to_string(), + user: 0, + attributes: "{}".to_string(), + key_type: "pgp".to_string(), + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: "0708090A".to_string(), + public_key: "040506".to_string(), + certificate: "010203".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + key_state: "disabled".to_string(), + user_email: None, + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }], + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let datakey1 = DataKey{ + id: 1, + name: "Test Key".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + let datakey2 = DataKey{ + id: 2, + name: "Test Key2".to_string(), + description: "".to_string(), + visibility: Visibility::Public, + user: 0, + attributes: HashMap::new(), + key_type: KeyType::OpenPGP, + parent_id: None, + fingerprint: "".to_string(), + serial_number: None, + private_key: vec![7,8,9,10], + public_key: vec![4,5,6], + certificate: vec![1,2,3], + create_at: now.clone(), + expire_at: now.clone(), + key_state: KeyState::Disabled, + user_email: None, + request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + assert_eq!( + datakey_repository.get_by_parent_id(1).await?, vec![datakey1, datakey2] + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `data_key`.`id`, `data_key`.`name`, `data_key`.`description`, `data_key`.`visibility`, `data_key`.`user`, `data_key`.`attributes`, `data_key`.`key_type`, `data_key`.`parent_id`, `data_key`.`fingerprint`, `data_key`.`serial_number`, `data_key`.`private_key`, `data_key`.`public_key`, `data_key`.`certificate`, `data_key`.`create_at`, `data_key`.`expire_at`, `data_key`.`key_state`, user_table.email as user_email, GROUP_CONCAT(request_delete_table.user_email) as request_delete_users, GROUP_CONCAT(request_revoke_table.user_email) as request_revoke_users FROM `data_key` INNER JOIN `user` AS `user_table` ON `user_table`.`id` = `data_key`.`user` LEFT JOIN `pending_operation` AS `request_delete_table` ON `request_delete_table`.`key_id` = `data_key`.`id` AND `request_delete_table`.`request_type` = ? LEFT JOIN `pending_operation` AS `request_revoke_table` ON `request_revoke_table`.`key_id` = `data_key`.`id` AND `request_revoke_table`.`request_type` = ? WHERE `data_key`.`parent_id` = ? AND `data_key`.`key_state` <> ? GROUP BY `data_key`.`id`"#, + ["delete".into(), "revoke".into(), 1i32.into(), "deleted".into()] + ), + ] + ); + Ok(()) + } + + #[tokio::test] + async fn test_datakey_repository_create_delete_pending_operation_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let datakey_repository = DataKeyRepository::new(&db); + let mut tx = db.begin().await?; + assert_eq!( + datakey_repository.create_pending_operation(request_dto::Model { + id: 0, + user_id: 1, + key_id: 1, + request_type: RequestType::Delete.to_string(), + user_email: "fake_email".to_string(), + create_at: now, + }, &mut tx).await?, ()); + tx.commit().await?; + //TODO 1.Now mock database begin statement is configured with postgres backend, enabled this when fixed in upstream + // assert_eq!( + // db.into_transaction_log(), + // [ + // Transaction::many( + // [ + // Statement::from_sql_and_values(DatabaseBackend::Postgres, + // r#"BEGIN"#, + // []), + // Statement::from_sql_and_values( + // DatabaseBackend::MySql, + // r#"INSERT INTO `pending_operation` (`user_id`, `key_id`, `request_type`, `user_email`, `create_at`) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE `id` = VALUES(`id`)"#, + // [1i32.into(), 1i32.into(), "delete".into(), "fake_email".into()] + // ), + // Statement::from_sql_and_values( + // DatabaseBackend::MySql, + // r#"COMMIT"#, + // [] + // ), + // ], + // ), + // ] + // ); + Ok(()) + } + +} + diff --git a/src/infra/database/model/mod.rs b/src/infra/database/model/mod.rs index 82c6425728284c262c5983b1811842cd486d7581..4953ee7363fff7e645e3e8098cf4bbd51c77da98 100644 --- a/src/infra/database/model/mod.rs +++ b/src/infra/database/model/mod.rs @@ -2,4 +2,6 @@ pub mod clusterkey; pub mod datakey; pub mod user; pub mod token; -pub mod request_delete; \ No newline at end of file +pub mod request_delete; +pub mod x509_revoked_key; +pub mod x509_crl_content; \ No newline at end of file diff --git a/src/infra/database/model/request_delete/dto.rs b/src/infra/database/model/request_delete/dto.rs index 3bf334ef97235f3a37a9d330588e6340b4b8d67c..80426ff894996bf7c16cf46ee953fe2034026373 100644 --- a/src/infra/database/model/request_delete/dto.rs +++ b/src/infra/database/model/request_delete/dto.rs @@ -15,11 +15,13 @@ */ use std::fmt::{Display, Formatter}; use std::str::FromStr; -use sqlx::FromRow; use chrono::{DateTime, Utc}; -use crate::domain::datakey::entity::{RevokedKey, X509RevokeReason}; use crate::util::error::Error; +use sqlx::types::chrono; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + #[derive(Debug, Clone, PartialEq, sqlx::Type)] pub enum RequestType { #[sqlx(rename = "delete")] @@ -49,57 +51,25 @@ impl FromStr for RequestType { } } -#[derive(Debug, FromRow)] -pub struct RevokedKeyDTO { +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "pending_operation")] +pub struct Model { + #[sea_orm(primary_key)] pub id: i32, + pub user_id: i32, pub key_id: i32, - pub ca_id: i32, - pub reason: String, - pub serial_number: Option, + pub request_type: String, + pub user_email: String, pub create_at: DateTime, } -impl RevokedKeyDTO { - pub fn new(key_id: i32, ca_id: i32, reason: X509RevokeReason) -> Self { - Self { - id: 0, - key_id, - ca_id, - create_at: Utc::now(), - reason: reason.to_string(), - serial_number: None, - } - } -} +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} -impl TryFrom for RevokedKey { - type Error = Error; - - fn try_from(dto: RevokedKeyDTO) -> Result { - Ok(RevokedKey { - id: dto.id, - key_id: dto.key_id, - ca_id: dto.ca_id, - reason: X509RevokeReason::from_str(&dto.reason)?, - create_at: dto.create_at, - serial_number: dto.serial_number, - }) - } -} +impl ActiveModelBehavior for ActiveModel {} - -#[derive(Debug, FromRow)] -pub struct PendingOperationDTO { - pub id: i32, - pub user_id: i32, - pub key_id: i32, - pub request_type: RequestType, - pub user_email: String, - pub create_at: DateTime, -} - -impl PendingOperationDTO { +impl Model { pub fn new_for_delete(key_id: i32, user_id: i32, user_email: String) -> Self { Self { id: 0, @@ -107,7 +77,7 @@ impl PendingOperationDTO { key_id, user_email, create_at: Utc::now(), - request_type: RequestType::Delete, + request_type: RequestType::Delete.to_string(), } } @@ -118,7 +88,7 @@ impl PendingOperationDTO { key_id, user_email, create_at: Utc::now(), - request_type: RequestType::Revoke, + request_type: RequestType::Revoke.to_string(), } } } @@ -126,7 +96,6 @@ impl PendingOperationDTO { #[cfg(test)] mod tests { use super::*; - use chrono::Utc; #[test] fn test_request_type_display() { @@ -146,22 +115,11 @@ mod tests { assert_eq!(revoke, RequestType::Revoke); } - #[test] - fn test_revoked_key_dto_conversion() { - let now = Utc::now(); - let dto = RevokedKeyDTO::new(1, 2, X509RevokeReason::KeyCompromise); - let revoked_key = RevokedKey::try_from(dto).unwrap(); - assert_eq!(revoked_key.key_id, 1); - assert_eq!(revoked_key.ca_id, 2); - assert_eq!(revoked_key.reason, X509RevokeReason::KeyCompromise); - assert!(revoked_key.create_at > now); - } - #[test] fn test_pending_operation_dto() { - let delete_dto = PendingOperationDTO::new_for_delete(1, 2, "test@email.com".into()); - assert_eq!(delete_dto.request_type, RequestType::Delete); - let revoke_dto = PendingOperationDTO::new_for_revoke(3, 4, "test2@email.com".into()); - assert_eq!(revoke_dto.request_type, RequestType::Revoke); + let delete_dto = Model::new_for_delete(1, 2, "test@email.com".into()); + assert_eq!(delete_dto.request_type, RequestType::Delete.to_string()); + let revoke_dto = Model::new_for_revoke(3, 4, "test2@email.com".into()); + assert_eq!(revoke_dto.request_type, RequestType::Revoke.to_string()); } } diff --git a/src/infra/database/model/token/dto.rs b/src/infra/database/model/token/dto.rs index 12084a69746294ca6ee3b1e2a9c6193f5a8c6638..0b3f5b024f34ab7ef6aed5e762dbea7ae1d0b0c0 100644 --- a/src/infra/database/model/token/dto.rs +++ b/src/infra/database/model/token/dto.rs @@ -13,14 +13,16 @@ * * // See the Mulan PSL v2 for more details. * */ -use sqlx::FromRow; use chrono::{DateTime, Utc}; use crate::domain::token::entity::Token; -use crate::util::key::get_token_hash; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Debug, FromRow, Clone)] -pub(super) struct TokenDTO { +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "token")] +pub struct Model { + #[sea_orm(primary_key)] pub id: i32, pub user_id: i32, pub description: String, @@ -29,21 +31,13 @@ pub(super) struct TokenDTO { pub expire_at: DateTime, } -impl From for TokenDTO { - fn from(token: Token) -> Self { - Self { - id: token.id, - user_id: token.user_id, - description: token.description.clone(), - token: get_token_hash(&token.token), - create_at: token.create_at, - expire_at: token.expire_at, - } - } -} +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} -impl From for Token { - fn from(dto: TokenDTO) -> Self { +impl From for Token { + fn from(dto: Model) -> Self { Self { id: dto.id, user_id: dto.user_id, @@ -59,25 +53,10 @@ impl From for Token { mod tests { use super::*; use chrono::Utc; - - #[test] - fn test_token_dto_from_entity() { - let token = Token::new(1, "Test token".to_string(), "abc123".to_string()).unwrap(); - let token_hash = get_token_hash(&token.token); - let dto = TokenDTO::from(token.clone()); - assert_eq!(dto.id, token.id); - assert_eq!(dto.user_id, token.user_id); - assert_eq!(dto.description, token.description); - assert_ne!(dto.token, token.token); - assert_eq!(dto.token, token_hash); - assert_eq!(dto.create_at, token.create_at); - assert_eq!(dto.expire_at, token.expire_at); - } - #[test] fn test_token_entity_from_dto() { let now = Utc::now(); - let dto = TokenDTO { + let dto = Model { id: 1, user_id: 2, description: "Test token".to_string(), diff --git a/src/infra/database/model/token/repository.rs b/src/infra/database/model/token/repository.rs index 4677be7e3f2ed9efe015fe398955e8beb0909f20..aaa5efccf9a50527efffee9ffb44c2cf3dbc71aa 100644 --- a/src/infra/database/model/token/repository.rs +++ b/src/infra/database/model/token/repository.rs @@ -14,80 +14,303 @@ * */ -use crate::infra::database::pool::DbPool; use crate::domain::token::entity::{Token}; use crate::domain::token::repository::Repository; use crate::util::error::Result; use async_trait::async_trait; -use std::boxed::Box; - -use crate::infra::database::model::token::dto::TokenDTO; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, ActiveValue::Set, Condition, ActiveModelTrait}; +use crate::infra::database::model::token; +use crate::infra::database::model::token::dto::Entity as TokenDTO; +use crate::util::error; use crate::util::key::get_token_hash; #[derive(Clone)] -pub struct TokenRepository { - db_pool: DbPool, +pub struct TokenRepository<'a> { + db_connection: &'a DatabaseConnection } -impl TokenRepository { - pub fn new(db_pool: DbPool) -> Self { +impl<'a> TokenRepository<'a> { + pub fn new(db_connection: &'a DatabaseConnection) -> Self { Self { - db_pool, + db_connection, } } } #[async_trait] -impl Repository for TokenRepository { - +impl<'a> Repository for TokenRepository<'a> { async fn create(&self, token: Token) -> Result { - let dto = TokenDTO::from(token); - let record : u64 = sqlx::query("INSERT INTO token(user_id, description, token, create_at, expire_at) VALUES (?, ?, ?, ?, ?)") - .bind(dto.user_id) - .bind(&dto.description) - .bind(&dto.token) - .bind(dto.create_at) - .bind(dto.expire_at) - .execute(&self.db_pool) - .await?.last_insert_id(); - self.get_token_by_id(record as i32).await + let token = token::dto::ActiveModel { + user_id: Set(token.user_id), + description: Set(token.description), + token: Set(get_token_hash(&token.token)), + create_at:Set(token.create_at), + expire_at: Set(token.expire_at), + ..Default::default() + }; + Ok(Token::from(token.insert(self.db_connection).await?)) } async fn get_token_by_id(&self, id: i32) -> Result { - let selected: TokenDTO = sqlx::query_as("SELECT * FROM token WHERE id = ?") - .bind(id) - .fetch_one(&self.db_pool) - .await?; - Ok(Token::from(selected)) + match TokenDTO::find_by_id(id).one(self.db_connection).await? { + None => { + Err(error::Error::NotFoundError) + } + Some(token) => { + Ok(Token::from(token)) + } + } } async fn get_token_by_value(&self, token: &str) -> Result { - let selected: TokenDTO = sqlx::query_as("SELECT * FROM token WHERE token = ?") - .bind(get_token_hash(token)) - .fetch_one(&self.db_pool) - .await?; - Ok(Token::from(selected)) + match TokenDTO::find().filter( + token::dto::Column::Token.eq(get_token_hash(token))).one( + self.db_connection).await? { + None => { + Err(error::Error::NotFoundError) + } + Some(token) => { + Ok(Token::from(token)) + } + } } async fn delete_by_user_and_id(&self, id: i32, user_id: i32) -> Result<()> { - let _: Option = sqlx::query_as("DELETE FROM token where id = ? AND user_id = ?") - .bind(id) - .bind(user_id) - .fetch_optional(&self.db_pool) + let _ = TokenDTO::delete_many().filter(Condition::all() + .add(token::dto::Column::Id.eq(id)) + .add(token::dto::Column::UserId.eq(user_id))).exec(self.db_connection) .await?; Ok(()) } async fn get_token_by_user_id(&self, id: i32) -> Result> { - let dtos: Vec = sqlx::query_as("SELECT * FROM token WHERE user_id = ?") - .bind(id) - .fetch_all(&self.db_pool) - .await?; + let tokens = TokenDTO::find().filter( + token::dto::Column::UserId.eq(id)).all(self.db_connection).await?; let mut results = vec![]; - for dto in dtos.into_iter() { + for dto in tokens.into_iter() { results.push(Token::from(dto)); } Ok(results) } } + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use crate::domain::token::entity::Token; + use crate::domain::token::repository::Repository; + use crate::infra::database::model::token::dto; + use crate::util::error::Result; + use crate::infra::database::model::token::repository::{TokenRepository}; + use crate::util::key::get_token_hash; + + #[tokio::test] + async fn test_token_repository_create_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }], + ]).append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let token_repository = TokenRepository::new(&db); + let user = Token{ + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }; + assert_eq!( + token_repository.create(user).await?, + Token::from(dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }) + ); + let hashed_token = get_token_hash("random_number"); + assert_eq!( + db.into_transaction_log(), + [ + //create + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"INSERT INTO `token` (`user_id`, `description`, `token`, `create_at`, `expire_at`) VALUES (?, ?, ?, ?, ?)"#, + [0i32.into(), "fake_token".into(), hashed_token.into(), now.clone().into(), now.clone().into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `token`.`id`, `token`.`user_id`, `token`.`description`, `token`.`token`, `token`.`create_at`, `token`.`expire_at` FROM `token` WHERE `token`.`id` = ? LIMIT ?"#, + [1i32.into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_token_repository_delete_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }], + ]).append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let token_repository = TokenRepository::new(&db); + assert_eq!(token_repository.delete_by_user_and_id(1, 1).await?, ()); + assert_eq!( + db.into_transaction_log(), + [ + //delete + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"DELETE FROM `token` WHERE `token`.`id` = ? AND `token`.`user_id` = ?"#, + [1i32.into(), 1i32.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_token_repository_query_sql_statement() -> Result<()> { + let now = chrono::Utc::now(); + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }], + vec![dto::Model { + id: 2, + user_id: 0, + description: "fake_token2".to_string(), + token: "random_number2".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }], + vec![dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }, dto::Model { + id: 2, + user_id: 0, + description: "fake_token2".to_string(), + token: "random_number2".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }], + ]).into_connection(); + + let token_repository = TokenRepository::new(&db); + assert_eq!( + token_repository.get_token_by_id(1).await?, + Token::from(dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }) + ); + assert_eq!( + token_repository.get_token_by_value("fake_content").await?, + Token::from(dto::Model { + id: 2, + user_id: 0, + description: "fake_token2".to_string(), + token: "random_number2".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }) + ); + + assert_eq!( + token_repository.get_token_by_user_id(0).await?, + vec![ + Token::from(dto::Model { + id: 1, + user_id: 0, + description: "fake_token".to_string(), + token: "random_number".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + }), + Token::from(dto::Model { + id: 2, + user_id: 0, + description: "fake_token2".to_string(), + token: "random_number2".to_string(), + create_at: now.clone(), + expire_at: now.clone(), + })] + ); + + let hashed_token = get_token_hash("fake_content"); + assert_eq!( + db.into_transaction_log(), + [ + //get_token_by_id + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `token`.`id`, `token`.`user_id`, `token`.`description`, `token`.`token`, `token`.`create_at`, `token`.`expire_at` FROM `token` WHERE `token`.`id` = ? LIMIT ?"#, + [1i32.into(), 1u64.into()] + ), + //get_token_by_value + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `token`.`id`, `token`.`user_id`, `token`.`description`, `token`.`token`, `token`.`create_at`, `token`.`expire_at` FROM `token` WHERE `token`.`token` = ? LIMIT ?"#, + [hashed_token.into(), 1u64.into()] + ), + //get_token_by_user_id + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `token`.`id`, `token`.`user_id`, `token`.`description`, `token`.`token`, `token`.`create_at`, `token`.`expire_at` FROM `token` WHERE `token`.`user_id` = ?"#, + [0i32.into()] + ), + ] + ); + + Ok(()) + } +} diff --git a/src/infra/database/model/user/dto.rs b/src/infra/database/model/user/dto.rs index c9011aae655c613eb2a2b27a0bf9514a4c721cae..a80cf180aff4b04675faf920d6f6d5c160a88770 100644 --- a/src/infra/database/model/user/dto.rs +++ b/src/infra/database/model/user/dto.rs @@ -13,26 +13,39 @@ * * // See the Mulan PSL v2 for more details. * */ -use sqlx::FromRow; use crate::domain::user::entity::User; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Debug, FromRow)] -pub(super) struct UserDTO { +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "user")] +pub struct Model { + #[sea_orm(primary_key)] pub id: i32, pub email: String } -impl From for UserDTO { - fn from(user: User) -> Self { - Self { - id: user.id, - email: user.email, - } +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::super::datakey::dto::Entity", + from = "Column::Id", + to = "super::super::datakey::dto::Column::User" + )] + Datakey, +} + +// `Related` trait has to be implemented by hand +impl Related for Entity { + fn to() -> RelationDef { + Relation::Datakey.def() } } -impl From for User { - fn from(dto: UserDTO) -> Self { +impl ActiveModelBehavior for ActiveModel {} + +impl From for User { + fn from(dto: Model) -> Self { Self { id: dto.id, email: dto.email diff --git a/src/infra/database/model/user/repository.rs b/src/infra/database/model/user/repository.rs index 4f1775ef8aaa4717ab68d247e905e031c5081f92..f6ecaf8d67d3092d8a7706d9807cba06abe42b9c 100644 --- a/src/infra/database/model/user/repository.rs +++ b/src/infra/database/model/user/repository.rs @@ -14,30 +14,29 @@ * */ -use super::dto::UserDTO; - -use crate::infra::database::pool::DbPool; +use super::dto::Entity as UserDTO; +use crate::infra::database::model::user; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, ActiveValue::Set, ActiveModelTrait}; use crate::domain::user::entity::User; use crate::domain::user::repository::Repository; -use crate::util::error::Result; +use crate::util::error::{Error, Result}; use async_trait::async_trait; -use std::boxed::Box; #[derive(Clone)] -pub struct UserRepository { - db_pool: DbPool, +pub struct UserRepository<'a> { + db_connection: &'a DatabaseConnection } -impl UserRepository { - pub fn new(db_pool: DbPool) -> Self { +impl<'a> UserRepository<'a> { + pub fn new(db_connection: &'a DatabaseConnection) -> Self { Self { - db_pool, + db_connection, } } } #[async_trait] -impl Repository for UserRepository { +impl<'a> Repository for UserRepository<'a> { async fn create(&self, user: User) -> Result { return match self.get_by_email(&user.email).await { @@ -45,37 +44,189 @@ impl Repository for UserRepository { Ok(existed) } Err(_err) => { - let dto = UserDTO::from(user); - let record : u64 = sqlx::query("INSERT INTO user(email) VALUES (?)") - .bind(&dto.email) - .execute(&self.db_pool) - .await?.last_insert_id(); - self.get_by_id(record as i32).await + let user = user::dto::ActiveModel { + email: Set(user.email), + ..Default::default() + }; + Ok(User::from(user.insert(self.db_connection).await?)) } } } async fn get_by_id(&self, id: i32) -> Result { - let selected: UserDTO = sqlx::query_as("SELECT * FROM user WHERE id = ?") - .bind(id) - .fetch_one(&self.db_pool) - .await?; - Ok(User::from(selected)) + match UserDTO::find_by_id(id).one( + self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(user) => { + Ok(User::from(user)) + } + } } async fn get_by_email(&self, email: &str) -> Result { - let selected: UserDTO = sqlx::query_as("SELECT * FROM user WHERE email = ?") - .bind(email) - .fetch_one(&self.db_pool) - .await?; - Ok(User::from(selected)) + match UserDTO::find().filter( + user::dto::Column::Email.eq(email)).one( + self.db_connection).await? { + None => { + Err(Error::NotFoundError) + } + Some(user) => { + Ok(User::from(user)) + } + } } async fn delete_by_id(&self, id: i32) -> Result<()> { - let _: Option = sqlx::query_as("DELETE FROM user where id = ?") - .bind(id) - .fetch_optional(&self.db_pool) + let _ = UserDTO::delete_by_id(id).exec(self.db_connection) .await?; Ok(()) } } + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use crate::domain::user::entity::User; + use crate::domain::user::repository::Repository; + use crate::infra::database::model::user::dto; + use crate::util::error::Result; + use crate::infra::database::model::user::repository::UserRepository; + + #[tokio::test] + async fn test_user_repository_query_sql_statement() -> Result<()> { + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + email: "fake_email".to_string(), + }], + vec![dto::Model { + id: 2, + email: "fake_email".to_string(), + }], + ]).into_connection(); + + let user_repository = UserRepository::new(&db); + assert_eq!( + user_repository.get_by_email("fake_email").await?, + User::from(dto::Model { + id: 1, + email: "fake_email".to_string(), + }) + ); + + assert_eq!( + user_repository.get_by_id(1).await?, + User::from(dto::Model { + id: 2, + email: "fake_email".to_string(), + }) + ); + + assert_eq!( + db.into_transaction_log(), + [ + //get_by_email + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `user`.`id`, `user`.`email` FROM `user` WHERE `user`.`email` = ? LIMIT ?"#, + ["fake_email".into(), 1u64.into()] + ), + //get_by_id + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `user`.`id`, `user`.`email` FROM `user` WHERE `user`.`id` = ? LIMIT ?"#, + [1i32.into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_user_repository_create_sql_statement() -> Result<()> { + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![], + vec![dto::Model { + id: 3, + email: "fake_email".to_string(), + }], + ]).append_exec_results([ + MockExecResult{ + last_insert_id: 3, + rows_affected: 1, + } + ]).into_connection(); + + let user_repository = UserRepository::new(&db); + let user = User{ + id: 0, + email: "fake_string".to_string(), + }; + assert_eq!( + user_repository.create(user).await?, + User::from(dto::Model { + id: 3, + email: "fake_email".to_string(), + }) + ); + assert_eq!( + db.into_transaction_log(), + [ + //create + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `user`.`id`, `user`.`email` FROM `user` WHERE `user`.`email` = ? LIMIT ?"#, + ["fake_string".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"INSERT INTO `user` (`email`) VALUES (?)"#, + ["fake_string".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"SELECT `user`.`id`, `user`.`email` FROM `user` WHERE `user`.`id` = ? LIMIT ?"#, + [3i32.into(), 1u64.into()] + ), + ] + ); + + Ok(()) + } + #[tokio::test] + async fn test_user_repository_delete_sql_statement() -> Result<()> { + let db = MockDatabase::new(DatabaseBackend::MySql) + .append_query_results([ + vec![dto::Model { + id: 1, + email: "fake_email".to_string(), + }], + ]).append_exec_results([ + MockExecResult{ + last_insert_id: 1, + rows_affected: 1, + } + ]).into_connection(); + + let user_repository = UserRepository::new(&db); + assert_eq!(user_repository.delete_by_id(1).await?, ()); + assert_eq!( + db.into_transaction_log(), + [ + //delete + Transaction::from_sql_and_values( + DatabaseBackend::MySql, + r#"DELETE FROM `user` WHERE `user`.`id` = ?"#, + [1i32.into()] + ), + ] + ); + + Ok(()) + } +} diff --git a/src/infra/database/model/x509_crl_content/dto.rs b/src/infra/database/model/x509_crl_content/dto.rs new file mode 100644 index 0000000000000000000000000000000000000000..45e4e330212fc5ba92db7ddb5af68e2fdc687c1d --- /dev/null +++ b/src/infra/database/model/x509_crl_content/dto.rs @@ -0,0 +1,82 @@ +/* + * + * * // Copyright (c) 2023 Huawei Technologies Co.,Ltd. All rights reserved. + * * // + * * // signatrust is licensed under Mulan PSL v2. + * * // You can use this software according to the terms and conditions of the Mulan + * * // PSL v2. + * * // You may obtain a copy of Mulan PSL v2 at: + * * // http://license.coscl.org.cn/MulanPSL2 + * * // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY + * * // KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * * // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * * // See the Mulan PSL v2 for more details. + * + */ +use chrono::{DateTime, Utc}; +use crate::domain::datakey::entity::{X509CRL}; +use crate::util::error::Error; + +use sqlx::types::chrono; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; +use crate::util::key::{decode_hex_string_to_u8, encode_u8_to_hex_string}; + + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "x509_crl_content")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub ca_id: i32, + pub data: String, + pub create_at: DateTime, + pub update_at: DateTime, +} + +impl TryFrom for Model { + type Error = Error; + + fn try_from(value: X509CRL) -> Result { + Ok(Model { + id: value.id, + ca_id: value.ca_id, + data: encode_u8_to_hex_string(&value.data), + create_at: value.create_at, + update_at: value.update_at, + }) + } +} + +impl TryFrom for X509CRL { + type Error = Error; + + fn try_from(value: Model) -> Result { + Ok(X509CRL { + id: value.id, + ca_id: value.ca_id, + data: decode_hex_string_to_u8(&value.data), + create_at: value.create_at, + update_at: value.update_at, + }) + } +} + + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::super::datakey::dto::Entity", + from = "Column::CaId", + to = "super::super::datakey::dto::Column::Id" + )] + Datakey, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Datakey.def() + } +} +impl ActiveModelBehavior for ActiveModel {} + diff --git a/src/infra/database/model/x509_crl_content/mod.rs b/src/infra/database/model/x509_crl_content/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..a07dce5c05c690e53c1a9e66d4c860769aa719b8 --- /dev/null +++ b/src/infra/database/model/x509_crl_content/mod.rs @@ -0,0 +1 @@ +pub mod dto; diff --git a/src/infra/database/model/x509_revoked_key/dto.rs b/src/infra/database/model/x509_revoked_key/dto.rs new file mode 100644 index 0000000000000000000000000000000000000000..dff5d854ac6763046a5c48f8f3ddeeb3819afe23 --- /dev/null +++ b/src/infra/database/model/x509_revoked_key/dto.rs @@ -0,0 +1,76 @@ +/* + * + * * // Copyright (c) 2023 Huawei Technologies Co.,Ltd. All rights reserved. + * * // + * * // signatrust is licensed under Mulan PSL v2. + * * // You can use this software according to the terms and conditions of the Mulan + * * // PSL v2. + * * // You may obtain a copy of Mulan PSL v2 at: + * * // http://license.coscl.org.cn/MulanPSL2 + * * // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY + * * // KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * * // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * * // See the Mulan PSL v2 for more details. + * + */ +use std::str::FromStr; +use chrono::{DateTime, Utc}; +use crate::domain::datakey::entity::{RevokedKey, X509RevokeReason}; +use crate::util::error::Error; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "x509_keys_revoked")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub key_id: i32, + pub ca_id: i32, + pub reason: String, + pub serial_number: Option, + pub create_at: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} +impl TryFrom for RevokedKey { + type Error = Error; + + fn try_from(dto: Model) -> Result { + Ok(RevokedKey { + id: dto.id, + key_id: dto.key_id, + ca_id: dto.ca_id, + reason: X509RevokeReason::from_str(&dto.reason)?, + create_at: dto.create_at, + serial_number: dto.serial_number, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + #[test] + fn test_revoked_key_dto_conversion() { + let now = Utc::now(); + let dto = Model{ + id: 0, + key_id: 1, + ca_id: 2, + reason: X509RevokeReason::KeyCompromise.to_string(), + serial_number: None, + create_at: now, + }; + let revoked_key = RevokedKey::try_from(dto).unwrap(); + assert_eq!(revoked_key.key_id, 1); + assert_eq!(revoked_key.ca_id, 2); + assert_eq!(revoked_key.reason, X509RevokeReason::KeyCompromise); + assert_eq!(revoked_key.create_at, now); + } +} diff --git a/src/infra/database/model/x509_revoked_key/mod.rs b/src/infra/database/model/x509_revoked_key/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..a07dce5c05c690e53c1a9e66d4c860769aa719b8 --- /dev/null +++ b/src/infra/database/model/x509_revoked_key/mod.rs @@ -0,0 +1 @@ +pub mod dto; diff --git a/src/infra/database/pool.rs b/src/infra/database/pool.rs index d36503fc7078b23fb596e3c450e9fdb243d237d2..f9d49ca194bf57d2c4bd6474be328b32df36b1e5 100644 --- a/src/infra/database/pool.rs +++ b/src/infra/database/pool.rs @@ -14,17 +14,19 @@ * */ -use crate::util::error; use config::Value; use once_cell::sync::OnceCell; -use sqlx::mysql::{MySql, MySqlPoolOptions}; +use sqlx::mysql::{MySql}; use sqlx::pool::Pool; use std::collections::HashMap; +use std::time::Duration; +use sea_orm::{DatabaseConnection, ConnectOptions, Database}; use crate::util::error::{Error, Result}; pub type DbPool = Pool; -static DB_POOL: OnceCell = OnceCell::new(); +//Now we have database pool for sqlx framework and database connection for sea-orm framework, +static DB_CONNECTION: OnceCell = OnceCell::new(); pub async fn create_pool(config: &HashMap) -> Result<()> { let max_connections: u32 = config @@ -48,37 +50,26 @@ pub async fn create_pool(config: &HashMap) -> Result<()> { db_connection ))); } - let pool = MySqlPoolOptions::new() - .max_connections(max_connections) - .connect(db_connection.as_str()) - .await - .map_err(Error::from)?; - DB_POOL.set(pool).expect("db pool configured"); - ping().await?; + //initialize the database connection + let mut opt = ConnectOptions::new(db_connection); + opt.max_connections(max_connections) + .min_connections(5) + .connect_timeout(Duration::from_secs(8)) + .acquire_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .max_lifetime(Duration::from_secs(8)) + .sqlx_logging(true) + .sqlx_logging_level(log::LevelFilter::Info); + + DB_CONNECTION.set(Database::connect(opt).await?).expect("database connection configured"); + get_db_connection()?.ping().await.expect("database connection failed"); Ok(()) } - -pub fn get_db_pool() -> Result { - return match DB_POOL.get() { - None => Err(error::Error::DatabaseError( +pub fn get_db_connection() -> Result<&'static DatabaseConnection> { + return match DB_CONNECTION.get() { + None => Err(Error::DatabaseError( "failed to get database pool".to_string(), )), - Some(pool) => Ok(pool.clone()), + Some(pool) => Ok(pool), }; -} - -pub async fn ping() -> Result<()> { - info!("Checking on database connection..."); - let pool = get_db_pool(); - match pool { - Ok(pool) => { - sqlx::query("SELECT 1") - .fetch_one(&pool) - .await - .expect("Failed to PING database"); - info!("Database PING executed successfully!"); - } - Err(e) => return Err(Error::DatabaseError(e.to_string())), - } - Ok(()) -} +} \ No newline at end of file diff --git a/src/infra/sign_backend/factory.rs b/src/infra/sign_backend/factory.rs index 98b04ba710de38617133eb3ccf4cb00ec60892f5..980418796f48e21ff1eb6072fb5059c15042543d 100644 --- a/src/infra/sign_backend/factory.rs +++ b/src/infra/sign_backend/factory.rs @@ -19,19 +19,19 @@ use crate::domain::sign_service::{SignBackend, SignBackendType}; use crate::util::error::{Result}; use std::sync::{Arc, RwLock}; use config::{Config}; -use crate::infra::database::pool::DbPool; +use sea_orm::DatabaseConnection; use crate::infra::sign_backend::memory::backend::MemorySignBackend; pub struct SignBackendFactory {} impl SignBackendFactory { - pub async fn new_engine(config: Arc>, db_pool: DbPool) -> Result> { + pub async fn new_engine(config: Arc>, db_connection: &'static DatabaseConnection) -> Result> { let engine_type = SignBackendType::from_str( config.read()?.get_string("sign-backend.type")?.as_str(), )?; info!("sign backend configured with plugin {:?}", engine_type); match engine_type { - SignBackendType::Memory => Ok(Box::new(MemorySignBackend::new(config, db_pool).await?)), + SignBackendType::Memory => Ok(Box::new(MemorySignBackend::new(config, db_connection).await?)), } } } diff --git a/src/infra/sign_backend/memory/backend.rs b/src/infra/sign_backend/memory/backend.rs index a1b51b4bbd36f41cc3ede0c29f3652d6da192cbc..176a8c5e656636647852b9ee43343c673d0f483b 100644 --- a/src/infra/sign_backend/memory/backend.rs +++ b/src/infra/sign_backend/memory/backend.rs @@ -24,7 +24,6 @@ use config::Config; use std::sync::RwLock; use crate::infra::database::model::clusterkey::repository; -use crate::infra::database::pool::{DbPool}; use crate::infra::kms::factory; use crate::infra::encryption::engine::{EncryptionEngineWithClusterKey}; use crate::domain::encryption_engine::EncryptionEngine; @@ -34,6 +33,7 @@ use crate::domain::datakey::entity::DataKey; use crate::util::error::{Error, Result}; use async_trait::async_trait; use chrono::{DateTime, Utc}; +use sea_orm::DatabaseConnection; use crate::infra::encryption::algorithm::factory::AlgorithmFactory; @@ -50,13 +50,13 @@ impl MemorySignBackend { /// 2. initialize the cluster repo /// 2. initialize the encryption engine including the cluster key /// 3. initialize the signing plugins - pub async fn new(server_config: Arc>, db_pool: DbPool) -> Result { + pub async fn new(server_config: Arc>, db_connection: &'static DatabaseConnection) -> Result { //initialize the kms backend let kms_provider = factory::KMSProviderFactory::new_provider( &server_config.read()?.get_table("memory.kms-provider")? )?; let repository = - repository::ClusterKeyRepository::new(db_pool); + repository::ClusterKeyRepository::new(db_connection); let engine_config = server_config.read()?.get_table("memory.encryption-engine")?; let encryptor = AlgorithmFactory::new_algorithm( &engine_config diff --git a/src/presentation/handler/control/datakey_handler.rs b/src/presentation/handler/control/datakey_handler.rs index 83a6a7ce544a49c20a7f7ccd0a5aee83d60ba9ae..755b05334e5934bdf5e712e5ab1fceff57ded78b 100644 --- a/src/presentation/handler/control/datakey_handler.rs +++ b/src/presentation/handler/control/datakey_handler.rs @@ -314,6 +314,7 @@ async fn cancel_revoke_data_key(user: UserIdentity, key_service: web::Data, key_service: web::Data, key_service: web::Data Result { let key = self.key_service.create(user.clone(), data).await?; - self.key_service.enable(Some(user), format!("{}", key.id)).await?; + if data.key_state == KeyState::Disabled { + self.key_service.enable(Some(user), format!("{}", key.id)).await?; + } Ok(key) } diff --git a/src/presentation/server/data_server.rs b/src/presentation/server/data_server.rs index 2727ebe5a34c1ac742f4c950d3365e244f6931e6..3316d76fed5dc1181cba8138147be0308b08a7f9 100644 --- a/src/presentation/server/data_server.rs +++ b/src/presentation/server/data_server.rs @@ -31,7 +31,7 @@ use crate::application::user::{DBUserService, UserService}; use crate::infra::database::model::datakey::repository; use crate::infra::database::model::token::repository::TokenRepository; use crate::infra::database::model::user::repository::UserRepository; -use crate::infra::database::pool::{create_pool, get_db_pool}; +use crate::infra::database::pool::{create_pool, get_db_connection}; use crate::infra::sign_backend::factory::SignBackendFactory; @@ -107,12 +107,12 @@ impl DataServer { let mut server = Server::builder(); info!("data server starts"); let sign_backend = SignBackendFactory::new_engine( - self.server_config.clone(), get_db_pool()?).await?; + self.server_config.clone(), get_db_connection()?).await?; let data_repository = repository::DataKeyRepository::new( - get_db_pool()?); + get_db_connection()?); let key_service = DBKeyService::new(data_repository, sign_backend); - let user_repo = UserRepository::new(get_db_pool()?); - let token_repo = TokenRepository::new(get_db_pool()?); + let user_repo = UserRepository::new(get_db_connection()?); + let token_repo = TokenRepository::new(get_db_connection()?); let user_service = DBUserService::new(user_repo, token_repo, self.server_config.clone())?; key_service.start_cache_cleanup_loop(self.cancel_token.clone())?; diff --git a/src/util/error.rs b/src/util/error.rs index 3e4492cb13190422fa821e4d43f7c3263fa9be49..448b2f024decabef8bcd226503675b60589a47fa 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -45,6 +45,7 @@ use anyhow::Error as AnyhowError; use csrf::CsrfError; use utoipa::{ToSchema}; use efi_signer::error::Error as EFIError; +use sea_orm::DbErr; pub type Result = std::result::Result; @@ -402,4 +403,9 @@ impl From> for Error { fn from(error: Vec) -> Self { Error::KeyParseError(format!("original vec {:?}", error)) } } +impl From for Error { + fn from(error: DbErr) -> Self { Error::DatabaseError(error.to_string()) } +} + +