From a08eb207f3b33a21860fb900d2038e9192d1eb36 Mon Sep 17 00:00:00 2001 From: Manuthor <32013169+Manuthor@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:12:44 +0200 Subject: [PATCH] feat: build a generic database upgrade mechanism (#299) * feat: build a generic database upgrade mechanism * fix: PR review + add migration doc * fix: resorb clippy lints about cast (loss or truncation) and missing #Errors in docs --- .github/workflows/build_rhel9.yml | 2 - .gitignore | 1 + CHANGELOG.md | 34 + Cargo.lock | 8 + crate/cli/src/actions/access.rs | 59 ++ crate/cli/src/actions/certificates/mod.rs | 10 + crate/cli/src/actions/console.rs | 25 + crate/cli/src/actions/cover_crypt/decrypt.rs | 4 +- crate/cli/src/actions/cover_crypt/encrypt.rs | 6 +- .../cover_crypt/keys/create_user_key.rs | 2 +- crate/cli/src/actions/cover_crypt/mod.rs | 10 + crate/cli/src/actions/cover_crypt/policy.rs | 5 +- crate/cli/src/actions/elliptic_curves/mod.rs | 10 + crate/cli/src/actions/google/mod.rs | 10 + crate/cli/src/actions/login.rs | 49 ++ crate/cli/src/actions/logout.rs | 10 + crate/cli/src/actions/markdown.rs | 5 + crate/cli/src/actions/new_database.rs | 10 + crate/cli/src/actions/rsa/mod.rs | 9 + crate/cli/src/actions/shared/export_key.rs | 8 + .../cli/src/actions/shared/get_attributes.rs | 15 + crate/cli/src/actions/shared/import_key.rs | 17 + crate/cli/src/actions/shared/locate.rs | 4 + crate/cli/src/actions/shared/unwrap_key.rs | 12 + crate/cli/src/actions/shared/wrap_key.rs | 15 + crate/cli/src/actions/symmetric/mod.rs | 10 + crate/cli/src/actions/version.rs | 9 + crate/cli/src/error/result.rs | 18 + crate/cli/src/lib.rs | 2 +- crate/cli/src/tests/certificates/encrypt.rs | 4 +- crate/cli/src/tests/shared/export.rs | 104 +-- .../tests/shared/import_export_wrapping.rs | 5 +- crate/cli/src/tests/symmetric/create_key.rs | 24 +- crate/kmip/src/crypto/symmetric/aead.rs | 2 +- crate/kmip/src/crypto/wrap/unwrap_key.rs | 2 +- crate/kmip/src/crypto/wrap/wrap_key.rs | 2 +- crate/kmip/src/openssl/private_key.rs | 110 ++-- crate/server/Cargo.toml | 2 + crate/server/src/config/command_line/db.rs | 4 +- .../src/config/command_line/http_config.rs | 2 +- crate/server/src/config/params/db_params.rs | 6 +- crate/server/src/config/params/http_params.rs | 25 +- .../server/src/config/params/server_params.rs | 33 +- crate/server/src/core/certificate/find.rs | 6 +- .../cover_crypt/create_user_decryption_key.rs | 8 +- .../server/src/core/cover_crypt/rekey_keys.rs | 6 +- .../server/src/core/extra_database_params.rs | 17 +- crate/server/src/core/implementation.rs | 20 +- crate/server/src/core/kms.rs | 4 +- .../server/src/core/operations/certify/mod.rs | 30 +- .../src/core/operations/create_key_pair.rs | 16 +- crate/server/src/core/operations/decrypt.rs | 4 +- crate/server/src/core/operations/encrypt.rs | 6 +- crate/server/src/core/operations/import.rs | 18 +- crate/server/src/core/operations/locate.rs | 2 +- crate/server/src/core/operations/message.rs | 4 +- crate/server/src/core/operations/rekey.rs | 2 +- .../src/core/operations/rekey_keypair.rs | 4 +- crate/server/src/core/operations/revoke.rs | 2 +- crate/server/src/core/operations/validate.rs | 32 +- .../src/core/operations/wrapping/unwrap.rs | 2 +- crate/server/src/database/cached_sqlcipher.rs | 154 +++-- .../src/database/cached_sqlite_struct.rs | 186 +++--- crate/server/src/database/database_trait.rs | 3 + crate/server/src/database/locate_query.rs | 4 +- crate/server/src/database/migrate.rs | 24 + crate/server/src/database/mod.rs | 48 +- crate/server/src/database/mysql.rs | 617 +++++++++++------- .../src/database/object_with_metadata.rs | 3 +- crate/server/src/database/pgsql.rs | 615 ++++++++++------- crate/server/src/database/query.sql | 25 + crate/server/src/database/query_mysql.sql | 25 + crate/server/src/database/redis/objects_db.rs | 6 +- .../server/src/database/redis/permissions.rs | 8 +- .../src/database/redis/redis_with_findex.rs | 37 +- crate/server/src/database/sqlite.rs | 616 ++++++++++------- .../tests/additional_redis_findex_tests.rs | 72 +- .../src/database/tests/database_tests.rs | 41 +- .../database/tests/find_attributes_test.rs | 6 +- crate/server/src/database/tests/mod.rs | 37 +- crate/server/src/database/tests/owner_test.rs | 4 +- crate/server/src/error.rs | 11 +- crate/server/src/kms_server.rs | 6 +- crate/server/src/lib.rs | 30 +- .../server/src/middlewares/api_token_auth.rs | 18 +- crate/server/src/middlewares/jwks.rs | 27 +- crate/server/src/middlewares/jwt.rs | 10 +- .../server/src/middlewares/jwt_token_auth.rs | 2 +- crate/server/src/middlewares/main.rs | 5 +- crate/server/src/middlewares/ssl_auth.rs | 4 +- crate/server/src/result.rs | 20 +- crate/server/src/routes/google_cse/jwt.rs | 50 +- crate/server/src/routes/google_cse/mod.rs | 2 +- .../src/routes/google_cse/operations.rs | 89 ++- crate/server/src/routes/kmip.rs | 2 +- crate/server/src/routes/mod.rs | 2 +- crate/server/src/routes/ms_dke/mod.rs | 14 +- crate/server/src/telemetry/mod.rs | 8 + .../cover_crypt_tests/integration_tests.rs | 42 +- .../integration_tests_tags.rs | 16 +- .../src/tests/cover_crypt_tests/unit_tests.rs | 10 +- crate/server/src/tests/curve_25519_tests.rs | 25 +- crate/server/src/tests/google_cse/mod.rs | 30 +- crate/server/src/tests/google_cse/utils.rs | 4 +- crate/server/src/tests/kmip_messages.rs | 8 +- crate/server/src/tests/kmip_server_tests.rs | 25 +- .../src/tests/migrate/kms_4.12.0.sqlite | Bin 0 -> 98304 bytes .../src/tests/migrate/kms_4.16.0.sqlite | Bin 0 -> 86016 bytes .../src/tests/migrate/kms_4.17.0.sqlite | Bin 0 -> 122880 bytes crate/server/src/tests/ms_dke/mod.rs | 2 + crate/server/src/tests/test_utils.rs | 10 +- crate/server/src/tests/test_validate.rs | 26 +- docker-compose.yml | 2 +- documentation/docs/cli/main_commands.md | 2 +- documentation/docs/database.md | 120 ++++ documentation/docs/high_availability_mode.md | 91 --- documentation/mkdocs.yml | 1 + 117 files changed, 2641 insertions(+), 1491 deletions(-) create mode 100644 crate/server/src/database/migrate.rs create mode 100644 crate/server/src/tests/migrate/kms_4.12.0.sqlite create mode 100644 crate/server/src/tests/migrate/kms_4.16.0.sqlite create mode 100644 crate/server/src/tests/migrate/kms_4.17.0.sqlite create mode 100644 documentation/docs/database.md diff --git a/.github/workflows/build_rhel9.yml b/.github/workflows/build_rhel9.yml index 7d5c41cc..4dcd208e 100644 --- a/.github/workflows/build_rhel9.yml +++ b/.github/workflows/build_rhel9.yml @@ -108,9 +108,7 @@ jobs: MYSQL_ROOT_PASSWORD: kms KMS_MYSQL_URL: mysql://root:kms@mariadb/kms - KMS_ENCLAVE_DIR_PATH: data/public KMS_SQLITE_PATH: data/shared - KMS_CERTBOT_SSL_PATH: data/private REDIS_HOST: redis diff --git a/.gitignore b/.gitignore index 232de6e0..38e909ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target/ .cargo_check/ +.history/ *nix *.swp TODO.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9fd4c0..fff771c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to this project will be documented in this file. +## [4.18.0] - 2024-09-XX + +### 🚀 Features + +- Add ReKey KMIP operation ([#294](https://github.com/Cosmian/kms/pull/294)) +- Add API token authentication between server and clients ([#290](https://github.com/Cosmian/kms/pull/290)) +- Build a generic database upgrade mechanism ([#299](https://github.com/Cosmian/kms/pull/299)) + +### 🐛 Bug Fixes + +- KMIP Attributes: + * In get_attributes, use attributes from ObjectWithMetadata instead of Object.Attributes ([#278](https://github.com/Cosmian/kms/pull/278)) + * When inserting in db, force Object::Attributes to be synced with Attributes ([#279](https://github.com/Cosmian/kms/pull/279)) +- Certificates handling/tasks: + * **Validate** KMIP operation: + - Simplify getting CRLs and get returned errors ([#268](https://github.com/Cosmian/kms/pull/268)) + - Validate certificate generation ([#283](https://github.com/Cosmian/kms/pull/283)) + - Use certificate file path in ckms arguments ([#292](https://github.com/Cosmian/kms/pull/292)) + * **Certify** KMIP operation: Server must sign x509 after adding X509 extensions ([#282](https://github.com/Cosmian/kms/pull/282)) +- Merge decrypt match in same function ([#295](https://github.com/Cosmian/kms/pull/295)) +- Fix Public RSA Key size in get attributes ([#275](https://github.com/Cosmian/kms/pull/275)) +- RUSTSEC: + * **RUSTSEC-2024-0357**: MemBio::get_buf has undefined behavior with empty buffers: upgrade crate `openssl` from 1.0.64 to 1.0.66 ([#280](https://github.com/Cosmian/kms/pull/280)) + * **RUSTSEC-2024-0363**: Binary Protocol Misinterpretation caused by Truncating or Overflowing Casts: bump sqlx to 0.8.1 ([#291](https://github.com/Cosmian/kms/pull/291) and [#297](https://github.com/Cosmian/kms/pull/297)) + +### ⚙️ Miscellaneous Tasks + +- **clippy** tasks: + * Only expose pub functions that need to be public ([#277](https://github.com/Cosmian/kms/pull/277)) + * Hardcode clippy lints ([#293](https://github.com/Cosmian/kms/pull/293)) +- Rename MacOS artifacts giving CPU architecture +- Configure `ckms` to build reqwest with minimal idle connections reuse ([#272](https://github.com/Cosmian/kms/pull/272)) +- Do not delete tags if none are provided ([#276](https://github.com/Cosmian/kms/pull/276)) + ## [4.17.0] - 2024-07-05 ### 🚀 Features diff --git a/Cargo.lock b/Cargo.lock index 564c9863..40fa107d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,6 +1404,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "tempfile", "thiserror", "time", "tokio", @@ -1413,6 +1414,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "version-compare", "x509-parser", "zeroize", ] @@ -4992,6 +4994,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" diff --git a/crate/cli/src/actions/access.rs b/crate/cli/src/actions/access.rs index b073e51b..6d924cf7 100644 --- a/crate/cli/src/actions/access.rs +++ b/crate/cli/src/actions/access.rs @@ -21,6 +21,15 @@ pub enum AccessAction { } impl AccessAction { + /// Processes the access action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - The KMS client used for the action. + /// + /// # Errors + /// + /// Returns an error if there was a problem running the action. pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { match self { Self::Grant(action) => action.run(kms_rest_client).await?, @@ -58,6 +67,16 @@ pub struct GrantAccess { } impl GrantAccess { + /// Runs the `GrantAccess` action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let access = Access { unique_identifier: Some(UniqueIdentifier::TextString(self.object_uid.clone())), @@ -104,6 +123,16 @@ pub struct RevokeAccess { } impl RevokeAccess { + /// Runs the `RevokeAccess` action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let access = Access { unique_identifier: Some(UniqueIdentifier::TextString(self.object_uid.clone())), @@ -138,6 +167,16 @@ pub struct ListAccessesGranted { } impl ListAccessesGranted { + /// Runs the `ListAccessesGranted` action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let accesses = kms_rest_client .list_access(&self.object_uid) @@ -164,6 +203,16 @@ impl ListAccessesGranted { pub struct ListOwnedObjects; impl ListOwnedObjects { + /// Runs the `ListOwnedObjects` action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let objects = kms_rest_client .list_owned_objects() @@ -190,6 +239,16 @@ impl ListOwnedObjects { pub struct ListAccessRightsObtained; impl ListAccessRightsObtained { + /// Runs the `ListAccessRightsObtained` action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let objects = kms_rest_client .list_access_rights_obtained() diff --git a/crate/cli/src/actions/certificates/mod.rs b/crate/cli/src/actions/certificates/mod.rs index 1557454d..08931ee1 100644 --- a/crate/cli/src/actions/certificates/mod.rs +++ b/crate/cli/src/actions/certificates/mod.rs @@ -36,6 +36,16 @@ pub enum CertificatesCommands { } impl CertificatesCommands { + /// Process the `Certificates` main commands. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn process(&self, client_connector: &KmsClient) -> CliResult<()> { match self { Self::Certify(action) => action.run(client_connector).await, diff --git a/crate/cli/src/actions/console.rs b/crate/cli/src/actions/console.rs index f2d1a94c..885d1cee 100644 --- a/crate/cli/src/actions/console.rs +++ b/crate/cli/src/actions/console.rs @@ -90,53 +90,78 @@ impl Stdout { self.object_owned = Some(object_owned); } + /// Writes the output to the console. + /// + /// # Errors + /// + /// Returns an error if there is an issue with writing to the console. pub fn write(&self) -> CliResult<()> { + // Check if the output format should be JSON let json_format_from_env = std::env::var(KMS_CLI_FORMAT) .unwrap_or_else(|_| CLI_DEFAULT_FORMAT.to_string()) .to_lowercase() == CLI_JSON_FORMAT; if json_format_from_env { + // Serialize the output as JSON and print it let console_stdout = serde_json::to_string_pretty(&self)?; println!("{console_stdout}"); } else { + // Print the output in text format if !self.stdout.is_empty() { println!("{}", self.stdout); } + // Print the unique identifier if present if let Some(id) = &self.unique_identifier { println!("\t Unique identifier: {id}"); } + + // Print the list of unique identifiers if present if let Some(ids) = &self.unique_identifiers { for id in ids { println!("{id}"); } } + + // Print the public key unique identifier if present if let Some(id) = &self.public_key_unique_identifier { println!("\t Public key unique identifier: {id}"); } + + // Print the private key unique identifier if present if let Some(id) = &self.private_key_unique_identifier { println!("\t Private key unique identifier: {id}"); } + + // Print the attributes if present if let Some(attributes) = &self.attributes { let json = serde_json::to_string_pretty(attributes)?; println!("{json}"); } + + // Print the list of accesses if present if let Some(accesses) = &self.accesses { for access in accesses { println!(" - {}: {:?}", access.user_id, access.operations); } } + + // Print the list of access rights obtained if present if let Some(access_rights_obtained) = &self.access_rights_obtained { for access in access_rights_obtained { println!("{access}"); } } + + // Print the list of objects owned if present if let Some(object_owned) = &self.object_owned { for obj in object_owned { println!("{obj}"); } } + + // Print the list of tags if present if let Some(t) = &self.tags { if !t.is_empty() { println!("\n Tags:"); diff --git a/crate/cli/src/actions/cover_crypt/decrypt.rs b/crate/cli/src/actions/cover_crypt/decrypt.rs index 8a37ff9d..7e769656 100644 --- a/crate/cli/src/actions/cover_crypt/decrypt.rs +++ b/crate/cli/src/actions/cover_crypt/decrypt.rs @@ -107,13 +107,13 @@ impl DecryptAction { &metadata_and_cleartext.plaintext, &self.input_files, self.output_file.as_ref(), - )? + )?; } else { write_single_decrypted_data( &metadata_and_cleartext.plaintext, &self.input_files[0], self.output_file.as_ref(), - )? + )?; } Ok(()) } diff --git a/crate/cli/src/actions/cover_crypt/encrypt.rs b/crate/cli/src/actions/cover_crypt/encrypt.rs index 55bc46ba..46fd804c 100644 --- a/crate/cli/src/actions/cover_crypt/encrypt.rs +++ b/crate/cli/src/actions/cover_crypt/encrypt.rs @@ -26,7 +26,7 @@ pub struct EncryptAction { input_files: Vec, /// The encryption policy to encrypt the file with - /// Example: "department::marketing && level::confidential"` + /// Example: "`department::marketing` && `level::confidential`" #[clap(required = true)] encryption_policy: String, @@ -105,9 +105,9 @@ impl EncryptAction { // Write the encrypted data if cryptographic_algorithm == CryptographicAlgorithm::CoverCryptBulk { - write_bulk_encrypted_data(&data, &self.input_files, self.output_file.as_ref())? + write_bulk_encrypted_data(&data, &self.input_files, self.output_file.as_ref())?; } else { - write_single_encrypted_data(&data, &self.input_files[0], self.output_file.as_ref())? + write_single_encrypted_data(&data, &self.input_files[0], self.output_file.as_ref())?; } Ok(()) } diff --git a/crate/cli/src/actions/cover_crypt/keys/create_user_key.rs b/crate/cli/src/actions/cover_crypt/keys/create_user_key.rs index e746301f..274e3ed3 100644 --- a/crate/cli/src/actions/cover_crypt/keys/create_user_key.rs +++ b/crate/cli/src/actions/cover_crypt/keys/create_user_key.rs @@ -54,7 +54,7 @@ pub struct CreateUserKeyAction { /// The access policy as a boolean expression combining policy attributes. /// - /// Example: "(Department::HR || Department::MKG) && Security Level::Confidential" + /// Example: "(`Department::HR` || `Department::MKG`) && Security `Level::Confidential`" #[clap(required = true)] access_policy: String, diff --git a/crate/cli/src/actions/cover_crypt/mod.rs b/crate/cli/src/actions/cover_crypt/mod.rs index a16d2101..6b05eb65 100644 --- a/crate/cli/src/actions/cover_crypt/mod.rs +++ b/crate/cli/src/actions/cover_crypt/mod.rs @@ -25,6 +25,16 @@ pub enum CovercryptCommands { } impl CovercryptCommands { + /// Process the Covercrypt command and execute the corresponding action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - The KMS client used for communication with the KMS service. + /// + /// # Errors + /// + /// This function can return an error if any of the underlying actions encounter an error. + /// pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { match self { Self::Policy(command) => command.process(kms_rest_client).await?, diff --git a/crate/cli/src/actions/cover_crypt/policy.rs b/crate/cli/src/actions/cover_crypt/policy.rs index b49dfa8b..bae05025 100644 --- a/crate/cli/src/actions/cover_crypt/policy.rs +++ b/crate/cli/src/actions/cover_crypt/policy.rs @@ -64,7 +64,7 @@ impl PolicyCommands { Self::View(action) => action.run(kms_rest_client).await?, Self::Specs(action) => action.run(kms_rest_client).await?, Self::Binary(action) => action.run(kms_rest_client).await?, - Self::Create(action) => action.run().await?, + Self::Create(action) => action.run()?, Self::AddAttribute(action) => action.run(kms_rest_client).await?, Self::RemoveAttribute(action) => action.run(kms_rest_client).await?, Self::DisableAttribute(action) => action.run(kms_rest_client).await?, @@ -125,7 +125,7 @@ pub struct CreateAction { } impl CreateAction { - pub async fn run(&self) -> CliResult<()> { + pub fn run(&self) -> CliResult<()> { // Parse the json policy file let policy = policy_from_json_file(&self.policy_specifications_file)?; @@ -524,6 +524,7 @@ impl RemoveAttributeAction { } #[cfg(test)] +#[allow(clippy::items_after_statements)] mod tests { use std::path::PathBuf; diff --git a/crate/cli/src/actions/elliptic_curves/mod.rs b/crate/cli/src/actions/elliptic_curves/mod.rs index b81650df..b5222943 100644 --- a/crate/cli/src/actions/elliptic_curves/mod.rs +++ b/crate/cli/src/actions/elliptic_curves/mod.rs @@ -24,6 +24,16 @@ pub enum EllipticCurveCommands { } impl EllipticCurveCommands { + /// Runs the `EllipticCurveCommands` main commands. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { match self { Self::Keys(command) => command.process(kms_rest_client).await?, diff --git a/crate/cli/src/actions/google/mod.rs b/crate/cli/src/actions/google/mod.rs index 9ddc147a..1520e6a7 100644 --- a/crate/cli/src/actions/google/mod.rs +++ b/crate/cli/src/actions/google/mod.rs @@ -20,6 +20,16 @@ pub enum GoogleCommands { } impl GoogleCommands { + /// Process the Google command by delegating the execution to the appropriate subcommand. + /// + /// # Arguments + /// + /// * `conf_path` - The path to the configuration file. + /// + /// # Errors + /// + /// Returns a `CliResult` indicating the success or failure of the command. + /// pub async fn process(&self, conf_path: &PathBuf) -> CliResult<()> { match self { Self::Keypairs(command) => command.process(conf_path).await?, diff --git a/crate/cli/src/actions/login.rs b/crate/cli/src/actions/login.rs index fbcbd171..48c4f27e 100644 --- a/crate/cli/src/actions/login.rs +++ b/crate/cli/src/actions/login.rs @@ -48,6 +48,29 @@ use crate::{ pub struct LoginAction; impl LoginAction { + /// This function processes the login action. + /// It loads the client configuration from the specified path, retrieves the `OAuth2` configuration, + /// initializes the login state, prompts the user to browse to the authorization URL, + /// finalizes the login process by receiving the authorization code and exchanging it for an access token, + /// updates the configuration with the access token, and saves the configuration to the specified path. + /// + /// # Arguments + /// + /// * `conf_path` - The path to the client configuration file. + /// + /// # Errors + /// + /// This function can return a `CliError` in the following cases: + /// + /// * The `login` command requires an Identity Provider (`IdP`) that must be configured in the `oauth2_conf` object in the client configuration file. + /// * The client configuration file cannot be loaded. + /// * The `OAuth2` configuration is missing or invalid in the client configuration file. + /// * The authorization URL cannot be parsed. + /// * The authorization code is not received or does not match the CSRF token. + /// * The access token cannot be requested from the Identity Provider. + /// * The token exchange request fails. + /// * The token exchange response cannot be parsed. + /// * The client configuration cannot be updated or saved. pub async fn process(&self, conf_path: &PathBuf) -> CliResult<()> { let mut conf = ClientConf::load(conf_path)?; let oauth2_conf = conf.oauth2_conf.as_ref().ok_or_else(|| { @@ -189,6 +212,18 @@ impl LoginState { /// This function should be called immediately after the user has been instructed to browse to the authorization URL. /// It starts a server on localhost:17899 and waits for the authorization code to be received /// from the browser window. Once the code is received, the server is closed and the code is returned. + /// + /// # Errors + /// + /// This function can return a `CliError` in the following cases: + /// + /// * The authorization code, state, or other parameters are not received from the redirect URL. + /// * The received state does not match the CSRF token. + /// * The authorization code is not received on authentication. + /// * The code received on authentication does not match the CSRF token. + /// * The access token cannot be requested from the Identity Provider. + /// * The token exchange request fails. + /// * The token exchange response cannot be parsed. pub async fn finalize(&self) -> CliResult { // recover the authorization code, state and other parameters from the redirect URL let auth_parameters = Self::receive_authorization_parameters()?; @@ -294,6 +329,20 @@ pub struct OAuthResponse { /// not in the `access_token` field. /// /// For Google see: +/// +/// # Arguments +/// +/// * `login_config` - The `Oauth2LoginConfig` containing the client configuration. +/// * `redirect_url` - The redirect URL used in the `OAuth2` flow. +/// * `pkce_verifier` - The PKCE code verifier used in the `OAuth2` flow. +/// * `authorization_code` - The authorization code received from the Identity Provider. +/// +/// # Errors +/// +/// This function can return a `CliError` in the following cases: +/// +/// * The token exchange request fails. +/// * The token exchange response cannot be parsed. pub async fn request_token( login_config: &Oauth2LoginConfig, redirect_url: &Url, diff --git a/crate/cli/src/actions/logout.rs b/crate/cli/src/actions/logout.rs index 0cc5c0d4..3e7b8368 100644 --- a/crate/cli/src/actions/logout.rs +++ b/crate/cli/src/actions/logout.rs @@ -13,6 +13,16 @@ use crate::error::result::CliResult; pub struct LogoutAction; impl LogoutAction { + /// Process the logout action. + /// + /// # Arguments + /// + /// * `conf_path` - The path to the ckms configuration file. + /// + /// # Errors + /// + /// Returns an error if there is an issue loading or saving the configuration file. + /// pub fn process(&self, conf_path: &PathBuf) -> CliResult<()> { let mut conf = ClientConf::load(conf_path)?; conf.kms_access_token = None; diff --git a/crate/cli/src/actions/markdown.rs b/crate/cli/src/actions/markdown.rs index 896e01d5..52ca3252 100644 --- a/crate/cli/src/actions/markdown.rs +++ b/crate/cli/src/actions/markdown.rs @@ -13,6 +13,11 @@ pub struct MarkdownAction { } impl MarkdownAction { + /// Process the given command and generate the markdown documentation. + /// + /// # Errors + /// + /// Returns an error if there is an issue creating or writing to the markdown file. pub fn process(&self, cmd: &Command) -> CliResult<()> { let mut output = String::new(); writeln!( diff --git a/crate/cli/src/actions/new_database.rs b/crate/cli/src/actions/new_database.rs index d925b8e1..af47033f 100644 --- a/crate/cli/src/actions/new_database.rs +++ b/crate/cli/src/actions/new_database.rs @@ -19,6 +19,16 @@ use crate::error::result::{CliResult, CliResultHelper}; pub struct NewDatabaseAction; impl NewDatabaseAction { + /// Process the `NewDatabaseAction` by querying the KMS to get a new database. + /// + /// # Arguments + /// + /// * `kms_rest_client` - The KMS client used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the KMS server fails. + /// pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { // Query the KMS to get a new database let token = kms_rest_client diff --git a/crate/cli/src/actions/rsa/mod.rs b/crate/cli/src/actions/rsa/mod.rs index e4608c37..94392683 100644 --- a/crate/cli/src/actions/rsa/mod.rs +++ b/crate/cli/src/actions/rsa/mod.rs @@ -24,6 +24,15 @@ pub enum RsaCommands { } impl RsaCommands { + /// Process the RSA command by executing the corresponding action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - A reference to the KMS client used for communication with the KMS service. + /// + /// # Errors + /// + /// Returns an error if there is an issue executing the command. pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { match self { Self::Keys(command) => command.process(kms_rest_client).await?, diff --git a/crate/cli/src/actions/shared/export_key.rs b/crate/cli/src/actions/shared/export_key.rs index bb41001d..a2b94278 100644 --- a/crate/cli/src/actions/shared/export_key.rs +++ b/crate/cli/src/actions/shared/export_key.rs @@ -108,6 +108,14 @@ pub struct ExportKeyAction { impl ExportKeyAction { /// Export a key from the KMS + /// + /// # Errors + /// + /// This function can return an error if: + /// + /// - Either `--key-id` or one or more `--tag` is not specified. + /// - There is a server error while exporting the object. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let id = if let Some(key_id) = &self.key_id { key_id.clone() diff --git a/crate/cli/src/actions/shared/get_attributes.rs b/crate/cli/src/actions/shared/get_attributes.rs index fa1c277b..3401687f 100644 --- a/crate/cli/src/actions/shared/get_attributes.rs +++ b/crate/cli/src/actions/shared/get_attributes.rs @@ -78,6 +78,21 @@ pub struct GetAttributesAction { } impl GetAttributesAction { + /// Get the KMIP object attributes and tags. + /// + /// When using tags to retrieve the object, rather than the object id, + /// an error is returned if multiple objects matching the tags are found. + /// + /// # Errors + /// + /// This function can return an error if: + /// + /// - The `--id` or one or more `--tag` options is not specified. + /// - There is an error serializing the tags to a string. + /// - There is an error performing the Get Attributes request. + /// - There is an error serializing the attributes to JSON. + /// - There is an error writing the attributes to the output file. + /// - There is an error writing to the console. pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { trace!("GetAttributesAction: {:?}", self); let id = if let Some(key_id) = &self.id { diff --git a/crate/cli/src/actions/shared/import_key.rs b/crate/cli/src/actions/shared/import_key.rs index 864010d3..7eead374 100644 --- a/crate/cli/src/actions/shared/import_key.rs +++ b/crate/cli/src/actions/shared/import_key.rs @@ -111,6 +111,23 @@ pub struct ImportKeyAction { } impl ImportKeyAction { + /// Run the import key action. + /// + /// # Errors + /// + /// This function can return a [`CliError`] if an error occurs during the import process. + /// + /// Possible error cases include: + /// + /// - Failed to read the key file. + /// - Failed to parse the key file in the specified format. + /// - Invalid key format specified. + /// - Failed to assign cryptographic usage mask. + /// - Failed to generate import attributes. + /// - Failed to import the key. + /// - Failed to write the response to stdout. + /// + /// [`CliError`]: ../error/result/enum.CliError.html pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let cryptographic_usage_mask = self .key_usage diff --git a/crate/cli/src/actions/shared/locate.rs b/crate/cli/src/actions/shared/locate.rs index 992a7f05..1cc10c66 100644 --- a/crate/cli/src/actions/shared/locate.rs +++ b/crate/cli/src/actions/shared/locate.rs @@ -94,6 +94,10 @@ pub struct LocateObjectsAction { impl LocateObjectsAction { /// Export a key from the KMS + /// + /// # Errors + /// + /// Returns an error if there is a problem communicating with the KMS or if the requested key cannot be located. pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let mut attributes = Attributes::default(); diff --git a/crate/cli/src/actions/shared/unwrap_key.rs b/crate/cli/src/actions/shared/unwrap_key.rs index 069ac189..b61088dc 100644 --- a/crate/cli/src/actions/shared/unwrap_key.rs +++ b/crate/cli/src/actions/shared/unwrap_key.rs @@ -68,6 +68,18 @@ pub struct UnwrapKeyAction { impl UnwrapKeyAction { /// Export a key from the KMS + /// + /// # Errors + /// + /// This function can return an error if: + /// + /// - The key file cannot be read. + /// - The unwrap key fails to decode from base64. + /// - The unwrapping key fails to be created. + /// - The unwrapping key fails to unwrap the key. + /// - The output file fails to be written. + /// - The console output fails to be written. + /// pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { // read the key file let mut object = read_object_from_json_ttlv_file(&self.key_file_in)?; diff --git a/crate/cli/src/actions/shared/wrap_key.rs b/crate/cli/src/actions/shared/wrap_key.rs index 5333eb4e..6acc2dbb 100644 --- a/crate/cli/src/actions/shared/wrap_key.rs +++ b/crate/cli/src/actions/shared/wrap_key.rs @@ -62,6 +62,21 @@ pub struct WrapKeyAction { } impl WrapKeyAction { + /// Run the wrap key action. + /// + /// # Errors + /// + /// This function can return an error if: + /// + /// - The key file cannot be read. + /// - The key is already wrapped and cannot be wrapped again. + /// - The wrap key cannot be decoded from base64. + /// - The wrap password cannot be derived into a symmetric key. + /// - The wrap key cannot be exported from the KMS. + /// - The wrap key file cannot be read. + /// - The key block cannot be wrapped with the wrapping key. + /// - The wrapped key object cannot be written to the output file. + /// - The console output cannot be written. pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { // read the key file let mut object = read_object_from_json_ttlv_file(&self.key_file_in)?; diff --git a/crate/cli/src/actions/symmetric/mod.rs b/crate/cli/src/actions/symmetric/mod.rs index 5e8d7c85..8655fad5 100644 --- a/crate/cli/src/actions/symmetric/mod.rs +++ b/crate/cli/src/actions/symmetric/mod.rs @@ -18,6 +18,16 @@ pub enum SymmetricCommands { } impl SymmetricCommands { + /// Process the symmetric command and execute the corresponding action. + /// + /// # Errors + /// + /// This function can return an error if any of the underlying actions encounter an error. + /// + /// # Arguments + /// + /// * `kms_rest_client` - The KMS client used for communication with the KMS service. + /// pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { match self { Self::Keys(command) => command.process(kms_rest_client).await?, diff --git a/crate/cli/src/actions/version.rs b/crate/cli/src/actions/version.rs index 217f2e14..0f25e852 100644 --- a/crate/cli/src/actions/version.rs +++ b/crate/cli/src/actions/version.rs @@ -10,6 +10,15 @@ use crate::error::result::{CliResult, CliResultHelper}; pub struct ServerVersionAction; impl ServerVersionAction { + /// Process the server version action. + /// + /// # Arguments + /// + /// * `kms_rest_client` - The KMS client instance used to communicate with the KMS server. + /// + /// # Errors + /// + /// Returns an error if the version query fails or if there is an issue writing to the console. pub async fn process(&self, kms_rest_client: &KmsClient) -> CliResult<()> { let version = kms_rest_client .version() diff --git a/crate/cli/src/error/result.rs b/crate/cli/src/error/result.rs index 233aeb04..1ea6d751 100644 --- a/crate/cli/src/error/result.rs +++ b/crate/cli/src/error/result.rs @@ -6,9 +6,27 @@ use super::CliError; pub type CliResult = Result; +/// Trait for providing helper methods for `CliResult`. pub trait CliResultHelper { + /// Sets the reason for the error. + /// + /// # Errors + /// + /// Returns a `CliResult` with the specified `ErrorReason`. fn reason(self, reason: ErrorReason) -> CliResult; + + /// Sets the context for the error. + /// + /// # Errors + /// + /// Returns a `CliResult` with the specified context. fn context(self, context: &str) -> CliResult; + + /// Sets the context for the error using a closure. + /// + /// # Errors + /// + /// Returns a `CliResult` with the context returned by the closure. fn with_context(self, op: O) -> CliResult where D: Display + Send + Sync + 'static, diff --git a/crate/cli/src/lib.rs b/crate/cli/src/lib.rs index 649d3f3d..026dedc5 100644 --- a/crate/cli/src/lib.rs +++ b/crate/cli/src/lib.rs @@ -6,6 +6,7 @@ let_underscore, rust_2024_compatibility, unreachable_pub, + unused, clippy::all, clippy::suspicious, clippy::complexity, @@ -16,7 +17,6 @@ )] #![allow( clippy::module_name_repetitions, - clippy::missing_errors_doc, clippy::too_many_lines, clippy::cargo_common_metadata, clippy::multiple_crate_versions diff --git a/crate/cli/src/tests/certificates/encrypt.rs b/crate/cli/src/tests/certificates/encrypt.rs index 67458ad5..1c212596 100644 --- a/crate/cli/src/tests/certificates/encrypt.rs +++ b/crate/cli/src/tests/certificates/encrypt.rs @@ -198,7 +198,7 @@ async fn test_certificate_import_encrypt( true, )?; - let _subca_certificate_id = import_certificate( + let subca_certificate_id = import_certificate( &ctx.owner_client_conf_path, "certificates", &format!("test_data/certificates/{subca_path}"), @@ -221,7 +221,7 @@ async fn test_certificate_import_encrypt( None, None, Some(private_key_id.clone()), - Some(_subca_certificate_id), + Some(subca_certificate_id), Some(tags), None, false, diff --git a/crate/cli/src/tests/shared/export.rs b/crate/cli/src/tests/shared/export.rs index 749efb02..7eb0dbec 100644 --- a/crate/cli/src/tests/shared/export.rs +++ b/crate/cli/src/tests/shared/export.rs @@ -178,46 +178,6 @@ pub(crate) async fn test_export_sym_allow_revoked() -> CliResult<()> { #[cfg(not(feature = "fips"))] #[tokio::test] pub(crate) async fn test_export_covercrypt() -> CliResult<()> { - // create a temp dir - let tmp_dir = TempDir::new()?; - let tmp_path = tmp_dir.path(); - // init the test server - let ctx = start_default_test_kms_server().await; - - // generate a new master key pair - let (master_private_key_id, _master_public_key_id) = create_cc_master_key_pair( - &ctx.owner_client_conf_path, - "--policy-specifications", - "test_data/policy_specifications.json", - &[], - )?; - - _export_cc_test( - KeyFormatType::CoverCryptSecretKey, - &master_private_key_id, - tmp_path, - ctx, - )?; - _export_cc_test( - KeyFormatType::CoverCryptPublicKey, - &_master_public_key_id, - tmp_path, - ctx, - )?; - - let user_key_id = create_user_decryption_key( - &ctx.owner_client_conf_path, - &master_private_key_id, - "(Department::MKG || Department::FIN) && Security Level::Top Secret", - &[], - )?; - _export_cc_test( - KeyFormatType::CoverCryptSecretKey, - &user_key_id, - tmp_path, - ctx, - )?; - fn _export_cc_test( key_format_type: KeyFormatType, key_id: &str, @@ -258,6 +218,46 @@ pub(crate) async fn test_export_covercrypt() -> CliResult<()> { Ok(()) } + // create a temp dir + let tmp_dir = TempDir::new()?; + let tmp_path = tmp_dir.path(); + // init the test server + let ctx = start_default_test_kms_server().await; + + // generate a new master key pair + let (master_private_key_id, master_public_key_id) = create_cc_master_key_pair( + &ctx.owner_client_conf_path, + "--policy-specifications", + "test_data/policy_specifications.json", + &[], + )?; + + _export_cc_test( + KeyFormatType::CoverCryptSecretKey, + &master_private_key_id, + tmp_path, + ctx, + )?; + _export_cc_test( + KeyFormatType::CoverCryptPublicKey, + &master_public_key_id, + tmp_path, + ctx, + )?; + + let user_key_id = create_user_decryption_key( + &ctx.owner_client_conf_path, + &master_private_key_id, + "(Department::MKG || Department::FIN) && Security Level::Top Secret", + &[], + )?; + _export_cc_test( + KeyFormatType::CoverCryptSecretKey, + &user_key_id, + tmp_path, + ctx, + )?; + Ok(()) } @@ -348,12 +348,12 @@ pub(crate) async fn test_export_x25519() -> CliResult<()> { Some(CryptographicAlgorithm::ECDH) ); let kv = &key_block.key_value; - let (d, recommended_curve) = match &kv.key_material { - KeyMaterial::TransparentECPrivateKey { - d, - recommended_curve, - } => (d, recommended_curve), - _ => panic!("Invalid key value type"), + let KeyMaterial::TransparentECPrivateKey { + d, + recommended_curve, + } = &kv.key_material + else { + panic!("Invalid key value type"); }; assert_eq!(recommended_curve, &RecommendedCurve::CURVE25519); let mut d_vec = d.to_bytes_be(); @@ -407,12 +407,12 @@ pub(crate) async fn test_export_x25519() -> CliResult<()> { Some(CryptographicAlgorithm::ECDH) ); let kv = &key_block.key_value; - let (q_string, recommended_curve) = match &kv.key_material { - KeyMaterial::TransparentECPublicKey { - q_string, - recommended_curve, - } => (q_string, recommended_curve), - _ => panic!("Invalid key value type"), + let KeyMaterial::TransparentECPublicKey { + q_string, + recommended_curve, + } = &kv.key_material + else { + panic!("Invalid key value type") }; assert_eq!(recommended_curve, &RecommendedCurve::CURVE25519); let pkey_1 = PKey::public_key_from_raw_bytes(q_string, Id::X25519).unwrap(); diff --git a/crate/cli/src/tests/shared/import_export_wrapping.rs b/crate/cli/src/tests/shared/import_export_wrapping.rs index 51d0b6bc..77daa2c1 100644 --- a/crate/cli/src/tests/shared/import_export_wrapping.rs +++ b/crate/cli/src/tests/shared/import_export_wrapping.rs @@ -15,6 +15,7 @@ use cosmian_kms_client::{ }, }, }, + kmip::extra::tagging::EMPTY_TAGS, read_object_from_json_ttlv_file, write_kmip_object_to_file, }; use kms_test_server::start_default_test_kms_server; @@ -96,7 +97,7 @@ pub(crate) async fn test_import_export_wrap_rfc_5649() -> CliResult<()> { None, None, None, - &[] as &[&str], + &EMPTY_TAGS, )?; test_import_export_wrap_private_key( &ctx.owner_client_conf_path, @@ -192,7 +193,7 @@ pub(crate) async fn test_import_export_wrap_ecies() -> CliResult<()> { None, None, None, - &[] as &[&str], + &EMPTY_TAGS, )?; test_import_export_wrap_private_key( &ctx.owner_client_conf_path, diff --git a/crate/cli/src/tests/symmetric/create_key.rs b/crate/cli/src/tests/symmetric/create_key.rs index 66f9b236..981e930f 100644 --- a/crate/cli/src/tests/symmetric/create_key.rs +++ b/crate/cli/src/tests/symmetric/create_key.rs @@ -6,7 +6,7 @@ use cloudproof::reexport::crypto_core::{ reexport::rand_core::{RngCore, SeedableRng}, CsRng, }; -use cosmian_kms_client::KMS_CLI_CONF_ENV; +use cosmian_kms_client::{kmip::extra::tagging::EMPTY_TAGS, KMS_CLI_CONF_ENV}; use kms_test_server::start_default_test_kms_server; use super::SUB_COMMAND; @@ -78,7 +78,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { Some(128), None, None, - &[] as &[&str], + &EMPTY_TAGS, )?; // AES 256 bit key from a base64 encoded key rng.fill_bytes(&mut key); @@ -88,7 +88,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { None, Some(&key_b64), None, - &[] as &[&str], + &EMPTY_TAGS, )?; } @@ -100,7 +100,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { None, None, Some("chacha20"), - &[] as &[&str], + &EMPTY_TAGS, )?; // ChaCha20 128 bit key create_symmetric_key( @@ -108,7 +108,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { Some(128), None, Some("chacha20"), - &[] as &[&str], + &EMPTY_TAGS, )?; // ChaCha20 256 bit key from a base64 encoded key let mut rng = CsRng::from_entropy(); @@ -120,7 +120,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { None, Some(&key_b64), Some("chacha20"), - &[] as &[&str], + &EMPTY_TAGS, )?; } @@ -132,7 +132,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { None, None, Some("sha3"), - &[] as &[&str], + &EMPTY_TAGS, )?; // ChaCha20 salts create_symmetric_key( @@ -140,28 +140,28 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { Some(224), None, Some("sha3"), - &[] as &[&str], + &EMPTY_TAGS, )?; create_symmetric_key( &ctx.owner_client_conf_path, Some(256), None, Some("sha3"), - &[] as &[&str], + &EMPTY_TAGS, )?; create_symmetric_key( &ctx.owner_client_conf_path, Some(384), None, Some("sha3"), - &[] as &[&str], + &EMPTY_TAGS, )?; create_symmetric_key( &ctx.owner_client_conf_path, Some(512), None, Some("sha3"), - &[] as &[&str], + &EMPTY_TAGS, )?; // ChaCha20 256 bit salt from a base64 encoded salt let mut rng = CsRng::from_entropy(); @@ -173,7 +173,7 @@ pub(crate) async fn test_create_symmetric_key() -> CliResult<()> { None, Some(&key_b64), Some("sha3"), - &[] as &[&str], + &EMPTY_TAGS, )?; } Ok(()) diff --git a/crate/kmip/src/crypto/symmetric/aead.rs b/crate/kmip/src/crypto/symmetric/aead.rs index b118bc0d..cd6089c8 100644 --- a/crate/kmip/src/crypto/symmetric/aead.rs +++ b/crate/kmip/src/crypto/symmetric/aead.rs @@ -104,7 +104,7 @@ impl AeadCipher { CryptographicAlgorithm::ChaCha20 => { if block_cipher_mode.is_some() { kmip_bail!(KmipError::NotSupported( - "ChaCha20 is only supported with Pooly1305. Do not specify the Block \ + "ChaCha20 is only supported with Poly1305. Do not specify the Block \ Cipher Mode" .to_owned() )); diff --git a/crate/kmip/src/crypto/wrap/unwrap_key.rs b/crate/kmip/src/crypto/wrap/unwrap_key.rs index 9b6b2089..07799eb6 100644 --- a/crate/kmip/src/crypto/wrap/unwrap_key.rs +++ b/crate/kmip/src/crypto/wrap/unwrap_key.rs @@ -157,7 +157,7 @@ fn unwrap_with_private_key( match private_key.id() { Id::RSA => unwrap_with_rsa(private_key, key_wrapping_data, ciphertext), #[cfg(not(feature = "fips"))] - Id::EC | Id::X25519 | Id::ED25519 => ecies_decrypt(&private_key, ciphertext), + Id::EC | Id::X25519 | Id::ED25519 => ecies_decrypt(private_key, ciphertext), other => { kmip_bail!( "Unable to wrap key: wrapping public key type not supported: {:?}", diff --git a/crate/kmip/src/crypto/wrap/wrap_key.rs b/crate/kmip/src/crypto/wrap/wrap_key.rs index 52a1caf2..00f1912a 100644 --- a/crate/kmip/src/crypto/wrap/wrap_key.rs +++ b/crate/kmip/src/crypto/wrap/wrap_key.rs @@ -197,7 +197,7 @@ fn wrap_with_public_key( match public_key.id() { Id::RSA => wrap_with_rsa(public_key, key_wrapping_data, key_to_wrap), #[cfg(not(feature = "fips"))] - Id::EC | Id::X25519 | Id::ED25519 => ecies_encrypt(&public_key, key_to_wrap), + Id::EC | Id::X25519 | Id::ED25519 => ecies_encrypt(public_key, key_to_wrap), other => Err(kmip_error!( "Unable to wrap key: wrapping public key type not supported: {other:?}" )), diff --git a/crate/kmip/src/openssl/private_key.rs b/crate/kmip/src/openssl/private_key.rs index 7b8ddf55..ab30964d 100644 --- a/crate/kmip/src/openssl/private_key.rs +++ b/crate/kmip/src/openssl/private_key.rs @@ -482,7 +482,7 @@ mod tests { fn test_private_key_conversion_pkcs( private_key: &PKey, id: Id, - keysize: u32, + key_size: u32, kft: KeyFormatType, ) { #[cfg(feature = "fips")] @@ -511,14 +511,14 @@ mod tests { if kft == KeyFormatType::PKCS8 { let private_key_ = PKey::private_key_from_pkcs8(&key_value).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key_.private_key_to_pkcs8().unwrap(), key_value.to_vec() ); let private_key_ = kmip_private_key_to_openssl(&object_).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key_.private_key_to_pkcs8().unwrap(), key_value.to_vec() @@ -526,14 +526,14 @@ mod tests { } else { let private_key_ = PKey::private_key_from_der(&key_value).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key_.private_key_to_der().unwrap(), key_value.to_vec() ); let private_key_ = kmip_private_key_to_openssl(&object_).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key_.private_key_to_der().unwrap(), key_value.to_vec() @@ -541,7 +541,7 @@ mod tests { } } - fn test_private_key_conversion_sec1(private_key: &PKey, id: Id, keysize: u32) { + fn test_private_key_conversion_sec1(private_key: &PKey, id: Id, key_size: u32) { #[cfg(feature = "fips")] let mask = Some(FIPS_PRIVATE_ECC_MASK_SIGN_ECDH); #[cfg(not(feature = "fips"))] @@ -568,14 +568,14 @@ mod tests { let private_key_ = PKey::from_ec_key(EcKey::private_key_from_der(&key_value).unwrap()).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key_.private_key_to_der().unwrap(), key_value.to_vec() ); let private_key_ = kmip_private_key_to_openssl(&object_).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key_.private_key_to_der().unwrap(), key_value.to_vec() @@ -585,7 +585,7 @@ mod tests { fn test_private_key_conversion_transparent_rsa( private_key: &PKey, id: Id, - keysize: u32, + key_size: u32, ) { #[cfg(feature = "fips")] let mask = Some(FIPS_PRIVATE_RSA_MASK); @@ -636,7 +636,7 @@ mod tests { .unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key.private_key_to_der().unwrap(), private_key_.private_key_to_der().unwrap() @@ -644,7 +644,7 @@ mod tests { let private_key_ = kmip_private_key_to_openssl(&object_).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key.private_key_to_der().unwrap(), private_key_.private_key_to_der().unwrap() @@ -657,7 +657,7 @@ mod tests { ec_group: Option<&EcGroup>, curve: RecommendedCurve, id: Id, - keysize: u32, + key_size: u32, ) { #[cfg(feature = "fips")] let mask = Some(FIPS_PRIVATE_ECC_MASK_SIGN_ECDH); @@ -692,9 +692,9 @@ mod tests { let mut privkey_vec = d.to_bytes_be(); // privkey size on curve. - let bytes_keysize = 1 + ((keysize as usize - 1) / 8); + let bytes_key_size = 1 + ((key_size as usize - 1) / 8); - pad_be_bytes(&mut privkey_vec, bytes_keysize); + pad_be_bytes(&mut privkey_vec, bytes_key_size); if id == Id::EC { let private_key_ = PKey::from_ec_key( EcKey::from_private_components( @@ -706,14 +706,14 @@ mod tests { ) .unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key.private_key_to_der().unwrap(), private_key_.private_key_to_der().unwrap() ); let private_key_ = kmip_private_key_to_openssl(&object_).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key.private_key_to_der().unwrap(), private_key_.private_key_to_der().unwrap() @@ -721,14 +721,14 @@ mod tests { } else { let private_key_ = PKey::private_key_from_raw_bytes(&privkey_vec, id).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key.raw_private_key().unwrap(), private_key_.raw_private_key().unwrap() ); let private_key_ = kmip_private_key_to_openssl(&object_).unwrap(); assert_eq!(private_key_.id(), id); - assert_eq!(private_key_.bits(), keysize); + assert_eq!(private_key_.bits(), key_size); assert_eq!( private_key.raw_private_key().unwrap(), private_key_.raw_private_key().unwrap() @@ -742,26 +742,26 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 2048; - let rsa_private_key = Rsa::generate(keysize).unwrap(); + let key_size = 2048; + let rsa_private_key = Rsa::generate(key_size).unwrap(); let private_key = PKey::from_rsa(rsa_private_key).unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::RSA, keysize, KeyFormatType::PKCS8); - test_private_key_conversion_pkcs(&private_key, Id::RSA, keysize, KeyFormatType::PKCS1); - test_private_key_conversion_transparent_rsa(&private_key, Id::RSA, keysize); + test_private_key_conversion_pkcs(&private_key, Id::RSA, key_size, KeyFormatType::PKCS8); + test_private_key_conversion_pkcs(&private_key, Id::RSA, key_size, KeyFormatType::PKCS1); + test_private_key_conversion_transparent_rsa(&private_key, Id::RSA, key_size); } #[test] #[cfg(not(feature = "fips"))] fn test_conversion_ec_p_192_private_key() { - let keysize = 192; + let key_size = 192; let ec_group = EcGroup::from_curve_name(Nid::X9_62_PRIME192V1).unwrap(); let ec_key = EcKey::generate(&ec_group).unwrap(); let ec_public_key = ec_key.public_key().to_owned(&ec_group).unwrap(); let private_key = PKey::from_ec_key(ec_key).unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::EC, keysize, KeyFormatType::PKCS8); - test_private_key_conversion_sec1(&private_key, Id::EC, keysize); + test_private_key_conversion_pkcs(&private_key, Id::EC, key_size, KeyFormatType::PKCS8); + test_private_key_conversion_sec1(&private_key, Id::EC, key_size); test_private_key_conversion_transparent_ec( &private_key, @@ -769,7 +769,7 @@ mod tests { Some(&ec_group), RecommendedCurve::P192, Id::EC, - keysize, + key_size, ); } @@ -779,14 +779,14 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 224; + let key_size = 224; let ec_group = EcGroup::from_curve_name(Nid::SECP224R1).unwrap(); let ec_key = EcKey::generate(&ec_group).unwrap(); let ec_public_key = ec_key.public_key().to_owned(&ec_group).unwrap(); let private_key = PKey::from_ec_key(ec_key).unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::EC, keysize, KeyFormatType::PKCS8); - test_private_key_conversion_sec1(&private_key, Id::EC, keysize); + test_private_key_conversion_pkcs(&private_key, Id::EC, key_size, KeyFormatType::PKCS8); + test_private_key_conversion_sec1(&private_key, Id::EC, key_size); test_private_key_conversion_transparent_ec( &private_key, @@ -794,7 +794,7 @@ mod tests { Some(&ec_group), RecommendedCurve::P224, Id::EC, - keysize, + key_size, ); } @@ -804,14 +804,14 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 256; + let key_size = 256; let ec_group = EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1).unwrap(); let ec_key = EcKey::generate(&ec_group).unwrap(); let ec_public_key = ec_key.public_key().to_owned(&ec_group).unwrap(); let private_key = PKey::from_ec_key(ec_key).unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::EC, keysize, KeyFormatType::PKCS8); - test_private_key_conversion_sec1(&private_key, Id::EC, keysize); + test_private_key_conversion_pkcs(&private_key, Id::EC, key_size, KeyFormatType::PKCS8); + test_private_key_conversion_sec1(&private_key, Id::EC, key_size); test_private_key_conversion_transparent_ec( &private_key, @@ -819,7 +819,7 @@ mod tests { Some(&ec_group), RecommendedCurve::P256, Id::EC, - keysize, + key_size, ); } @@ -829,14 +829,14 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 384; + let key_size = 384; let ec_group = EcGroup::from_curve_name(openssl::nid::Nid::SECP384R1).unwrap(); let ec_key = EcKey::generate(&ec_group).unwrap(); let ec_public_key = ec_key.public_key().to_owned(&ec_group).unwrap(); let private_key = PKey::from_ec_key(ec_key).unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::EC, keysize, KeyFormatType::PKCS8); - test_private_key_conversion_sec1(&private_key, Id::EC, keysize); + test_private_key_conversion_pkcs(&private_key, Id::EC, key_size, KeyFormatType::PKCS8); + test_private_key_conversion_sec1(&private_key, Id::EC, key_size); test_private_key_conversion_transparent_ec( &private_key, @@ -844,7 +844,7 @@ mod tests { Some(&ec_group), RecommendedCurve::P384, Id::EC, - keysize, + key_size, ); } @@ -854,14 +854,14 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 521; + let key_size = 521; let ec_group = EcGroup::from_curve_name(openssl::nid::Nid::SECP521R1).unwrap(); let ec_key = EcKey::generate(&ec_group).unwrap(); let ec_public_key = ec_key.public_key().to_owned(&ec_group).unwrap(); let private_key = PKey::from_ec_key(ec_key).unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::EC, keysize, KeyFormatType::PKCS8); - test_private_key_conversion_sec1(&private_key, Id::EC, keysize); + test_private_key_conversion_pkcs(&private_key, Id::EC, key_size, KeyFormatType::PKCS8); + test_private_key_conversion_sec1(&private_key, Id::EC, key_size); test_private_key_conversion_transparent_ec( &private_key, @@ -869,24 +869,24 @@ mod tests { Some(&ec_group), RecommendedCurve::P521, Id::EC, - keysize, + key_size, ); } #[test] #[cfg(not(feature = "fips"))] fn test_conversion_ec_x25519_private_key() { - let keysize = 253; + let key_size = 253; let private_key = PKey::generate_x25519().unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::X25519, keysize, KeyFormatType::PKCS8); + test_private_key_conversion_pkcs(&private_key, Id::X25519, key_size, KeyFormatType::PKCS8); test_private_key_conversion_transparent_ec( &private_key, None, None, RecommendedCurve::CURVE25519, Id::X25519, - keysize, + key_size, ); } @@ -896,34 +896,34 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 256; + let key_size = 256; let private_key = PKey::generate_ed25519().unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::ED25519, keysize, KeyFormatType::PKCS8); + test_private_key_conversion_pkcs(&private_key, Id::ED25519, key_size, KeyFormatType::PKCS8); test_private_key_conversion_transparent_ec( &private_key, None, None, RecommendedCurve::CURVEED25519, Id::ED25519, - keysize, + key_size, ); } #[test] #[cfg(not(feature = "fips"))] fn test_conversion_ec_x448_private_key() { - let keysize = 448; + let key_size = 448; let private_key = PKey::generate_x448().unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::X448, keysize, KeyFormatType::PKCS8); + test_private_key_conversion_pkcs(&private_key, Id::X448, key_size, KeyFormatType::PKCS8); test_private_key_conversion_transparent_ec( &private_key, None, None, RecommendedCurve::CURVE448, Id::X448, - keysize, + key_size, ); } @@ -933,17 +933,17 @@ mod tests { // Load FIPS provider module from OpenSSL. openssl::provider::Provider::load(None, "fips").unwrap(); - let keysize = 456; + let key_size = 456; let private_key = PKey::generate_ed448().unwrap(); - test_private_key_conversion_pkcs(&private_key, Id::ED448, keysize, KeyFormatType::PKCS8); + test_private_key_conversion_pkcs(&private_key, Id::ED448, key_size, KeyFormatType::PKCS8); test_private_key_conversion_transparent_ec( &private_key, None, None, RecommendedCurve::CURVEED448, Id::ED448, - keysize, + key_size, ); } } diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index c4e02885..1befab23 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -107,6 +107,7 @@ tracing-opentelemetry = "0.24.0" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = { workspace = true } uuid = { workspace = true, features = ["v4"] } +version-compare = "0.2.0" x509-parser = { workspace = true } zeroize = { workspace = true } @@ -114,6 +115,7 @@ zeroize = { workspace = true } actix-http = "3.6" cosmian_logger = { path = "../logger" } pem = "3.0.4" +tempfile = "3.11" [build-dependencies] actix-http = "3.6" diff --git a/crate/server/src/config/command_line/db.rs b/crate/server/src/config/command_line/db.rs index ec0a1514..d8386ac4 100644 --- a/crate/server/src/config/command_line/db.rs +++ b/crate/server/src/config/command_line/db.rs @@ -201,7 +201,7 @@ impl DBConfig { fn ensure_url(database_url: Option<&str>, alternate_env_variable: &str) -> KResult { let url = if let Some(url) = database_url { - Ok(url.to_string()) + Ok(url.to_owned()) } else { std::env::var(alternate_env_variable).map_err(|_e| { kms_error!( @@ -220,7 +220,7 @@ fn ensure_value( env_variable_name: &str, ) -> KResult { if let Some(value) = value { - Ok(value.to_string()) + Ok(value.to_owned()) } else { std::env::var(env_variable_name).map_err(|_e| { kms_error!( diff --git a/crate/server/src/config/command_line/http_config.rs b/crate/server/src/config/command_line/http_config.rs index 74af7f0f..2a9957eb 100644 --- a/crate/server/src/config/command_line/http_config.rs +++ b/crate/server/src/config/command_line/http_config.rs @@ -65,7 +65,7 @@ impl Default for HttpConfig { fn default() -> Self { Self { port: DEFAULT_PORT, - hostname: DEFAULT_HOSTNAME.to_string(), + hostname: DEFAULT_HOSTNAME.to_owned(), https_p12_file: None, https_p12_password: None, authority_cert_file: None, diff --git a/crate/server/src/config/params/db_params.rs b/crate/server/src/config/params/db_params.rs index 731e8593..6cdd11aa 100644 --- a/crate/server/src/config/params/db_params.rs +++ b/crate/server/src/config/params/db_params.rs @@ -63,14 +63,16 @@ impl Display for DbParams { } /// Redact the username and password from the URL for logging purposes +#[allow(clippy::expect_used)] fn redact_url(original: &Url) -> Url { let mut url = original.clone(); if url.username() != "" { - url.set_username("****").unwrap(); + url.set_username("****").expect("masking username failed"); } if url.password().is_some() { - url.set_password(Some("****")).unwrap(); + url.set_password(Some("****")) + .expect("masking password failed"); } url diff --git a/crate/server/src/config/params/http_params.rs b/crate/server/src/config/params/http_params.rs index ce0accde..9facbc0c 100644 --- a/crate/server/src/config/params/http_params.rs +++ b/crate/server/src/config/params/http_params.rs @@ -1,4 +1,4 @@ -use std::{fmt, fs::File, io::Read}; +use std::fmt; use openssl::pkcs12::{ParsedPkcs12_2, Pkcs12}; @@ -13,16 +13,28 @@ pub enum HttpParams { Http, } +/// Represents the HTTP parameters for the server configuration. impl HttpParams { + /// Tries to create an instance of `HttpParams` from the given `HttpConfig`. + /// + /// # Arguments + /// + /// * `config` - The `HttpConfig` object containing the configuration parameters. + /// + /// # Returns + /// + /// Returns a `KResult` containing the created `HttpParams` instance on success. + /// + /// # Errors + /// + /// This function can return an error if there is an issue reading the PKCS#12 file or parsing it. pub fn try_from(config: &HttpConfig) -> KResult { // start in HTTPS mode if a PKCS#12 file is provided if let (Some(p12_file), Some(p12_password)) = (&config.https_p12_file, &config.https_p12_password) { // Open and read the file into a byte vector - let mut file = File::open(p12_file)?; - let mut der_bytes = Vec::new(); - file.read_to_end(&mut der_bytes)?; + let der_bytes = std::fs::read(p12_file)?; // Parse the byte vector as a PKCS#12 object let sealed_p12 = Pkcs12::from_der(der_bytes.as_slice())?; let p12 = sealed_p12 @@ -35,6 +47,11 @@ impl HttpParams { } } + /// Checks if the server is running in HTTPS mode. + /// + /// # Returns + /// + /// Returns `true` if the server is running in HTTPS mode, `false` otherwise. #[must_use] pub const fn is_running_https(&self) -> bool { matches!(self, Self::Https(_)) diff --git a/crate/server/src/config/params/server_params.rs b/crate/server/src/config/params/server_params.rs index a70ce246..52ed2f50 100644 --- a/crate/server/src/config/params/server_params.rs +++ b/crate/server/src/config/params/server_params.rs @@ -1,4 +1,4 @@ -use std::{fmt, fs::File, io::Read, path::PathBuf}; +use std::{fmt, path::PathBuf}; use openssl::x509::X509; @@ -59,7 +59,21 @@ pub struct ServerParams { pub ms_dke_service_url: Option, } +/// Represents the server parameters. impl ServerParams { + /// Tries to create a `ServerParams` instance from the given `ClapConfig`. + /// + /// # Arguments + /// + /// * `conf` - The `ClapConfig` object containing the configuration parameters. + /// + /// # Returns + /// + /// Returns a `KResult` containing the `ServerParams` instance if successful, or an error if the conversion fails. + /// + /// # Errors + /// + /// Returns an error if the conversion from `ClapConfig` to `ServerParams` fails. pub fn try_from(conf: ClapConfig) -> KResult { let http_params = HttpParams::try_from(&conf.http)?; @@ -95,11 +109,22 @@ impl ServerParams { }) } + /// Loads the certificate from the given file path. + /// + /// # Arguments + /// + /// * `authority_cert_file` - The path to the authority certificate file. + /// + /// # Returns + /// + /// Returns a `KResult` containing the loaded `X509` certificate if successful, or an error if the loading fails. + /// + /// # Errors + /// + /// Returns an error if the certificate file cannot be read or if the parsing of the certificate fails. fn load_cert(authority_cert_file: &PathBuf) -> KResult { // Open and read the file into a byte vector - let mut file = File::open(authority_cert_file)?; - let mut pem_bytes = Vec::new(); - file.read_to_end(&mut pem_bytes)?; + let pem_bytes = std::fs::read(authority_cert_file)?; // Parse the byte vector as a X509 object let x509 = X509::from_pem(pem_bytes.as_slice())?; diff --git a/crate/server/src/core/certificate/find.rs b/crate/server/src/core/certificate/find.rs index 7fa16dc8..937d1734 100644 --- a/crate/server/src/core/certificate/find.rs +++ b/crate/server/src/core/certificate/find.rs @@ -93,7 +93,7 @@ pub(crate) async fn retrieve_issuer_private_key_and_certificate( kms_bail!(KmsError::InvalidRequest( "Either an issuer certificate id or an issuer private key id or both must be provided" - .to_string(), + .to_owned(), )) } @@ -120,7 +120,7 @@ pub(crate) async fn retrieve_certificate_for_private_key( .attributes .get_link(LinkType::PublicKeyLink) .ok_or_else(|| { - KmsError::InvalidRequest("No public key link found for the private key".to_string()) + KmsError::InvalidRequest("No public key link found for the private key".to_owned()) })?; find_link_in_public_key( LinkType::CertificateLink, @@ -179,7 +179,7 @@ pub(crate) async fn retrieve_private_key_for_certificate( .get_link(LinkType::PublicKeyLink) .ok_or_else(|| { KmsError::InvalidRequest( - "No private or public key link found for the certificate".to_string(), + "No private or public key link found for the certificate".to_owned(), ) })?; find_link_in_public_key( diff --git a/crate/server/src/core/cover_crypt/create_user_decryption_key.rs b/crate/server/src/core/cover_crypt/create_user_decryption_key.rs index fd16ecd9..07377382 100644 --- a/crate/server/src/core/cover_crypt/create_user_decryption_key.rs +++ b/crate/server/src/core/cover_crypt/create_user_decryption_key.rs @@ -58,7 +58,7 @@ async fn create_user_decryption_key_( .ok_or_else(|| { KmsError::InvalidRequest( "there should be a reference to the master private key in the creation attributes" - .to_string(), + .to_owned(), ) })? .to_string(); @@ -130,7 +130,7 @@ pub(crate) async fn create_user_decryption_key_pair( .or(create_key_pair_request.common_attributes.as_ref()) .ok_or_else(|| { KmsError::InvalidRequest( - "Missing private attributes in CoverCrypt Create Keypair request".to_string(), + "Missing private attributes in CoverCrypt Create Keypair request".to_owned(), ) })?; let private_key = create_user_decryption_key_( @@ -149,13 +149,13 @@ pub(crate) async fn create_user_decryption_key_pair( .or(create_key_pair_request.common_attributes.as_ref()) .ok_or_else(|| { KmsError::InvalidRequest( - "Missing public attributes in CoverCrypt Create Keypair request".to_string(), + "Missing public attributes in CoverCrypt Create Keypair request".to_owned(), ) })?; let master_public_key_uid = public_key_attributes.get_parent_id().ok_or_else(|| { KmsError::InvalidRequest( "the master public key id should be available in the public creation attributes" - .to_string(), + .to_owned(), ) })?; let gr_public_key = kmip_server diff --git a/crate/server/src/core/cover_crypt/rekey_keys.rs b/crate/server/src/core/cover_crypt/rekey_keys.rs index 36c1d3d0..7827dd66 100644 --- a/crate/server/src/core/cover_crypt/rekey_keys.rs +++ b/crate/server/src/core/cover_crypt/rekey_keys.rs @@ -190,7 +190,7 @@ async fn get_master_keys_and_policy( .ok_or_else(|| { KmsError::KmipError( ErrorReason::Invalid_Object_Type, - "Private key MUST contain a public key link".to_string(), + "Private key MUST contain a public key link".to_owned(), ) })?; @@ -212,7 +212,7 @@ async fn import_rekeyed_master_keys( ) -> KResult<()> { // re-import master secret key let import_request = Import { - unique_identifier: UniqueIdentifier::TextString(msk.0.to_string()), + unique_identifier: UniqueIdentifier::TextString(msk.0), object_type: ObjectType::PrivateKey, replace_existing: Some(true), key_wrap_type: None, @@ -223,7 +223,7 @@ async fn import_rekeyed_master_keys( // re-import master public key let import_request = Import { - unique_identifier: UniqueIdentifier::TextString(mpk.0.to_string()), + unique_identifier: UniqueIdentifier::TextString(mpk.0), object_type: ObjectType::PublicKey, replace_existing: Some(true), key_wrap_type: None, diff --git a/crate/server/src/core/extra_database_params.rs b/crate/server/src/core/extra_database_params.rs index b5175747..d6011286 100644 --- a/crate/server/src/core/extra_database_params.rs +++ b/crate/server/src/core/extra_database_params.rs @@ -26,14 +26,19 @@ impl<'de> Deserialize<'de> for ExtraDatabaseParams { D: serde::Deserializer<'de>, { let bytes = Zeroizing::from(>::deserialize(deserializer)?); - let group_id_bytes: [u8; 16] = bytes[0..16] - .try_into() - .map_err(|_| serde::de::Error::custom("Could not deserialize ExtraDatabaseParams"))?; + let group_id_bytes: [u8; 16] = bytes[0..16].try_into().map_err(|e| { + serde::de::Error::custom(format!( + "Could not deserialize ExtraDatabaseParams. Error: {e:?}" + )) + })?; let group_id = u128::from_be_bytes(group_id_bytes); - let mut key_bytes: [u8; AES_256_GCM_KEY_LENGTH] = bytes[16..48] - .try_into() - .map_err(|_| serde::de::Error::custom("Could not deserialize ExtraDatabaseParams"))?; + let mut key_bytes: [u8; AES_256_GCM_KEY_LENGTH] = + bytes[16..48].try_into().map_err(|e| { + serde::de::Error::custom(format!( + "Could not deserialize ExtraDatabaseParams. Error: {e:?}" + )) + })?; let key = Secret::::from_unprotected_bytes(&mut key_bytes); Ok(Self { group_id, key }) } diff --git a/crate/server/src/core/implementation.rs b/crate/server/src/core/implementation.rs index 39075852..dc19e31b 100644 --- a/crate/server/src/core/implementation.rs +++ b/crate/server/src/core/implementation.rs @@ -60,7 +60,7 @@ impl KMS { DbParams::RedisFindex(url, master_key, label) => { // There is no reason to keep a copy of the key in the shared config // So we are going to create a "zeroizable" copy which will be passed to Redis with Findex - // and zerorize the one in the shared config + // and zeroize the one in the shared config let new_master_key = Secret::::from_unprotected_bytes( &mut master_key.to_bytes(), @@ -94,7 +94,7 @@ impl KMS { // check that the cryptographic algorithm is specified let cryptographic_algorithm = &attributes.cryptographic_algorithm.ok_or_else(|| { KmsError::InvalidRequest( - "the cryptographic algorithm must be specified for secret key creation".to_string(), + "the cryptographic algorithm must be specified for secret key creation".to_owned(), ) })?; @@ -102,7 +102,7 @@ impl KMS { let mut tags = attributes.get_tags(); Attributes::check_user_tags(&tags)?; //update the tags - tags.insert("_kk".to_string()); + tags.insert("_kk".to_owned()); match cryptographic_algorithm { CryptographicAlgorithm::AES @@ -115,14 +115,15 @@ impl KMS { | CryptographicAlgorithm::SHAKE128 | CryptographicAlgorithm::SHAKE256 => match attributes.key_format_type { None => Err(KmsError::InvalidRequest( - "Unable to create a symmetric key, the format type is not specified" - .to_string(), + "Unable to create a symmetric key, the format type is not specified".to_owned(), )), Some(KeyFormatType::TransparentSymmetricKey) => { // create the key - let key_len: usize = attributes + let key_len = attributes .cryptographic_length - .map_or(AES_256_GCM_KEY_LENGTH, |v| v as usize / 8); + .map(|len| usize::try_from(len / 8)) + .transpose()? + .map_or(AES_256_GCM_KEY_LENGTH, |v| v); let mut symmetric_key = Zeroizing::from(vec![0; key_len]); rand_bytes(&mut symmetric_key)?; let object = @@ -159,8 +160,7 @@ impl KMS { // check that the cryptographic algorithm is specified let cryptographic_algorithm = &attributes.cryptographic_algorithm.ok_or_else(|| { KmsError::InvalidRequest( - "the cryptographic algorithm must be specified for private key creation" - .to_string(), + "the cryptographic algorithm must be specified for private key creation".to_owned(), ) })?; @@ -168,7 +168,7 @@ impl KMS { let mut tags = attributes.get_tags(); Attributes::check_user_tags(&tags)?; //update the tags - tags.insert("_uk".to_string()); + tags.insert("_uk".to_owned()); match &cryptographic_algorithm { CryptographicAlgorithm::CoverCrypt => { diff --git a/crate/server/src/core/kms.rs b/crate/server/src/core/kms.rs index a8bf02b3..e5e40d00 100644 --- a/crate/server/src/core/kms.rs +++ b/crate/server/src/core/kms.rs @@ -553,7 +553,7 @@ impl KMS { if owner == access.user_id { kms_bail!(KmsError::Unauthorized( "You can't grant yourself, you have already all rights on your own objects" - .to_string() + .to_owned() )) } @@ -595,7 +595,7 @@ impl KMS { if owner == access.user_id { kms_bail!(KmsError::Unauthorized( "You can't revoke yourself, you should keep all rights on your own objects" - .to_string() + .to_owned() )) } diff --git a/crate/server/src/core/operations/certify/mod.rs b/crate/server/src/core/operations/certify/mod.rs index f86ae259..50d5c397 100644 --- a/crate/server/src/core/operations/certify/mod.rs +++ b/crate/server/src/core/operations/certify/mod.rs @@ -258,7 +258,7 @@ async fn get_subject( CertificateRequestType::PEM => X509Req::from_pem(pkcs10_bytes), CertificateRequestType::PKCS10 => X509Req::from_der(pkcs10_bytes), CertificateRequestType::CRMF => kms_bail!(KmsError::InvalidRequest( - "Certificate Request Type CRMF not supported".to_string() + "Certificate Request Type CRMF not supported".to_owned() )), }?; let certificate_id = request @@ -312,7 +312,7 @@ async fn get_subject( let attributes = request.attributes.as_ref().ok_or_else(|| { KmsError::InvalidRequest( "Certify from Subject: the attributes specifying the the subject name are missing" - .to_string(), + .to_owned(), ) })?; let subject_name = attributes @@ -320,7 +320,7 @@ async fn get_subject( .as_ref() .ok_or_else(|| { KmsError::InvalidRequest( - "Certify from Subject: the subject name is not found in the attributes".to_string(), + "Certify from Subject: the subject name is not found in the attributes".to_owned(), ) })? .subject_name()?; @@ -342,7 +342,7 @@ async fn get_subject( let (private_attributes, public_attributes) = { let cryptographic_algorithm = attributes.cryptographic_algorithm.ok_or_else(|| { KmsError::InvalidRequest( - "Keypair creation: the cryptographic algorithm is missing".to_string(), + "Keypair creation: the cryptographic algorithm is missing".to_owned(), ) })?; let private_attributes = Attributes { @@ -410,7 +410,10 @@ async fn get_issuer<'a>( } None => (None, None), }; - + trace!( + "Issuer certificate id: {issuer_certificate_id:?}, issuer private key id: \ + {issuer_private_key_id:?}" + ); if issuer_certificate_id.is_none() && issuer_private_key_id.is_none() { // If no issuer is provided, the subject is self-signed return issuer_for_self_signed_certificate(subject, kms, user, params).await; @@ -479,7 +482,7 @@ async fn issuer_for_self_signed_certificate<'a>( .ok_or_else(|| { KmsError::InvalidRequest( "No private key linked to the certificate found to renew it as self-signed" - .to_string(), + .to_owned(), ) })?; Ok(Issuer::PrivateKeyAndCertificate( @@ -503,7 +506,7 @@ async fn issuer_for_self_signed_certificate<'a>( KmsError::InvalidRequest( "No private key link found to create a self-signed certificate from a public \ key" - .to_string(), + .to_owned(), ) })?; // see if we can find an existing certificate to link to the public key @@ -543,7 +546,7 @@ fn build_and_sign_certificate( issuer: &Issuer, subject: &Subject, request: Certify, -) -> Result<(Object, HashSet, Attributes), KmsError> { +) -> KResult<(Object, HashSet, Attributes)> { debug!("Building and signing certificate"); // recover the attributes let mut attributes = request.attributes.unwrap_or_default(); @@ -566,10 +569,15 @@ fn build_and_sign_certificate( // Create a new Asn1Time object for the current time let now = Asn1Time::days_from_now(0).context("could not get a date in ASN.1")?; // retrieve the number of days for the validity of the certificate - let mut number_of_days = attributes.extract_requested_validity_days()?.unwrap_or(365) as u32; + let mut number_of_days = + u32::try_from(attributes.extract_requested_validity_days()?.unwrap_or(365))?; + trace!("Number of days: {}", number_of_days); + // the number of days cannot exceed that of the issuer certificate if let Some(issuer_not_after) = issuer.not_after() { - number_of_days = min(issuer_not_after.diff(&now)?.days as u32, number_of_days); + trace!("Issuer certificate not after: {issuer_not_after}"); + let days = u32::try_from(now.diff(issuer_not_after)?.days)?; + number_of_days = min(days, number_of_days); } x509_builder.set_not_before(now.as_ref())?; x509_builder.set_not_after( @@ -611,7 +619,7 @@ fn build_and_sign_certificate( // add subject tags if any tags.extend(subject.tags().iter().cloned()); // add the certificate "system" tag - tags.insert("_cert".to_string()); + tags.insert("_cert".to_owned()); // link the certificate to the issuer certificate attributes.add_link( diff --git a/crate/server/src/core/operations/create_key_pair.rs b/crate/server/src/core/operations/create_key_pair.rs index 2c2eb450..feb53a31 100644 --- a/crate/server/src/core/operations/create_key_pair.rs +++ b/crate/server/src/core/operations/create_key_pair.rs @@ -106,9 +106,9 @@ pub(crate) fn generate_key_pair_and_tags( Attributes::check_user_tags(&tags)?; // Update the tags for the private key and the public key. let mut sk_tags = tags.clone(); - sk_tags.insert("_sk".to_string()); + sk_tags.insert("_sk".to_owned()); let mut pk_tags = tags; - pk_tags.insert("_pk".to_string()); + pk_tags.insert("_pk".to_owned()); // Grab whatever attributes were supplied on the create request. let any_attributes = Some(&common_attributes) @@ -133,7 +133,7 @@ pub(crate) fn generate_key_pair_and_tags( // Check that the cryptographic algorithm is specified. let cryptographic_algorithm = any_attributes.cryptographic_algorithm.ok_or_else(|| { KmsError::InvalidRequest( - "the cryptographic algorithm must be specified for key pair creation".to_string(), + "the cryptographic algorithm must be specified for key pair creation".to_owned(), ) })?; @@ -195,7 +195,7 @@ pub(crate) fn generate_key_pair_and_tags( || cryptographic_algorithm == CryptographicAlgorithm::EC { kms_bail!(KmsError::NotSupported( - "Edwards curve can't be created for EC or ECDSA".to_string() + "Edwards curve can't be created for EC or ECDSA".to_owned() )) } warn!( @@ -217,7 +217,7 @@ pub(crate) fn generate_key_pair_and_tags( kms_bail!(KmsError::NotSupported( "An Edwards Keypair on curve 25519 should not be requested to perform \ Elliptic Curves operations in FIPS mode" - .to_string() + .to_owned() )) } #[cfg(not(feature = "fips"))] @@ -226,7 +226,7 @@ pub(crate) fn generate_key_pair_and_tags( || cryptographic_algorithm == CryptographicAlgorithm::EC { kms_bail!(KmsError::NotSupported( - "Edwards curve can't be created for EC or ECDSA".to_string() + "Edwards curve can't be created for EC or ECDSA".to_owned() )) } warn!( @@ -248,7 +248,7 @@ pub(crate) fn generate_key_pair_and_tags( kms_bail!(KmsError::NotSupported( "An Edwards Keypair on curve 448 should not be requested to perform ECDH \ in FIPS mode." - .to_string() + .to_owned() )) } other => kms_bail!(KmsError::NotSupported(format!( @@ -260,7 +260,7 @@ pub(crate) fn generate_key_pair_and_tags( let key_size_in_bits = u32::try_from( any_attributes .cryptographic_length - .ok_or_else(|| KmsError::InvalidRequest("RSA key size: error".to_string()))?, + .ok_or_else(|| KmsError::InvalidRequest("RSA key size: error".to_owned()))?, )?; trace!("RSA key pair generation: size in bits: {key_size_in_bits}"); diff --git a/crate/server/src/core/operations/decrypt.rs b/crate/server/src/core/operations/decrypt.rs index 066bdb55..25259b15 100644 --- a/crate/server/src/core/operations/decrypt.rs +++ b/crate/server/src/core/operations/decrypt.rs @@ -203,7 +203,7 @@ fn dispatch_decrypt(request: &Decrypt, owm: &ObjectWithMetadata) -> KResult KResult Result<(String, Vec) Attributes::check_user_tags(tags)?; // Insert the tag corresponding to the object type if tags should be // updated. - tags.insert("_cert".to_string()); + tags.insert("_cert".to_owned()); } // check if the object will be replaced if it already exists @@ -213,7 +213,7 @@ async fn process_public_key( let mut tags = attributes.remove_tags(); if let Some(tags) = tags.as_mut() { Attributes::check_user_tags(tags)?; - tags.insert("_pk".to_string()); + tags.insert("_pk".to_owned()); } // check if the object will be replaced if it already exists @@ -384,7 +384,7 @@ fn private_key_from_openssl( let sk_uid = if request_uid.is_empty() { Uuid::new_v4().to_string() } else { - request_uid.to_string() + request_uid.to_owned() }; let sk_key_block = sk.key_block_mut()?; @@ -399,7 +399,7 @@ fn private_key_from_openssl( ); let sk_tags = user_tags.map(|mut tags| { - tags.insert("_sk".to_string()); + tags.insert("_sk".to_owned()); tags }); Ok((sk_uid, sk, sk_tags)) @@ -460,7 +460,7 @@ fn process_pkcs12( // build the private key let (private_key_id, mut private_key, private_key_tags) = { let openssl_sk = pkcs12.pkey.ok_or_else(|| { - KmsError::InvalidRequest("Private key not found in PKCS12".to_string()) + KmsError::InvalidRequest("Private key not found in PKCS12".to_owned()) })?; private_key_from_openssl( &openssl_sk, @@ -479,12 +479,12 @@ fn process_pkcs12( ) = { // Recover the PKCS12 X509 certificate let openssl_cert = pkcs12.cert.ok_or_else(|| { - KmsError::InvalidRequest("X509 certificate not found in PKCS12".to_string()) + KmsError::InvalidRequest("X509 certificate not found in PKCS12".to_owned()) })?; // insert the tag corresponding to the object type if tags should be updated let mut leaf_certificate_tags = user_tags.clone().unwrap_or_default(); - leaf_certificate_tags.insert("_cert".to_string()); + leaf_certificate_tags.insert("_cert".to_owned()); // convert to KMIP let leaf_certificate = openssl_certificate_to_kmip(&openssl_cert)?; @@ -504,7 +504,7 @@ fn process_pkcs12( for openssl_cert in cas { // insert the tag corresponding to the object type if tags should be updated let mut chain_certificate_tags = user_tags.clone().unwrap_or_default(); - chain_certificate_tags.insert("_cert".to_string()); + chain_certificate_tags.insert("_cert".to_owned()); // convert to KMIP let chain_certificate = openssl_certificate_to_kmip(&openssl_cert)?; diff --git a/crate/server/src/core/operations/locate.rs b/crate/server/src/core/operations/locate.rs index 2f7a2d3b..2cc2880d 100644 --- a/crate/server/src/core/operations/locate.rs +++ b/crate/server/src/core/operations/locate.rs @@ -40,7 +40,7 @@ pub(crate) async fn locate( trace!("UIDs: {:?}", uids); let response = LocateResponse { - located_items: Some(uids.len() as i32), + located_items: Some(i32::try_from(uids.len())?), unique_identifiers: if uids.is_empty() { None } else { Some(uids) }, }; diff --git a/crate/server/src/core/operations/message.rs b/crate/server/src/core/operations/message.rs index dbd75dce..aa12fb5f 100644 --- a/crate/server/src/core/operations/message.rs +++ b/crate/server/src/core/operations/message.rs @@ -70,11 +70,11 @@ pub(crate) async fn message( let response_message = MessageResponse { header: MessageResponseHeader { protocol_version: request.header.protocol_version, - batch_count: response_items.len() as u32, + batch_count: u32::try_from(response_items.len())?, client_correlation_value: None, server_correlation_value: None, attestation_type: None, - timestamp: chrono::Utc::now().timestamp() as u64, + timestamp: u64::try_from(chrono::Utc::now().timestamp())?, nonce: None, server_hashed_password: None, }, diff --git a/crate/server/src/core/operations/rekey.rs b/crate/server/src/core/operations/rekey.rs index 0e35349c..9da43b1d 100644 --- a/crate/server/src/core/operations/rekey.rs +++ b/crate/server/src/core/operations/rekey.rs @@ -58,7 +58,7 @@ pub(crate) async fn rekey( // there can only be one private key let owm = owm_s .pop() - .ok_or_else(|| KmsError::KmipError(ErrorReason::Item_Not_Found, uid_or_tags.to_string()))?; + .ok_or_else(|| KmsError::KmipError(ErrorReason::Item_Not_Found, uid_or_tags.to_owned()))?; if !owm_s.is_empty() { return Err(KmsError::InvalidRequest(format!( diff --git a/crate/server/src/core/operations/rekey_keypair.rs b/crate/server/src/core/operations/rekey_keypair.rs index 085fbedb..15b3a81e 100644 --- a/crate/server/src/core/operations/rekey_keypair.rs +++ b/crate/server/src/core/operations/rekey_keypair.rs @@ -71,7 +71,7 @@ pub(crate) async fn rekey_keypair( // there can only be one private key let owm = owm_s .pop() - .ok_or_else(|| KmsError::KmipError(ErrorReason::Item_Not_Found, uid_or_tags.to_string()))?; + .ok_or_else(|| KmsError::KmipError(ErrorReason::Item_Not_Found, uid_or_tags.to_owned()))?; if !owm_s.is_empty() { return Err(KmsError::InvalidRequest(format!( @@ -90,7 +90,7 @@ pub(crate) async fn rekey_keypair( kms_bail!(KmsError::InvalidRequest( "The cryptographic algorithm must be specified in the private key attributes for key \ pair creation" - .to_string() + .to_owned() )) } } diff --git a/crate/server/src/core/operations/revoke.rs b/crate/server/src/core/operations/revoke.rs index cb379f94..88ae29f5 100644 --- a/crate/server/src/core/operations/revoke.rs +++ b/crate/server/src/core/operations/revoke.rs @@ -53,7 +53,7 @@ pub(crate) async fn revoke_operation( .await?; Ok(RevokeResponse { - unique_identifier: UniqueIdentifier::TextString(uid_or_tags.to_string()), + unique_identifier: UniqueIdentifier::TextString(uid_or_tags.to_owned()), }) } diff --git a/crate/server/src/core/operations/validate.rs b/crate/server/src/core/operations/validate.rs index cacc53b4..8bb4742c 100644 --- a/crate/server/src/core/operations/validate.rs +++ b/crate/server/src/core/operations/validate.rs @@ -81,7 +81,7 @@ pub(crate) async fn validate_operation( { (None, None) => { return Err(KmsError::Certificate( - "Empty chain cannot be validated".to_string(), + "Empty chain cannot be validated".to_owned(), )); } (None, Some(certificates)) => Ok::<_, KmsError>((certificates.clone(), certificates.len())), @@ -112,7 +112,7 @@ pub(crate) async fn validate_operation( return Err(KmsError::Certificate( "Number of certificates found in database and number of certificates in request do \ not match" - .to_string(), + .to_owned(), )); }; @@ -249,7 +249,7 @@ fn sort_certificates(certificates: &[X509]) -> KResult> { if sorted_chains.is_empty() { return Err(KmsError::Certificate( - "No root authority found, cannot proceed full chain validation".to_string(), + "No root authority found, cannot proceed full chain validation".to_owned(), )); } @@ -310,7 +310,7 @@ fn sort_certificates(certificates: &[X509]) -> KResult> { if sorted_chains.len() != certificates.len() { return Err(KmsError::Certificate( - "Failed to sort the certificates. Certificate chain incomplete?".to_string(), + "Failed to sort the certificates. Certificate chain incomplete?".to_owned(), )); } @@ -343,7 +343,7 @@ fn sort_certificates(certificates: &[X509]) -> KResult> { fn verify_chain_signature(certificates: &[X509]) -> KResult { if certificates.is_empty() { return Err(KmsError::Certificate( - "Certificate chain is empty".to_string(), + "Certificate chain is empty".to_owned(), )); } @@ -352,7 +352,7 @@ fn verify_chain_signature(certificates: &[X509]) -> KResult { // Get leaf let leaf = certificates.last().ok_or_else(|| { - KmsError::Certificate("Failed to get last element of the chain".to_string()) + KmsError::Certificate("Failed to get last element of the chain".to_owned()) })?; // Add authorities to the store @@ -378,7 +378,7 @@ fn verify_chain_signature(certificates: &[X509]) -> KResult { if !result { return Err(KmsError::Certificate( - "Result of the function verify_cert: {result:?}".to_string(), + "Result of the function verify_cert: {result:?}".to_owned(), )); } @@ -386,7 +386,7 @@ fn verify_chain_signature(certificates: &[X509]) -> KResult { let mut issuer_public_key = certificates .first() .ok_or_else(|| { - KmsError::Certificate("Failed to get the first element of the chain".to_string()) + KmsError::Certificate("Failed to get the first element of the chain".to_owned()) })? .public_key()?; for cert in certificates { @@ -441,10 +441,10 @@ async fn get_crl_bytes(uri_list: Vec) -> KResult } else { let path_buf = path::Path::new(&uri).canonicalize()?; match path_buf.to_str() { - Some(s) => Some(UriType::Path(s.to_string())), + Some(s) => Some(UriType::Path(s.to_owned())), None => { return Err(KmsError::Certificate( - "The uri provided is invalid".to_string(), + "The uri provided is invalid".to_owned(), )) } } @@ -499,7 +499,7 @@ async fn get_crl_bytes(uri_list: Vec) -> KResult } _ => { return Err(KmsError::Certificate( - "Error that should not manifest".to_string(), + "Error that should not manifest".to_owned(), )) } }; @@ -555,7 +555,7 @@ async fn verify_crls(certificates: Vec) -> KResult { debug!("Parent CRL verification: revocation status: {res:?}"); if res == ValidityIndicator::Invalid { return Err(KmsError::Certificate( - "Certificate is revoked or removed from CRL".to_string(), + "Certificate is revoked or removed from CRL".to_owned(), )); } } @@ -571,8 +571,8 @@ async fn verify_crls(certificates: Vec) -> KResult { .and_then(|x| x.get(0)) .and_then(GeneralNameRef::uri); if let Some(crl_uri) = crl_uri { - if !uri_list.contains(&crl_uri.to_string()) { - uri_list.push(crl_uri.to_string()); + if !uri_list.contains(&crl_uri.to_owned()) { + uri_list.push(crl_uri.to_owned()); trace!("Found CRL URI: {crl_uri}"); } } @@ -602,7 +602,7 @@ async fn verify_crls(certificates: Vec) -> KResult { debug!("Revocation status: result: {res:?}"); if res == ValidityIndicator::Invalid { return Err(KmsError::Certificate( - "Certificate is revoked or removed from CRL".to_string(), + "Certificate is revoked or removed from CRL".to_owned(), )); } } @@ -624,7 +624,7 @@ async fn certificates_by_uid( let mut results = Vec::new(); for unique_identifier in unique_identifiers { let unique_identifier = unique_identifier.as_str().ok_or_else(|| { - KmsError::Certificate("as_str returned None in certificates_by_uid".to_string()) + KmsError::Certificate("as_str returned None in certificates_by_uid".to_owned()) })?; let result = certificate_by_uid(unique_identifier, kms, user, params).await?; results.push(result); diff --git a/crate/server/src/core/operations/wrapping/unwrap.rs b/crate/server/src/core/operations/wrapping/unwrap.rs index 83f72c49..4677cb93 100644 --- a/crate/server/src/core/operations/wrapping/unwrap.rs +++ b/crate/server/src/core/operations/wrapping/unwrap.rs @@ -57,7 +57,7 @@ pub(crate) async fn unwrap_key( ObjectType::PublicKey | ObjectType::Certificate => { let attributes = match object_type { ObjectType::PublicKey | ObjectType::Certificate => unwrapping_key.attributes, - _ => unreachable!("unwrap_key: unsupported object type: {object_type}"), + _ => kms_bail!("unwrap_key: unsupported object type: {object_type}"), }; let private_key_uid = attributes diff --git a/crate/server/src/database/cached_sqlcipher.rs b/crate/server/src/database/cached_sqlcipher.rs index ca3dc8b9..7b19f39a 100644 --- a/crate/server/src/database/cached_sqlcipher.rs +++ b/crate/server/src/database/cached_sqlcipher.rs @@ -6,6 +6,7 @@ use std::{ }; use async_trait::async_trait; +use clap::crate_version; use cosmian_kmip::{ crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}, kmip::{ @@ -16,9 +17,9 @@ use cosmian_kmip::{ use cosmian_kms_client::access::{IsWrapped, ObjectOperationType}; use sqlx::{ sqlite::{SqliteConnectOptions, SqlitePoolOptions}, - ConnectOptions, Pool, Sqlite, + ConnectOptions, Pool, Row, Sqlite, }; -use tracing::trace; +use tracing::{debug, trace}; use super::{ cached_sqlite_struct::KMSSqliteCache, @@ -33,10 +34,11 @@ use crate::{ core::extra_database_params::ExtraDatabaseParams, database::{ database_trait::AtomicOperation, - sqlite::{atomic_, retrieve_tags_}, - Database, SQLITE_QUERIES, + migrate::do_migration, + sqlite::{atomic_, is_migration_in_progress_, migrate_, retrieve_tags_}, + Database, KMS_VERSION_BEFORE_MIGRATION_SUPPORT, SQLITE_QUERIES, }, - kms_bail, kms_error, + get_sqlite_query, kms_bail, kms_error, result::{KResult, KResultHelper}, }; @@ -88,29 +90,21 @@ impl CachedSqlCipher { } async fn create_tables(pool: &Pool) -> KResult<()> { - sqlx::query( - SQLITE_QUERIES - .get("create-table-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(pool) - .await?; + sqlx::query(get_sqlite_query!("create-table-context")) + .execute(pool) + .await?; - sqlx::query( - SQLITE_QUERIES - .get("create-table-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(pool) - .await?; + sqlx::query(get_sqlite_query!("create-table-objects")) + .execute(pool) + .await?; - sqlx::query( - SQLITE_QUERIES - .get("create-table-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(pool) - .await?; + sqlx::query(get_sqlite_query!("create-table-read_access")) + .execute(pool) + .await?; + + sqlx::query(get_sqlite_query!("create-table-tags")) + .execute(pool) + .await?; Ok(()) } @@ -124,11 +118,11 @@ impl CachedSqlCipher { group_id: u128, key: &Secret, ) -> KResult>> { - if !self.cache.exists(group_id) { + if !self.cache.exists(group_id)? { let pool = self.instantiate_group_database(group_id, key).await?; Self::create_tables(&pool).await?; self.cache.save(group_id, key, pool).await?; - } else if !self.cache.opened(group_id) { + } else if !self.cache.opened(group_id)? { let pool = self.instantiate_group_database(group_id, key).await?; self.cache.save(group_id, key, pool).await?; } @@ -143,6 +137,50 @@ impl Database for CachedSqlCipher { Some(self.path.join(format!("{group_id}.sqlite"))) } + async fn migrate(&self, params: Option<&ExtraDatabaseParams>) -> KResult<()> { + if let Some(params) = params { + let pool = self.pre_query(params.group_id, ¶ms.key).await?; + + trace!("Migrate database"); + // Get the context rows + match sqlx::query(get_sqlite_query!("select-context")) + .fetch_optional(&*pool) + .await? + { + None => { + trace!("No context row found, migrating from scratch"); + return migrate_( + &pool, + KMS_VERSION_BEFORE_MIGRATION_SUPPORT, + "insert-context", + ) + .await; + } + Some(context_row) => { + let last_kms_version_run = context_row.get::(0); + let state = context_row.get::(1); + trace!( + "Context row found, migrating from version {last_kms_version_run} (state: \ + {state})" + ); + let current_kms_version = crate_version!(); + debug!( + "[state={state}] Last KMS version run: {last_kms_version_run}, Current \ + KMS version: {current_kms_version}" + ); + + if do_migration(&last_kms_version_run, current_kms_version, &state)? { + return migrate_(&pool, current_kms_version, "update-context").await; + } + } + } + + return Ok(()); + } + + kms_bail!("Missing group_id/key for opening SQLCipher") + } + async fn create( &self, uid: Option, @@ -154,6 +192,10 @@ impl Database for CachedSqlCipher { ) -> KResult { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = pool.begin().await?; match create_(uid, owner, object, attributes, tags, &mut tx).await { Ok(uid) => { @@ -174,14 +216,14 @@ impl Database for CachedSqlCipher { async fn retrieve( &self, - uid: &str, + uid_or_tags: &str, user: &str, - operation_type: ObjectOperationType, + query_access_grant: ObjectOperationType, params: Option<&ExtraDatabaseParams>, ) -> KResult> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; - let ret = retrieve_(uid, user, operation_type, &*pool).await; + let ret = retrieve_(uid_or_tags, user, query_access_grant, &*pool).await; self.post_query(params.group_id)?; return ret } @@ -214,6 +256,10 @@ impl Database for CachedSqlCipher { ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = pool.begin().await?; match update_object_(uid, object, attributes, tags, &mut tx).await { Ok(()) => { @@ -240,6 +286,9 @@ impl Database for CachedSqlCipher { ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = pool.begin().await?; match update_state_(uid, state, &mut tx).await { Ok(()) => { @@ -270,6 +319,9 @@ impl Database for CachedSqlCipher { ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = pool.begin().await?; match upsert_(uid, user, object, attributes, tags, state, &mut tx).await { Ok(()) => { @@ -291,13 +343,16 @@ impl Database for CachedSqlCipher { async fn delete( &self, uid: &str, - owner: &str, + user: &str, params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = pool.begin().await?; - match delete_(uid, owner, &mut tx).await { + match delete_(uid, user, &mut tx).await { Ok(()) => { tx.commit().await?; self.post_query(params.group_id)?; @@ -316,12 +371,12 @@ impl Database for CachedSqlCipher { async fn list_user_granted_access_rights( &self, - owner: &str, + user: &str, params: Option<&ExtraDatabaseParams>, ) -> KResult)>> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; - let ret = list_user_granted_access_rights_(owner, &*pool).await; + let ret = list_user_granted_access_rights_(user, &*pool).await; self.post_query(params.group_id)?; return ret } @@ -347,13 +402,16 @@ impl Database for CachedSqlCipher { async fn grant_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; - let ret = insert_access_(uid, userid, operation_types, &*pool).await; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let ret = insert_access_(uid, user, operation_types, &*pool).await; self.post_query(params.group_id)?; return ret } @@ -364,13 +422,16 @@ impl Database for CachedSqlCipher { async fn remove_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; - let ret = remove_access_(uid, userid, operation_types, &*pool).await; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let ret = remove_access_(uid, user, operation_types, &*pool).await; self.post_query(params.group_id)?; return ret } @@ -381,12 +442,12 @@ impl Database for CachedSqlCipher { async fn is_object_owned_by( &self, uid: &str, - userid: &str, + owner: &str, params: Option<&ExtraDatabaseParams>, ) -> KResult { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; - let ret = is_object_owned_by_(uid, userid, &*pool).await; + let ret = is_object_owned_by_(uid, owner, &*pool).await; self.post_query(params.group_id)?; return ret } @@ -424,7 +485,7 @@ impl Database for CachedSqlCipher { async fn list_user_access_rights_on_object( &self, uid: &str, - userid: &str, + user: &str, no_inherited_access: bool, params: Option<&ExtraDatabaseParams>, ) -> KResult> { @@ -433,7 +494,7 @@ impl Database for CachedSqlCipher { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; let ret = - list_user_access_rights_on_object_(uid, userid, no_inherited_access, &*pool).await; + list_user_access_rights_on_object_(uid, user, no_inherited_access, &*pool).await; self.post_query(params.group_id)?; return ret } @@ -443,14 +504,17 @@ impl Database for CachedSqlCipher { async fn atomic( &self, - owner: &str, + user: &str, operations: &[AtomicOperation], params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { if let Some(params) = params { let pool = self.pre_query(params.group_id, ¶ms.key).await?; + if is_migration_in_progress_(&*pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = pool.begin().await?; - return match atomic_(owner, operations, &mut tx).await { + return match atomic_(user, operations, &mut tx).await { Ok(()) => { tx.commit().await?; self.post_query(params.group_id)?; diff --git a/crate/server/src/database/cached_sqlite_struct.rs b/crate/server/src/database/cached_sqlite_struct.rs index 4c50ddf1..8ea3b584 100644 --- a/crate/server/src/database/cached_sqlite_struct.rs +++ b/crate/server/src/database/cached_sqlite_struct.rs @@ -13,7 +13,7 @@ use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; use sqlx::{Pool, Sqlite}; use tracing::info; -use crate::{kms_bail, kms_error, result::KResult}; +use crate::{error::KmsError, kms_bail, kms_error, result::KResult}; macro_rules! mac { ($res: expr, $key:expr, $($bytes: expr),+) => { @@ -64,26 +64,31 @@ impl fmt::Debug for KMSSqliteCacheItem { } /// Give the time since EPOCH in secs -pub(crate) fn _now() -> u64 { - SystemTime::now() +pub(crate) fn _now() -> KResult { + Ok(SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unable to get duration since epoch") - .as_secs() + .map_err(|e| { + KmsError::DatabaseError(format!("Unable to get duration since epoch. Error: {e:?}")) + })? + .as_secs()) } impl KMSSqliteCacheItem { - #[must_use] - pub(crate) fn new(sqlite: Pool, mac: Vec, freeable_cache_index: usize) -> Self { - Self { + pub(crate) fn new( + sqlite: Pool, + mac: Vec, + freeable_cache_index: usize, + ) -> KResult { + Ok(Self { sqlite: Arc::new(sqlite), mac, - inserted_at: _now(), + inserted_at: _now()?, in_used: 0, last_used_at: 0, closed: false, closed_at: 0, freeable_cache_index, - } + }) } } @@ -122,21 +127,24 @@ impl KMSSqliteCache { } /// Test if a sqlite connection is opened for a given id - pub(crate) fn opened(&self, id: u128) -> bool { - let sqlites = self.sqlites.read().expect("Unable to lock for read"); + pub(crate) fn opened(&self, id: u128) -> KResult { + let sqlites = self.sqlites.read().map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for read. Error: {e:?}")) + })?; if !sqlites.contains_key(&id) { - return false + return Ok(false); } - !sqlites[&id].closed + Ok(!sqlites[&id].closed) } /// Test if a sqlite connection exist in the cache - pub(crate) fn exists(&self, id: u128) -> bool { - self.sqlites + pub(crate) fn exists(&self, id: u128) -> KResult { + Ok(self + .sqlites .read() - .expect("Unable to lock for read") - .contains_key(&id) + .map_err(|e| KmsError::DatabaseError(format!("Unable to lock for read. Error: {e:?}")))? + .contains_key(&id)) } /// Get the sqlite handler and tag it as "used" @@ -147,7 +155,9 @@ impl KMSSqliteCache { id: u128, key: &Secret, ) -> KResult>> { - let mut sqlites = self.sqlites.write().expect("Unable to lock for write"); + let mut sqlites = self.sqlites.write().map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })?; let item = sqlites .get_mut(&id) @@ -160,7 +170,7 @@ impl KMSSqliteCache { // We need to check if the key provided by the user is the same that was used to open the database // If we do not, we can just send any password: the database is already opened anyway. // Do this by checking the macs - let mut mac = vec![0u8; 32]; + let mut mac = vec![0_u8; 32]; mac!(mac.as_mut_slice(), key, id.to_be_bytes().as_slice()); if mac != item.mac { kms_bail!("Database secret is wrong"); @@ -170,12 +180,14 @@ impl KMSSqliteCache { if item.in_used == 0 { self.freeable_sqlites .write() - .expect("Unable to lock for write") + .map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })? .uncache(item.freeable_cache_index)?; } item.in_used += 1; - item.last_used_at = _now(); + item.last_used_at = _now()?; Ok(Arc::clone(&item.sqlite)) } @@ -185,7 +197,9 @@ impl KMSSqliteCache { /// /// The function will return an error if the database is not in the cache or already released pub(crate) fn release(&self, id: u128) -> KResult<()> { - let mut sqlites = self.sqlites.write().expect("Unable to lock for write"); + let mut sqlites = self.sqlites.write().map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })?; let item = sqlites .get_mut(&id) @@ -201,7 +215,9 @@ impl KMSSqliteCache { if item.in_used == 0 { self.freeable_sqlites .write() - .expect("Unable to lock for write") + .map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })? .recache(item.freeable_cache_index)?; } @@ -219,20 +235,24 @@ impl KMSSqliteCache { let id = self .freeable_sqlites .write() - .expect("Unable to lock for write") + .map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })? .pop(); let Ok(id) = id else { break }; // nothing in the cache, just leave let sq = { - let mut sqlites = self.sqlites.write().expect("Unable to lock for write"); + let mut sqlites = self.sqlites.write().map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })?; let item = sqlites .get_mut(&id) .ok_or_else(|| kms_error!("Key is not in the cache"))?; item.closed = true; - item.closed_at = _now(); + item.closed_at = _now()?; info!("CachedSQLCipher: freeing = {item:?}"); @@ -266,12 +286,13 @@ impl KMSSqliteCache { self.flush().await?; // If nothing has been flush, allow to exceed max cache size - let mut sqlites = self.sqlites.write().expect("Unable to lock for write"); + let mut sqlites = self.sqlites.write().map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })?; - let mut freeable_sqlites = self - .freeable_sqlites - .write() - .expect("Unable to lock for write"); + let mut freeable_sqlites = self.freeable_sqlites.write().map_err(|e| { + KmsError::DatabaseError(format!("Unable to lock for write. Error: {e:?}")) + })?; let item = sqlites.get_mut(&id); if let Some(item) = item { @@ -285,7 +306,7 @@ impl KMSSqliteCache { item.sqlite = Arc::new(pool); item.closed = false; item.in_used = 1; - item.last_used_at = _now(); + item.last_used_at = _now()?; } else { info!("CachedSQLCipher: new group_id={id}"); @@ -294,15 +315,15 @@ impl KMSSqliteCache { // Add it to the SqliteCache // compute the mac - let mut mac = vec![0u8; 32]; + let mut mac = vec![0_u8; 32]; mac!(mac.as_mut_slice(), key, id.to_be_bytes().as_slice()); - let mut item = KMSSqliteCacheItem::new(pool, mac, freeable_cache_id); + let mut item = KMSSqliteCacheItem::new(pool, mac, freeable_cache_id)?; freeable_sqlites.uncache(freeable_cache_id)?; // Make it usable (to avoid direct free after alloc in case of cache overflow) item.in_used = 1; - item.last_used_at = _now(); + item.last_used_at = _now()?; sqlites.insert(id, item); }; @@ -484,6 +505,7 @@ impl FreeableSqliteCache { #[cfg(test)] mod tests { + #![allow(clippy::unwrap_used, clippy::expect_used)] use std::{str::FromStr, sync::atomic::Ordering, time::Duration}; use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; @@ -566,7 +588,7 @@ mod tests { assert_eq!(fsc.length, 0); assert_eq!(fsc.size, 4); - assert!(fsc.pop().is_err()); + fsc.pop().unwrap_err(); assert_eq!(fsc.push(5), 4); @@ -591,7 +613,7 @@ mod tests { assert!(fsc.uncache(4).is_err()); - assert!(fsc.uncache(2).is_ok()); + fsc.uncache(2).unwrap(); assert_eq!(fsc.head, 0); assert_eq!(fsc.tail, 3); @@ -604,7 +626,7 @@ mod tests { assert_eq!(fsc.entries[3].next, FSCNeighborEntry::Nil); assert_eq!(fsc.entries[3].prev, FSCNeighborEntry::Chained(1)); - assert!(fsc.uncache(0).is_ok()); + fsc.uncache(0).unwrap(); assert_eq!(fsc.head, 1); assert_eq!(fsc.tail, 3); @@ -615,7 +637,7 @@ mod tests { assert_eq!(fsc.entries[1].next, FSCNeighborEntry::Chained(3)); assert_eq!(fsc.entries[1].prev, FSCNeighborEntry::Nil); - assert!(fsc.uncache(3).is_ok()); + fsc.uncache(3).unwrap(); assert_eq!(fsc.head, 1); assert_eq!(fsc.tail, 1); @@ -626,7 +648,7 @@ mod tests { assert_eq!(fsc.entries[1].next, FSCNeighborEntry::Nil); assert_eq!(fsc.entries[1].prev, FSCNeighborEntry::Nil); - assert!(fsc.uncache(1).is_ok()); + fsc.uncache(1).unwrap(); assert_eq!(fsc.length, 0); assert_eq!(fsc.size, 4); @@ -634,7 +656,7 @@ mod tests { assert!(!fsc.entries[1].chained); assert!(fsc.uncache(1).is_err()); - assert!(fsc.pop().is_err()); + fsc.pop().unwrap_err(); assert_eq!(fsc.push(5), 4); assert_eq!(fsc.head, 4); @@ -658,7 +680,7 @@ mod tests { assert!(fsc.recache(4).is_err()); assert!(fsc.recache(3).is_err()); - assert!(fsc.uncache(2).is_ok()); + fsc.uncache(2).unwrap(); assert_eq!(fsc.head, 0); assert_eq!(fsc.tail, 3); @@ -671,7 +693,7 @@ mod tests { assert_eq!(fsc.entries[3].next, FSCNeighborEntry::Nil); assert_eq!(fsc.entries[3].prev, FSCNeighborEntry::Chained(1)); - assert!(fsc.recache(2).is_ok()); + fsc.recache(2).unwrap(); assert!(fsc.recache(2).is_err()); assert_eq!(fsc.head, 0); @@ -685,11 +707,11 @@ mod tests { assert_eq!(fsc.entries[3].next, FSCNeighborEntry::Chained(2)); assert_eq!(fsc.entries[3].prev, FSCNeighborEntry::Chained(1)); - assert!(fsc.uncache(0).is_ok()); - assert!(fsc.uncache(1).is_ok()); - assert!(fsc.uncache(2).is_ok()); - assert!(fsc.uncache(3).is_ok()); - assert!(fsc.recache(3).is_ok()); + fsc.uncache(0).unwrap(); + fsc.uncache(1).unwrap(); + fsc.uncache(2).unwrap(); + fsc.uncache(3).unwrap(); + fsc.recache(3).unwrap(); assert_eq!(fsc.head, 3); assert_eq!(fsc.tail, 3); @@ -714,66 +736,64 @@ mod tests { let sqlite2 = connect().await.expect("Can't create database"); let sqlite3 = connect().await.expect("Can't create database"); - assert!(cache.save(1, &password, sqlite).await.is_ok()); + cache.save(1, &password, sqlite).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 1); - assert!(cache.save(2, &password, sqlite2).await.is_ok()); + cache.save(2, &password, sqlite2).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 2); // flush should do nothing here - assert!(cache.opened(1)); - assert!(cache.opened(2)); + assert!(cache.opened(1).unwrap()); + assert!(cache.opened(2).unwrap()); - assert!(cache.exists(1)); + assert!(cache.exists(1).unwrap()); let sqlite2 = connect().await.expect("Can't create database"); - assert!(cache.save(2, &password, sqlite2).await.is_ok()); // double saved = ok + cache.save(2, &password, sqlite2).await.unwrap(); // double saved = ok - assert!(cache.release(2).is_ok()); + cache.release(2).unwrap(); assert!(cache.release(2).is_err()); // not twice - assert!(cache.exists(2)); - assert!(cache.opened(2)); // still opened + assert!(cache.exists(2).unwrap()); + assert!(cache.opened(2).unwrap()); // still opened - assert!(!cache.exists(3)); - assert!(cache.save(3, &password, sqlite3).await.is_ok()); + assert!(!cache.exists(3).unwrap()); + cache.save(3, &password, sqlite3).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 2); // flush should do nothing here - assert!(cache.opened(3)); // still opened - assert!(!cache.opened(2)); // not opened anymore - assert!(cache.exists(2)); + assert!(cache.opened(3).unwrap()); // still opened + assert!(!cache.opened(2).unwrap()); // not opened anymore + assert!(cache.exists(2).unwrap()); let sqlite2 = connect().await.expect("Can't create database"); - assert!(cache.save(2, &password, sqlite2).await.is_ok()); + cache.save(2, &password, sqlite2).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 3); // flush should do nothing here - assert!(cache.opened(2)); + assert!(cache.opened(2).unwrap()); - assert!(cache.get(4, &password).is_err()); - assert!( - cache - .get(1, &Secret::::new_random().unwrap()) - .is_err() - ); // bad &password - assert!(cache.get(1, &password).is_ok()); // 2 uses of sqlite1 + cache.get(4, &password).unwrap_err(); + cache + .get(1, &Secret::::new_random().unwrap()) + .unwrap_err(); // bad &password + cache.get(1, &password).unwrap(); // 2 uses of sqlite1 let sqlite4 = connect().await.expect("Can't create database"); - assert!(cache.save(4, &password, sqlite4).await.is_ok()); + cache.save(4, &password, sqlite4).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 4); // flush should do nothing here - assert!(cache.opened(1)); + assert!(cache.opened(1).unwrap()); - assert!(cache.release(1).is_ok()); // 1 uses of sqlite1 + cache.release(1).unwrap(); // 1 uses of sqlite1 let sqlite5 = connect().await.expect("Can't create database"); - assert!(cache.save(5, &password, sqlite5).await.is_ok()); + cache.save(5, &password, sqlite5).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 5); // flush should do nothing here - assert!(cache.opened(1)); + assert!(cache.opened(1).unwrap()); - assert!(cache.release(1).is_ok()); // 0 uses of sqlite1 - assert!(cache.opened(1)); + cache.release(1).unwrap(); // 0 uses of sqlite1 + assert!(cache.opened(1).unwrap()); let sqlite6 = connect().await.expect("Can't create database"); - assert!(cache.save(6, &password, sqlite6).await.is_ok()); + cache.save(6, &password, sqlite6).await.unwrap(); assert_eq!(cache.current_size.load(Ordering::Relaxed), 5); // flush should do something here - assert!(!cache.opened(1)); - assert!(cache.exists(1)); + assert!(!cache.opened(1).unwrap()); + assert!(cache.exists(1).unwrap()); - assert!(cache.get(1, &password).is_err()); // get after close + cache.get(1, &password).unwrap_err(); // get after close } async fn connect() -> std::result::Result, sqlx::Error> { diff --git a/crate/server/src/database/database_trait.rs b/crate/server/src/database/database_trait.rs index 430191f9..e6d5c121 100644 --- a/crate/server/src/database/database_trait.rs +++ b/crate/server/src/database/database_trait.rs @@ -18,6 +18,9 @@ pub(crate) trait Database { /// Return the filename of the database or `None` if not supported fn filename(&self, group_id: u128) -> Option; + /// Migrate the database to the latest version + async fn migrate(&self, params: Option<&ExtraDatabaseParams>) -> KResult<()>; + /// Insert the given Object in the database. /// /// A new UUID will be created if none is supplier. diff --git a/crate/server/src/database/locate_query.rs b/crate/server/src/database/locate_query.rs index 68663925..8e85e251 100644 --- a/crate/server/src/database/locate_query.rs +++ b/crate/server/src/database/locate_query.rs @@ -60,7 +60,7 @@ pub(crate) trait PlaceholderTrait { /// Get node specifier depending on `object_type` (ie: `PrivateKey` or `Certificate`) #[must_use] fn extract_text_from_object_type_path() -> String { - "object ->> 'object_type'".to_string() + "object ->> 'object_type'".to_owned() } } @@ -73,7 +73,7 @@ impl PlaceholderTrait for MySqlPlaceholder { const TYPE_INTEGER: &'static str = "SIGNED"; fn binder(_param_number: usize) -> String { - "?".to_string() + "?".to_owned() } fn additional_rq_from() -> Option { diff --git a/crate/server/src/database/migrate.rs b/crate/server/src/database/migrate.rs new file mode 100644 index 00000000..eef10cf0 --- /dev/null +++ b/crate/server/src/database/migrate.rs @@ -0,0 +1,24 @@ +use tracing::trace; +use version_compare::{compare, Cmp}; + +use crate::{error::KmsError, result::KResult}; + +pub(crate) fn do_migration( + last_kms_version_run: &str, + current_kms_version: &str, + state: &str, +) -> KResult { + if let Ok(cmp) = compare(last_kms_version_run, current_kms_version) { + match (cmp, state) { + (Cmp::Eq | Cmp::Ge | Cmp::Gt, "ready") => { + trace!("No migration needed"); + Ok(false) + } + (Cmp::Eq | Cmp::Ge | Cmp::Gt | Cmp::Ne | Cmp::Lt | Cmp::Le, _) => Ok(true), + } + } else { + Err(KmsError::DatabaseError( + "Error comparing versions".to_owned(), + )) + } +} diff --git a/crate/server/src/database/mod.rs b/crate/server/src/database/mod.rs index d89292ab..256b5a5c 100644 --- a/crate/server/src/database/mod.rs +++ b/crate/server/src/database/mod.rs @@ -1,3 +1,38 @@ +/// This module contains the database implementation for the KMS server. +/// It provides functionality for interacting with different types of databases, +/// such as `SQLite``MySQL``PostgreSQL`eSQL, and Redis. +/// +/// The module includes the following sub-modules: +/// - `cached_sqlcipher`: Contains the implementation for caching SQL queries using `SQLCipher`. +/// - `cached_sqlite_struct`: Contains the implementation for caching `SQLite` structures. +/// - `database_trait`: Contains the trait definition for a generic database. +/// - `mysql`: Contains the implementation for `MySQL` database. +/// - `object_with_metadata`: Contains the implementation for objects with metadata. +/// - `pgsql`: Contains the implementation for `PostgreSQL` database. +/// - `redis`: Contains the implementation for Redis database. +/// - `sqlite`: Contains the implementation for `SQLite` database. +/// - `locate_query`: Contains utility functions for locating queries. +/// - `migrate`: Contains functions for database migration. +/// - `retrieve_object_utils`: Contains utility functions for retrieving objects. +/// +/// The module also defines the following types and constants: +/// - `KMSServer`: A type alias for the KMS server. +/// - `DBObject`: A struct representing a database object. +/// - `KMS_VERSION_BEFORE_MIGRATION_SUPPORT`: A constant representing the KMS version before migration support. +/// - `PGSQL_FILE_QUERIES`: A constant representing the `PostgreSQL` file queries. +/// - `MYSQL_FILE_QUERIES`: A constant representing the `MySQL` file queries. +/// - `SQLITE_FILE_QUERIES`: A constant representing the `SQLite` file queries. +/// +/// The module also includes the following functions: +/// - `state_from_string`: Converts a string to a `StateEnumeration` value. +/// +/// Finally, the module includes a test module for unit testing. +/// +/// # Errors +/// +/// This module does not define any specific errors. However, it may return errors +/// from the underlying database operations or from the functions defined in the sub-modules. +/// The specific error types and conditions are documented in the respective functions. use cosmian_kmip::kmip::{ kmip_objects::{Object, ObjectType}, kmip_types::StateEnumeration, @@ -20,15 +55,14 @@ pub(crate) mod redis; pub(crate) mod sqlite; pub(crate) use database_trait::{AtomicOperation, Database}; mod locate_query; +mod migrate; mod retrieve_object_utils; pub(crate) use locate_query::{ query_from_attributes, MySqlPlaceholder, PgSqlPlaceholder, SqlitePlaceholder, }; pub(crate) use retrieve_object_utils::retrieve_object_for_operation; -#[cfg(test)] -mod tests; - +const KMS_VERSION_BEFORE_MIGRATION_SUPPORT: &str = "4.12.0"; const PGSQL_FILE_QUERIES: &str = include_str!("query.sql"); const MYSQL_FILE_QUERIES: &str = include_str!("query_mysql.sql"); const SQLITE_FILE_QUERIES: &str = include_str!("query.sql"); @@ -55,6 +89,11 @@ pub(crate) struct DBObject { pub(crate) object: Object, } +/// Converts a string to a `StateEnumeration` value. +/// +/// # Errors +/// +/// Returns an error if the input string does not match any valid `StateEnumeration` value. pub fn state_from_string(s: &str) -> KResult { match s { "PreActive" => Ok(StateEnumeration::PreActive), @@ -66,3 +105,6 @@ pub fn state_from_string(s: &str) -> KResult { x => kms_bail!("invalid state in db: {}", x), } } + +#[cfg(test)] +mod tests; diff --git a/crate/server/src/database/mysql.rs b/crate/server/src/database/mysql.rs index 418443be..db165516 100644 --- a/crate/server/src/database/mysql.rs +++ b/crate/server/src/database/mysql.rs @@ -5,6 +5,7 @@ use std::{ }; use async_trait::async_trait; +use clap::crate_version; use cosmian_kmip::kmip::{ kmip_objects::Object, kmip_operations::ErrorReason, @@ -25,11 +26,28 @@ use super::{ }; use crate::{ core::extra_database_params::ExtraDatabaseParams, - database::database_trait::AtomicOperation, + database::{ + database_trait::AtomicOperation, migrate::do_migration, + KMS_VERSION_BEFORE_MIGRATION_SUPPORT, + }, kms_bail, kms_error, result::{KResult, KResultHelper}, }; +#[macro_export] +macro_rules! get_mysql_query { + ($name:literal) => { + MYSQL_QUERIES + .get($name) + .ok_or_else(|| kms_error!("{} SQL query can't be found", $name))? + }; + ($name:expr) => { + MYSQL_QUERIES + .get($name) + .ok_or_else(|| kms_error!("{} SQL query can't be found", $name))? + }; +} + /// The `MySQL` connector is also compatible to connect a `MariaDB` /// see: / pub(crate) struct MySqlPool { @@ -47,35 +65,29 @@ impl MySqlPool { .connect_with(options) .await?; - sqlx::query( - MYSQL_QUERIES - .get("create-table-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_mysql_query!("create-table-context")) + .execute(&pool) + .await?; - sqlx::query( - MYSQL_QUERIES - .get("create-table-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_mysql_query!("create-table-objects")) + .execute(&pool) + .await?; - sqlx::query( - MYSQL_QUERIES - .get("create-table-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_mysql_query!("create-table-read_access")) + .execute(&pool) + .await?; + + sqlx::query(get_mysql_query!("create-table-tags")) + .execute(&pool) + .await?; if clear_database { clear_database_(&pool).await?; } - Ok(Self { pool }) + let mysql_pool = Self { pool }; + mysql_pool.migrate(None).await?; + Ok(mysql_pool) } } @@ -85,17 +97,59 @@ impl Database for MySqlPool { None } + async fn migrate(&self, _params: Option<&ExtraDatabaseParams>) -> KResult<()> { + trace!("Migrate database"); + // Get the context rows + match sqlx::query(get_mysql_query!("select-context")) + .fetch_optional(&self.pool) + .await? + { + None => { + trace!("No context row found, migrating from scratch"); + return migrate_( + &self.pool, + KMS_VERSION_BEFORE_MIGRATION_SUPPORT, + "insert-context", + ) + .await; + } + Some(context_row) => { + let last_kms_version_run = context_row.get::(0); + let state = context_row.get::(1); + trace!( + "Context row found, migrating from version {last_kms_version_run} (state: \ + {state})" + ); + let current_kms_version = crate_version!(); + debug!( + "[state={state}] Last KMS version run: {last_kms_version_run}, Current KMS \ + version: {current_kms_version}" + ); + + if do_migration(&last_kms_version_run, current_kms_version, &state)? { + return migrate_(&self.pool, current_kms_version, "update-context").await; + } + } + } + + Ok(()) + } + async fn create( &self, uid: Option, - user: &str, + owner: &str, object: &Object, attributes: &Attributes, tags: &HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; - let uid = match create_(uid, user, object, attributes, tags, &mut tx).await { + let uid = match create_(uid, owner, object, attributes, tags, &mut tx).await { Ok(uid) => uid, Err(e) => { tx.rollback().await.context("transaction failed")?; @@ -110,10 +164,10 @@ impl Database for MySqlPool { &self, uid_or_tags: &str, user: &str, - operation_type: ObjectOperationType, + query_access_grant: ObjectOperationType, _params: Option<&ExtraDatabaseParams>, ) -> KResult> { - retrieve_(uid_or_tags, user, operation_type, &self.pool).await + retrieve_(uid_or_tags, user, query_access_grant, &self.pool).await } async fn retrieve_tags( @@ -151,6 +205,9 @@ impl Database for MySqlPool { state: StateEnumeration, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = self.pool.begin().await?; match update_state_(uid, state, &mut tx).await { Ok(()) => { @@ -174,6 +231,10 @@ impl Database for MySqlPool { state: StateEnumeration, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match upsert_(uid, user, object, attributes, tags, state, &mut tx).await { Ok(()) => { @@ -193,6 +254,10 @@ impl Database for MySqlPool { user: &str, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match delete_(uid, user, &mut tx).await { Ok(()) => { @@ -225,30 +290,38 @@ impl Database for MySqlPool { async fn grant_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { - insert_access_(uid, userid, operation_types, &self.pool).await + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + + insert_access_(uid, user, operation_types, &self.pool).await } async fn remove_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { - remove_access_(uid, userid, operation_types, &self.pool).await + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + + remove_access_(uid, user, operation_types, &self.pool).await } async fn is_object_owned_by( &self, uid: &str, - userid: &str, + owner: &str, _params: Option<&ExtraDatabaseParams>, ) -> KResult { - is_object_owned_by_(uid, userid, &self.pool).await + is_object_owned_by_(uid, owner, &self.pool).await } async fn find( @@ -272,21 +345,25 @@ impl Database for MySqlPool { async fn list_user_access_rights_on_object( &self, uid: &str, - userid: &str, + user: &str, no_inherited_access: bool, _params: Option<&ExtraDatabaseParams>, ) -> KResult> { - list_user_access_rights_on_object_(uid, userid, no_inherited_access, &self.pool).await + list_user_access_rights_on_object_(uid, user, no_inherited_access, &self.pool).await } async fn atomic( &self, - owner: &str, + user: &str, operations: &[AtomicOperation], _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; - match atomic_(owner, operations, &mut tx).await { + match atomic_(user, operations, &mut tx).await { Ok(()) => { tx.commit().await?; Ok(()) @@ -321,30 +398,22 @@ pub(crate) async fn create_( // If the uid is not provided, generate a new one let uid = uid.unwrap_or_else(|| Uuid::new_v4().to_string()); - sqlx::query( - MYSQL_QUERIES - .get("insert-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid.clone()) - .bind(object_json) - .bind(attributes_json) - .bind(StateEnumeration::Active.to_string()) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_mysql_query!("insert-objects")) + .bind(uid.clone()) + .bind(object_json) + .bind(attributes_json) + .bind(StateEnumeration::Active.to_string()) + .bind(owner) + .execute(&mut **executor) + .await?; // Insert the tags for tag in tags { - sqlx::query( - MYSQL_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid.clone()) - .bind(tag) - .execute(&mut **executor) - .await?; + sqlx::query(get_mysql_query!("insert-tags")) + .bind(uid.clone()) + .bind(tag) + .execute(&mut **executor) + .await?; } trace!("Created in DB: {uid} / {owner}"); @@ -371,10 +440,7 @@ where let tags_params = tags.iter().map(|_| "?").collect::>().join(", "); // Build the raw SQL query - let raw_sql = MYSQL_QUERIES - .get("select-from-tags") - .context("SQL query can't be found")? - .replace("@TAGS", &tags_params); + let raw_sql = get_mysql_query!("select-from-tags").replace("@TAGS", &tags_params); // Bind the tags params let mut query = sqlx::query::(&raw_sql); @@ -382,21 +448,17 @@ where query = query.bind(tag); } // Bind the tags len and the user - query = query.bind(tags.len() as i16).bind(user); + query = query.bind(i16::try_from(tags.len())?).bind(user); // Execute the query query.fetch_all(executor).await? } else { - sqlx::query( - MYSQL_QUERIES - .get("select-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(user) - .bind(uid_or_tags) - .fetch_optional(executor) - .await? - .map_or(vec![], |row| vec![row]) + sqlx::query(get_mysql_query!("select-object")) + .bind(user) + .bind(uid_or_tags) + .fetch_optional(executor) + .await? + .map_or(vec![], |row| vec![row]) }; // process the rows and find the tags @@ -434,14 +496,10 @@ async fn retrieve_tags_<'e, E>(uid: &str, executor: E) -> KResult + Copy, { - let rows: Vec = sqlx::query( - MYSQL_QUERIES - .get("select-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .fetch_all(executor) - .await?; + let rows: Vec = sqlx::query(get_mysql_query!("select-tags")) + .bind(uid) + .fetch_all(executor) + .await?; let tags = rows.iter().map(|r| r.get(0)).collect::>(); @@ -466,39 +524,27 @@ pub(crate) async fn update_object_( .context("failed serializing the attributes to JSON") .reason(ErrorReason::Internal_Server_Error)?; - sqlx::query( - MYSQL_QUERIES - .get("update-object-with-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(object_json) - .bind(attributes_json) - .bind(uid) - .execute(&mut **executor) - .await?; - - // Insert the new tags if any - if let Some(tags) = tags { - // delete the existing tags - sqlx::query( - MYSQL_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + sqlx::query(get_mysql_query!("update-object-with-object")) + .bind(object_json) + .bind(attributes_json) .bind(uid) .execute(&mut **executor) .await?; - for tag in tags { - sqlx::query( - MYSQL_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + // Insert the new tags if any + if let Some(tags) = tags { + // delete the existing tags + sqlx::query(get_mysql_query!("delete-tags")) .bind(uid) - .bind(tag) .execute(&mut **executor) .await?; + + for tag in tags { + sqlx::query(get_mysql_query!("insert-tags")) + .bind(uid) + .bind(tag) + .execute(&mut **executor) + .await?; } } @@ -511,15 +557,11 @@ pub(crate) async fn update_state_( state: StateEnumeration, executor: &mut Transaction<'_, MySql>, ) -> KResult<()> { - sqlx::query( - MYSQL_QUERIES - .get("update-object-with-state") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(state.to_string()) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_mysql_query!("update-object-with-state")) + .bind(state.to_string()) + .bind(uid) + .execute(&mut **executor) + .await?; trace!("Updated in DB: {uid}"); Ok(()) } @@ -530,25 +572,17 @@ pub(crate) async fn delete_( executor: &mut Transaction<'_, MySql>, ) -> KResult<()> { // delete the object - sqlx::query( - MYSQL_QUERIES - .get("delete-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_mysql_query!("delete-object")) + .bind(uid) + .bind(owner) + .execute(&mut **executor) + .await?; // delete the tags - sqlx::query( - MYSQL_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_mysql_query!("delete-tags")) + .bind(uid) + .execute(&mut **executor) + .await?; trace!("Deleted in DB: {uid}"); Ok(()) @@ -574,43 +608,31 @@ pub(crate) async fn upsert_( .context("failed serializing the attributes to JSON") .reason(ErrorReason::Internal_Server_Error)?; - sqlx::query( - MYSQL_QUERIES - .get("upsert-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(object_json) - .bind(attributes_json) - .bind(state.to_string()) - .bind(owner) - .bind(owner) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_mysql_query!("upsert-object")) + .bind(uid) + .bind(object_json) + .bind(attributes_json) + .bind(state.to_string()) + .bind(owner) + .bind(owner) + .bind(owner) + .execute(&mut **executor) + .await?; // Insert the new tags if present if let Some(tags) = tags { // delete the existing tags - sqlx::query( - MYSQL_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; - // insert the new ones - for tag in tags { - sqlx::query( - MYSQL_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + sqlx::query(get_mysql_query!("delete-tags")) .bind(uid) - .bind(tag) .execute(&mut **executor) .await?; + // insert the new ones + for tag in tags { + sqlx::query(get_mysql_query!("insert-tags")) + .bind(uid) + .bind(tag) + .execute(&mut **executor) + .await?; } } @@ -627,14 +649,10 @@ where { debug!("Uid = {}", uid); - let list = sqlx::query( - MYSQL_QUERIES - .get("select-rows-read_access-with-object-id") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .fetch_all(executor) - .await?; + let list = sqlx::query(get_mysql_query!("select-rows-read_access-with-object-id")) + .bind(uid) + .fetch_all(executor) + .await?; let mut ids: HashMap> = HashMap::with_capacity(list.len()); for row in list { ids.insert( @@ -656,14 +674,10 @@ where E: Executor<'e, Database = MySql> + Copy, { debug!("Owner = {}", user); - let list = sqlx::query( - MYSQL_QUERIES - .get("select-objects-access-obtained") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(user) - .fetch_all(executor) - .await?; + let list = sqlx::query(get_mysql_query!("select-objects-access-obtained")) + .bind(user) + .fetch_all(executor) + .await?; let mut ids: HashMap)> = HashMap::with_capacity(list.len()); for row in list { @@ -704,15 +718,11 @@ async fn perms<'e, E>(uid: &str, userid: &str, executor: E) -> KResult + Copy, { - let row: Option = sqlx::query( - MYSQL_QUERIES - .get("select-user-accesses-for-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .fetch_optional(executor) - .await?; + let row: Option = sqlx::query(get_mysql_query!("select-user-accesses-for-object")) + .bind(uid) + .bind(userid) + .fetch_optional(executor) + .await?; row.map_or(Ok(HashSet::new()), |row| { let perms_raw = row.get::, _>(0); @@ -745,16 +755,12 @@ where .reason(ErrorReason::Internal_Server_Error)?; // Upsert the DB - sqlx::query( - MYSQL_QUERIES - .get("upsert-row-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .bind(json) - .execute(executor) - .await?; + sqlx::query(get_mysql_query!("upsert-row-read_access")) + .bind(uid) + .bind(userid) + .bind(json) + .execute(executor) + .await?; trace!("Insert read access right in DB: {uid} / {userid}"); Ok(()) } @@ -777,15 +783,11 @@ where // No remaining permissions, delete the row if perms.is_empty() { - sqlx::query( - MYSQL_QUERIES - .get("delete-rows-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .execute(executor) - .await?; + sqlx::query(get_mysql_query!("delete-rows-read_access")) + .bind(uid) + .bind(userid) + .execute(executor) + .await?; return Ok(()) } @@ -795,16 +797,12 @@ where .reason(ErrorReason::Internal_Server_Error)?; // Update the DB - sqlx::query( - MYSQL_QUERIES - .get("update-rows-read_access-with-permission") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(json) - .bind(uid) - .bind(userid) - .execute(executor) - .await?; + sqlx::query(get_mysql_query!("update-rows-read_access-with-permission")) + .bind(json) + .bind(uid) + .bind(userid) + .execute(executor) + .await?; Ok(()) } @@ -812,15 +810,11 @@ pub(crate) async fn is_object_owned_by_<'e, E>(uid: &str, owner: &str, executor: where E: Executor<'e, Database = MySql> + Copy, { - let row: Option = sqlx::query( - MYSQL_QUERIES - .get("has-row-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(owner) - .fetch_optional(executor) - .await?; + let row: Option = sqlx::query(get_mysql_query!("has-row-objects")) + .bind(uid) + .bind(owner) + .fetch_optional(executor) + .await?; Ok(row.is_some()) } @@ -876,30 +870,22 @@ async fn clear_database_<'e, E>(executor: E) -> KResult<()> where E: Executor<'e, Database = MySql> + Copy, { + // Erase `context` table + sqlx::query(get_mysql_query!("clean-table-context")) + .execute(executor) + .await?; // Erase `objects` table - sqlx::query( - MYSQL_QUERIES - .get("clean-table-objects") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_mysql_query!("clean-table-objects")) + .execute(executor) + .await?; // Erase `read_access` table - sqlx::query( - MYSQL_QUERIES - .get("clean-table-read_access") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_mysql_query!("clean-table-read_access")) + .execute(executor) + .await?; // Erase `tags` table - sqlx::query( - MYSQL_QUERIES - .get("clean-table-tags") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_mysql_query!("clean-table-tags")) + .execute(executor) + .await?; Ok(()) } @@ -943,3 +929,140 @@ pub(crate) async fn atomic_( } Ok(()) } + +pub(crate) async fn is_migration_in_progress_<'e, E>(executor: E) -> KResult +where + E: Executor<'e, Database = MySql> + Copy, +{ + match sqlx::query(get_mysql_query!("select-context")) + .fetch_optional(executor) + .await? + { + Some(context_row) => { + let state = context_row.get::(1); + Ok(state == "upgrading") + } + None => Ok(false), + } +} + +pub(crate) async fn migrate_( + executor: &Pool, + last_version_run: &str, + query_name: &str, +) -> KResult<()> { + trace!("Set status to upgrading and last version run: {last_version_run}"); + let upsert_context = get_mysql_query!(query_name); + trace!("{query_name}: {upsert_context}"); + match query_name { + "insert-context" => { + sqlx::query(upsert_context) + .bind(last_version_run) + .bind("upgrading") + .execute(executor) + .await + } + "update-context" => { + sqlx::query(upsert_context) + .bind(last_version_run) + .bind("upgrading") + .bind("upgrading") + .execute(executor) + .await + } + _ => kms_bail!("Unknown query name: {query_name}"), + }?; + + trace!("Migrate data from version {last_version_run}"); + + // Process migration for each KMS version + let current_kms_version = crate_version!(); + if last_version_run == KMS_VERSION_BEFORE_MIGRATION_SUPPORT { + migrate_from_4_12_0_to_4_13_0(executor).await?; + } else { + trace!("No migration needed between {last_version_run} and {current_kms_version}"); + } + + // Set the current running version + trace!("Set status to ready and last version run: {current_kms_version}"); + sqlx::query(get_mysql_query!("update-context")) + .bind(current_kms_version) + .bind("ready") + .bind("upgrading") + .execute(executor) + .await?; + + Ok(()) +} + +/// Before the version 4.13.0, the KMIP attributes were stored in the objects table (via the objects themselves). +/// The new column attributes allows to store the KMIP attributes in a dedicated column even for KMIP objects that do not have KMIP attributes (such as Certificates). +pub(crate) async fn migrate_from_4_12_0_to_4_13_0(executor: &Pool) -> KResult<()> { + trace!("Migrating from 4.12.0 to 4.13.0"); + + // Add the column attributes to the objects table + if (sqlx::query(get_mysql_query!("has-column-attributes")) + .execute(executor) + .await) + .is_ok() + { + trace!("Column attributes already exists, nothing to do"); + return Ok(()); + } + + trace!("Column attributes does not exist, adding it"); + sqlx::query(get_mysql_query!("add-column-attributes")) + .execute(executor) + .await?; + + // Select all objects and extract the KMIP attributes to be stored in the new column + let rows = sqlx::query("SELECT * FROM objects") + .fetch_all(executor) + .await?; + + let mut operations = Vec::with_capacity(rows.len()); + for row in rows { + let uid = row.get::(0); + let db_object: DBObject = serde_json::from_slice(&row.get::, _>(1)) + .context("migrate: failed deserializing the object") + .reason(ErrorReason::Internal_Server_Error)?; + let object = Object::post_fix(db_object.object_type, db_object.object); + trace!( + "migrate_from_4_12_0_to_4_13_0: object (type: {})={:?}", + object.object_type(), + uid + ); + let attributes = match object.clone().attributes() { + Ok(attrs) => attrs.clone(), + Err(_error) => { + // For example, Certificate object has no KMIP-attribute + Attributes::default() + } + }; + let tags = retrieve_tags_(&uid, executor).await?; + operations.push(AtomicOperation::UpdateObject(( + uid, + object, + attributes, + Some(tags), + ))); + } + + let mut tx = executor.begin().await?; + match atomic_( + "this user is not used to update objects", + &operations, + &mut tx, + ) + .await + { + Ok(()) => { + tx.commit().await?; + Ok(()) + } + Err(e) => { + tx.rollback().await.context("transaction failed")?; + Err(e) + } + } +} diff --git a/crate/server/src/database/object_with_metadata.rs b/crate/server/src/database/object_with_metadata.rs index fc003463..7a653505 100644 --- a/crate/server/src/database/object_with_metadata.rs +++ b/crate/server/src/database/object_with_metadata.rs @@ -62,7 +62,8 @@ impl TryFrom<&SqliteRow> for ObjectWithMetadata { .context("failed deserializing the object") .reason(ErrorReason::Internal_Server_Error)?; let object = Object::post_fix(db_object.object_type, db_object.object); - let attributes = serde_json::from_str(&row.get::(2))?; + let raw_attributes = row.get::(2); + let attributes = serde_json::from_value(raw_attributes)?; let owner = row.get::(3); let state = state_from_string(&row.get::(4))?; let raw_permissions = row.get::, _>(5); diff --git a/crate/server/src/database/pgsql.rs b/crate/server/src/database/pgsql.rs index 79c03d43..0f4d15af 100644 --- a/crate/server/src/database/pgsql.rs +++ b/crate/server/src/database/pgsql.rs @@ -5,6 +5,7 @@ use std::{ }; use async_trait::async_trait; +use clap::crate_version; use cosmian_kmip::kmip::{ kmip_objects::Object, kmip_operations::ErrorReason, @@ -22,15 +23,29 @@ use uuid::Uuid; use crate::{ core::extra_database_params::ExtraDatabaseParams, database::{ - database_trait::AtomicOperation, object_with_metadata::ObjectWithMetadata, - query_from_attributes, state_from_string, DBObject, Database, PgSqlPlaceholder, - PGSQL_QUERIES, + database_trait::AtomicOperation, migrate::do_migration, + object_with_metadata::ObjectWithMetadata, query_from_attributes, state_from_string, + DBObject, Database, PgSqlPlaceholder, KMS_VERSION_BEFORE_MIGRATION_SUPPORT, PGSQL_QUERIES, }, error::KmsError, kms_bail, kms_error, result::{KResult, KResultHelper}, }; +#[macro_export] +macro_rules! get_pgsql_query { + ($name:literal) => { + PGSQL_QUERIES + .get($name) + .ok_or_else(|| kms_error!("{} SQL query can't be found", $name))? + }; + ($name:expr) => { + PGSQL_QUERIES + .get($name) + .ok_or_else(|| kms_error!("{} SQL query can't be found", $name))? + }; +} + pub(crate) struct PgPool { pool: Pool, } @@ -48,35 +63,29 @@ impl PgPool { .connect_with(options) .await?; - sqlx::query( - PGSQL_QUERIES - .get("create-table-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_pgsql_query!("create-table-objects")) + .execute(&pool) + .await?; - sqlx::query( - PGSQL_QUERIES - .get("create-table-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_pgsql_query!("create-table-context")) + .execute(&pool) + .await?; - sqlx::query( - PGSQL_QUERIES - .get("create-table-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_pgsql_query!("create-table-read_access")) + .execute(&pool) + .await?; + + sqlx::query(get_pgsql_query!("create-table-tags")) + .execute(&pool) + .await?; if clear_database { clear_database_(&pool).await?; } - Ok(Self { pool }) + let pgsql_pool = Self { pool }; + pgsql_pool.migrate(None).await?; + Ok(pgsql_pool) } } @@ -86,17 +95,59 @@ impl Database for PgPool { None } + async fn migrate(&self, _params: Option<&ExtraDatabaseParams>) -> KResult<()> { + trace!("Migrate database"); + // Get the context rows + match sqlx::query(get_pgsql_query!("select-context")) + .fetch_optional(&self.pool) + .await? + { + None => { + trace!("No context row found, migrating from scratch"); + return migrate_( + &self.pool, + KMS_VERSION_BEFORE_MIGRATION_SUPPORT, + "insert-context", + ) + .await; + } + Some(context_row) => { + let last_kms_version_run = context_row.get::(0); + let state = context_row.get::(1); + trace!( + "Context row found, migrating from version {last_kms_version_run} (state: \ + {state})" + ); + let current_kms_version = crate_version!(); + debug!( + "[state={state}] Last KMS version run: {last_kms_version_run}, Current KMS \ + version: {current_kms_version}" + ); + + if do_migration(&last_kms_version_run, current_kms_version, &state)? { + return migrate_(&self.pool, current_kms_version, "update-context").await; + } + } + } + + Ok(()) + } + async fn create( &self, uid: Option, - user: &str, + owner: &str, object: &Object, attributes: &Attributes, tags: &HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; - let uid = match create_(uid, user, object, attributes, tags, &mut tx).await { + let uid = match create_(uid, owner, object, attributes, tags, &mut tx).await { Ok(uid) => uid, Err(e) => { tx.rollback().await.context("transaction failed")?; @@ -111,10 +162,10 @@ impl Database for PgPool { &self, uid_or_tags: &str, user: &str, - operation_type: ObjectOperationType, + query_access_grant: ObjectOperationType, _params: Option<&ExtraDatabaseParams>, ) -> KResult> { - retrieve_(uid_or_tags, user, operation_type, &self.pool).await + retrieve_(uid_or_tags, user, query_access_grant, &self.pool).await } async fn retrieve_tags( @@ -152,6 +203,9 @@ impl Database for PgPool { state: StateEnumeration, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = self.pool.begin().await?; match update_state_(uid, state, &mut tx).await { Ok(()) => { @@ -175,6 +229,10 @@ impl Database for PgPool { state: StateEnumeration, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match upsert_(uid, user, object, attributes, tags, state, &mut tx).await { Ok(()) => { @@ -194,6 +252,10 @@ impl Database for PgPool { user: &str, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match delete_(uid, user, &mut tx).await { Ok(()) => { @@ -226,30 +288,38 @@ impl Database for PgPool { async fn grant_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { - insert_access_(uid, userid, operation_types, &self.pool).await + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + + insert_access_(uid, user, operation_types, &self.pool).await } async fn remove_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { - remove_access_(uid, userid, operation_types, &self.pool).await + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + + remove_access_(uid, user, operation_types, &self.pool).await } async fn is_object_owned_by( &self, uid: &str, - userid: &str, + owner: &str, _params: Option<&ExtraDatabaseParams>, ) -> KResult { - is_object_owned_by_(uid, userid, &self.pool).await + is_object_owned_by_(uid, owner, &self.pool).await } async fn find( @@ -273,21 +343,25 @@ impl Database for PgPool { async fn list_user_access_rights_on_object( &self, uid: &str, - userid: &str, + user: &str, no_inherited_access: bool, _params: Option<&ExtraDatabaseParams>, ) -> KResult> { - list_user_access_rights_on_object_(uid, userid, no_inherited_access, &self.pool).await + list_user_access_rights_on_object_(uid, user, no_inherited_access, &self.pool).await } async fn atomic( &self, - owner: &str, + user: &str, operations: &[AtomicOperation], _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; - match atomic_(owner, operations, &mut tx).await { + match atomic_(user, operations, &mut tx).await { Ok(()) => { tx.commit().await?; Ok(()) @@ -322,30 +396,22 @@ pub(crate) async fn create_( // If the uid is not provided, generate a new one let uid = uid.unwrap_or_else(|| Uuid::new_v4().to_string()); - sqlx::query( - PGSQL_QUERIES - .get("insert-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid.clone()) - .bind(object_json) - .bind(attributes_json) - .bind(StateEnumeration::Active.to_string()) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_pgsql_query!("insert-objects")) + .bind(uid.clone()) + .bind(object_json) + .bind(attributes_json) + .bind(StateEnumeration::Active.to_string()) + .bind(owner) + .execute(&mut **executor) + .await?; // Insert the tags for tag in tags { - sqlx::query( - PGSQL_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid.clone()) - .bind(tag) - .execute(&mut **executor) - .await?; + sqlx::query(get_pgsql_query!("insert-tags")) + .bind(uid.clone()) + .bind(tag) + .execute(&mut **executor) + .await?; } trace!("Created in DB: {uid} / {owner}"); @@ -377,9 +443,7 @@ where .join(", "); // Build the raw SQL query - let raw_sql = PGSQL_QUERIES - .get("select-from-tags") - .context("SQL query can't be found")? + let raw_sql = get_pgsql_query!("select-from-tags") .replace("@TAGS", &tags_params) .replace("@LEN", &format!("${}", tags.len() + 1)) .replace("@USER", &format!("${}", tags.len() + 2)); @@ -390,21 +454,17 @@ where query = query.bind(tag); } // Bind the tags len and the user - query = query.bind(tags.len() as i16).bind(user); + query = query.bind(i16::try_from(tags.len())?).bind(user); // Execute the query query.fetch_all(executor).await? } else { - sqlx::query( - PGSQL_QUERIES - .get("select-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid_or_tags) - .bind(user) - .fetch_optional(executor) - .await? - .map_or(vec![], |row| vec![row]) + sqlx::query(get_pgsql_query!("select-object")) + .bind(uid_or_tags) + .bind(user) + .fetch_optional(executor) + .await? + .map_or(vec![], |row| vec![row]) }; // process the rows and find the tags @@ -442,14 +502,10 @@ async fn retrieve_tags_<'e, E>(uid: &str, executor: E) -> KResult + Copy, { - let rows: Vec = sqlx::query( - PGSQL_QUERIES - .get("select-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .fetch_all(executor) - .await?; + let rows: Vec = sqlx::query(get_pgsql_query!("select-tags")) + .bind(uid) + .fetch_all(executor) + .await?; let tags = rows.iter().map(|r| r.get(0)).collect::>(); @@ -474,39 +530,27 @@ pub(crate) async fn update_object_( .context("failed serializing the attributes to JSON") .reason(ErrorReason::Internal_Server_Error)?; - sqlx::query( - PGSQL_QUERIES - .get("update-object-with-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(object_json) - .bind(attributes_json) - .bind(uid) - .execute(&mut **executor) - .await?; - - // Update the tags - if let Some(tags) = tags { - // delete the existing tags - sqlx::query( - PGSQL_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + sqlx::query(get_pgsql_query!("update-object-with-object")) + .bind(object_json) + .bind(attributes_json) .bind(uid) .execute(&mut **executor) .await?; - for tag in tags { - sqlx::query( - PGSQL_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + // Insert the new tags if any + if let Some(tags) = tags { + // delete the existing tags + sqlx::query(get_pgsql_query!("delete-tags")) .bind(uid) - .bind(tag) .execute(&mut **executor) .await?; + + for tag in tags { + sqlx::query(get_pgsql_query!("insert-tags")) + .bind(uid) + .bind(tag) + .execute(&mut **executor) + .await?; } } @@ -519,15 +563,11 @@ pub(crate) async fn update_state_( state: StateEnumeration, executor: &mut Transaction<'_, Postgres>, ) -> KResult<()> { - sqlx::query( - PGSQL_QUERIES - .get("update-object-with-state") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(state.to_string()) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_pgsql_query!("update-object-with-state")) + .bind(state.to_string()) + .bind(uid) + .execute(&mut **executor) + .await?; trace!("Updated in DB: {uid}"); Ok(()) } @@ -538,25 +578,17 @@ pub(crate) async fn delete_( executor: &mut Transaction<'_, Postgres>, ) -> KResult<()> { // delete the object - sqlx::query( - PGSQL_QUERIES - .get("delete-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_pgsql_query!("delete-object")) + .bind(uid) + .bind(owner) + .execute(&mut **executor) + .await?; // delete the tags - sqlx::query( - PGSQL_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_pgsql_query!("delete-tags")) + .bind(uid) + .execute(&mut **executor) + .await?; trace!("Deleted in DB: {uid}"); Ok(()) @@ -582,41 +614,29 @@ pub(crate) async fn upsert_( .context("failed serializing the attributes to JSON") .reason(ErrorReason::Internal_Server_Error)?; - sqlx::query( - PGSQL_QUERIES - .get("upsert-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(object_json) - .bind(attributes_json) - .bind(state.to_string()) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_pgsql_query!("upsert-object")) + .bind(uid) + .bind(object_json) + .bind(attributes_json) + .bind(state.to_string()) + .bind(owner) + .execute(&mut **executor) + .await?; // Insert the new tags if present if let Some(tags) = tags { // delete the existing tags - sqlx::query( - PGSQL_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; - // insert the new ones - for tag in tags { - sqlx::query( - PGSQL_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + sqlx::query(get_pgsql_query!("delete-tags")) .bind(uid) - .bind(tag) .execute(&mut **executor) .await?; + // insert the new ones + for tag in tags { + sqlx::query(get_pgsql_query!("insert-tags")) + .bind(uid) + .bind(tag) + .execute(&mut **executor) + .await?; } } @@ -633,14 +653,10 @@ where { debug!("Uid = {}", uid); - let list = sqlx::query( - PGSQL_QUERIES - .get("select-rows-read_access-with-object-id") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .fetch_all(executor) - .await?; + let list = sqlx::query(get_pgsql_query!("select-rows-read_access-with-object-id")) + .bind(uid) + .fetch_all(executor) + .await?; let mut ids: HashMap> = HashMap::with_capacity(list.len()); for row in list { ids.insert( @@ -662,14 +678,10 @@ where E: Executor<'e, Database = Postgres> + Copy, { debug!("Owner = {}", user); - let list = sqlx::query( - PGSQL_QUERIES - .get("select-objects-access-obtained") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(user) - .fetch_all(executor) - .await?; + let list = sqlx::query(get_pgsql_query!("select-objects-access-obtained")) + .bind(user) + .fetch_all(executor) + .await?; let mut ids: HashMap)> = HashMap::with_capacity(list.len()); for row in list { @@ -710,15 +722,11 @@ async fn perms<'e, E>(uid: &str, userid: &str, executor: E) -> KResult + Copy, { - let row: Option = sqlx::query( - PGSQL_QUERIES - .get("select-user-accesses-for-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .fetch_optional(executor) - .await?; + let row: Option = sqlx::query(get_pgsql_query!("select-user-accesses-for-object")) + .bind(uid) + .bind(userid) + .fetch_optional(executor) + .await?; row.map_or(Ok(HashSet::new()), |row| { let perms_value = row @@ -753,16 +761,12 @@ where .reason(ErrorReason::Internal_Server_Error)?; // Upsert the DB - sqlx::query( - PGSQL_QUERIES - .get("upsert-row-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .bind(json) - .execute(executor) - .await?; + sqlx::query(get_pgsql_query!("upsert-row-read_access")) + .bind(uid) + .bind(userid) + .bind(json) + .execute(executor) + .await?; trace!("Insert read access right in DB: {uid} / {userid}"); Ok(()) } @@ -785,15 +789,11 @@ where // No remaining permissions, delete the row if perms.is_empty() { - sqlx::query( - PGSQL_QUERIES - .get("delete-rows-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .execute(executor) - .await?; + sqlx::query(get_pgsql_query!("delete-rows-read_access")) + .bind(uid) + .bind(userid) + .execute(executor) + .await?; return Ok(()) } @@ -803,16 +803,12 @@ where .reason(ErrorReason::Internal_Server_Error)?; // Update the DB - sqlx::query( - PGSQL_QUERIES - .get("update-rows-read_access-with-permission") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .bind(json) - .execute(executor) - .await?; + sqlx::query(get_pgsql_query!("update-rows-read_access-with-permission")) + .bind(uid) + .bind(userid) + .bind(json) + .execute(executor) + .await?; trace!("Deleted in DB: {uid} / {userid}"); Ok(()) } @@ -821,15 +817,11 @@ pub(crate) async fn is_object_owned_by_<'e, E>(uid: &str, owner: &str, executor: where E: Executor<'e, Database = Postgres> + Copy, { - let row: Option = sqlx::query( - PGSQL_QUERIES - .get("has-row-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(owner) - .fetch_optional(executor) - .await?; + let row: Option = sqlx::query(get_pgsql_query!("has-row-objects")) + .bind(uid) + .bind(owner) + .fetch_optional(executor) + .await?; Ok(row.is_some()) } @@ -863,7 +855,7 @@ fn to_qualified_uids( let mut uids = Vec::with_capacity(rows.len()); for row in rows { let attrs: Attributes = match row.try_get::(2) { - Err(_) => return Err(KmsError::DatabaseError("no attributes found".to_string())), + Err(_) => return Err(KmsError::DatabaseError("no attributes found".to_owned())), Ok(v) => serde_json::from_value(v) .context("failed deserializing the attributes") .map_err(|e| KmsError::DatabaseError(e.to_string()))?, @@ -883,30 +875,22 @@ async fn clear_database_<'e, E>(executor: E) -> KResult<()> where E: Executor<'e, Database = Postgres> + Copy, { + // Erase `context` table + sqlx::query(get_pgsql_query!("clean-table-context")) + .execute(executor) + .await?; // Erase `objects` table - sqlx::query( - PGSQL_QUERIES - .get("clean-table-objects") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_pgsql_query!("clean-table-objects")) + .execute(executor) + .await?; // Erase `read_access` table - sqlx::query( - PGSQL_QUERIES - .get("clean-table-read_access") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_pgsql_query!("clean-table-read_access")) + .execute(executor) + .await?; // Erase `tags` table - sqlx::query( - PGSQL_QUERIES - .get("clean-table-tags") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_pgsql_query!("clean-table-tags")) + .execute(executor) + .await?; Ok(()) } @@ -950,3 +934,140 @@ pub(crate) async fn atomic_( } Ok(()) } + +pub(crate) async fn is_migration_in_progress_<'e, E>(executor: E) -> KResult +where + E: Executor<'e, Database = Postgres> + Copy, +{ + match sqlx::query(get_pgsql_query!("select-context")) + .fetch_optional(executor) + .await? + { + Some(context_row) => { + let state = context_row.get::(1); + Ok(state == "upgrading") + } + None => Ok(false), + } +} + +pub(crate) async fn migrate_( + executor: &Pool, + last_version_run: &str, + query_name: &str, +) -> KResult<()> { + trace!("Set status to upgrading and last version run: {last_version_run}"); + let upsert_context = get_pgsql_query!(query_name); + trace!("{query_name}: {upsert_context}"); + match query_name { + "insert-context" => { + sqlx::query(upsert_context) + .bind(last_version_run) + .bind("upgrading") + .execute(executor) + .await + } + "update-context" => { + sqlx::query(upsert_context) + .bind(last_version_run) + .bind("upgrading") + .bind("upgrading") + .execute(executor) + .await + } + _ => kms_bail!("Unknown query name: {query_name}"), + }?; + + trace!("Migrate data from version {last_version_run}"); + + // Process migration for each KMS version + let current_kms_version = crate_version!(); + if last_version_run == KMS_VERSION_BEFORE_MIGRATION_SUPPORT { + migrate_from_4_12_0_to_4_13_0(executor).await?; + } else { + trace!("No migration needed between {last_version_run} and {current_kms_version}"); + } + + // Set the current running version + trace!("Set status to ready and last version run: {current_kms_version}"); + sqlx::query(get_pgsql_query!("update-context")) + .bind(current_kms_version) + .bind("ready") + .bind("upgrading") + .execute(executor) + .await?; + + Ok(()) +} + +/// Before the version 4.13.0, the KMIP attributes were stored in the objects table (via the objects themselves). +/// The new column attributes allows to store the KMIP attributes in a dedicated column even for KMIP objects that do not have KMIP attributes (such as Certificates). +pub(crate) async fn migrate_from_4_12_0_to_4_13_0(executor: &Pool) -> KResult<()> { + trace!("Migrating from 4.12.0 to 4.13.0"); + + // Add the column attributes to the objects table + if (sqlx::query(get_pgsql_query!("has-column-attributes")) + .execute(executor) + .await) + .is_ok() + { + trace!("Column attributes already exists, nothing to do"); + return Ok(()); + } + + trace!("Column attributes does not exist, adding it"); + sqlx::query(get_pgsql_query!("add-column-attributes")) + .execute(executor) + .await?; + + // Select all objects and extract the KMIP attributes to be stored in the new column + let rows = sqlx::query("SELECT * FROM objects") + .fetch_all(executor) + .await?; + + let mut operations = Vec::with_capacity(rows.len()); + for row in rows { + let uid = row.get::(0); + let db_object: DBObject = serde_json::from_slice(&row.get::, _>(1)) + .context("migrate: failed deserializing the object") + .reason(ErrorReason::Internal_Server_Error)?; + let object = Object::post_fix(db_object.object_type, db_object.object); + trace!( + "migrate_from_4_12_0_to_4_13_0: object (type: {})={:?}", + object.object_type(), + uid + ); + let attributes = match object.clone().attributes() { + Ok(attrs) => attrs.clone(), + Err(_error) => { + // For example, Certificate object has no KMIP-attribute + Attributes::default() + } + }; + let tags = retrieve_tags_(&uid, executor).await?; + operations.push(AtomicOperation::UpdateObject(( + uid, + object, + attributes, + Some(tags), + ))); + } + + let mut tx = executor.begin().await?; + match atomic_( + "this user is not used to update objects", + &operations, + &mut tx, + ) + .await + { + Ok(()) => { + tx.commit().await?; + Ok(()) + } + Err(e) => { + tx.rollback().await.context("transaction failed")?; + Err(e) + } + } +} diff --git a/crate/server/src/database/query.sql b/crate/server/src/database/query.sql index 7257f01a..6745c78d 100644 --- a/crate/server/src/database/query.sql +++ b/crate/server/src/database/query.sql @@ -1,3 +1,9 @@ +-- name: create-table-context +CREATE TABLE IF NOT EXISTS context ( + version VARCHAR(40) PRIMARY KEY, + state VARCHAR(40) +); + -- name: create-table-objects CREATE TABLE IF NOT EXISTS objects ( id VARCHAR(40) PRIMARY KEY, @@ -6,6 +12,10 @@ CREATE TABLE IF NOT EXISTS objects ( state VARCHAR(32), owner VARCHAR(255) ); +-- name: add-column-attributes +ALTER TABLE objects ADD COLUMN attributes json; +-- name: has-column-attributes +SELECT attributes from objects; -- name: create-table-read_access CREATE TABLE IF NOT EXISTS read_access ( @@ -22,6 +32,9 @@ CREATE TABLE IF NOT EXISTS tags ( UNIQUE (id, tag) ); +-- name: clean-table-context +DELETE FROM context; + -- name: clean-table-objects DELETE FROM objects; @@ -31,6 +44,18 @@ DELETE FROM read_access; -- name: clean-table-tags DELETE FROM tags; +-- name: select-context +SELECT * FROM context ORDER BY version ASC LIMIT 1; + +-- name: insert-context +INSERT INTO context (version, state) VALUES ($1, $2); + +-- name: update-context +UPDATE context SET version=$1, state=$2 WHERE state=$3; + +-- name: delete-version +DELETE FROM context WHERE version=$1; + -- name: insert-objects INSERT INTO objects (id, object, attributes, state, owner) VALUES ($1, $2, $3, $4, $5); diff --git a/crate/server/src/database/query_mysql.sql b/crate/server/src/database/query_mysql.sql index b253d24c..59df1065 100644 --- a/crate/server/src/database/query_mysql.sql +++ b/crate/server/src/database/query_mysql.sql @@ -1,3 +1,9 @@ +-- name: create-table-context +CREATE TABLE IF NOT EXISTS context ( + version VARCHAR(40) PRIMARY KEY, + state VARCHAR(40) +); + -- name: create-table-objects CREATE TABLE IF NOT EXISTS objects ( id VARCHAR(40) PRIMARY KEY, @@ -6,6 +12,10 @@ CREATE TABLE IF NOT EXISTS objects ( state VARCHAR(32), owner VARCHAR(255) ); +-- name: add-column-attributes +ALTER TABLE objects ADD COLUMN attributes json; +-- name: has-column-attributes +SHOW COLUMNS FROM objects LIKE 'attributes'; -- name: create-table-read_access CREATE TABLE IF NOT EXISTS read_access ( @@ -22,6 +32,9 @@ CREATE TABLE IF NOT EXISTS tags ( UNIQUE (id, tag) ); +-- name: clean-table-context +DELETE FROM context; + -- name: clean-table-objects DELETE FROM objects; @@ -31,6 +44,18 @@ DELETE FROM read_access; -- name: clean-table-tags DELETE FROM tags; +-- name: select-context +SELECT * FROM context LIMIT 1; + +-- name: insert-context +INSERT INTO context (version, state) VALUES (?, ?); + +-- name: update-context +UPDATE context SET version=?, state=? WHERE state=?; + +-- name: delete-version +DELETE FROM context WHERE version=?; + -- name: insert-objects INSERT INTO objects (id, object, attributes, state, owner) VALUES (?, ?, ?, ?, ?); diff --git a/crate/server/src/database/redis/objects_db.rs b/crate/server/src/database/redis/objects_db.rs index 11f7b52a..fc242f3b 100644 --- a/crate/server/src/database/redis/objects_db.rs +++ b/crate/server/src/database/redis/objects_db.rs @@ -119,7 +119,9 @@ impl ObjectsDB { fn encrypt_object(&self, uid: &str, redis_db_object: &RedisDbObject) -> KResult> { let nonce = { - let mut rng = self.rng.lock().expect("failed acquiring a lock on the RNG"); + let mut rng = self.rng.lock().map_err(|e| { + KmsError::DatabaseError(format!("failed acquiring a lock on the RNG. Error: {e:?}")) + })?; Nonce::new(&mut *rng) }; let ct = self.dem.encrypt( @@ -136,7 +138,7 @@ impl ObjectsDB { fn decrypt_object(&self, uid: &str, ciphertext: &[u8]) -> KResult { if ciphertext.len() <= Aes256Gcm::NONCE_LENGTH { return Err(KmsError::CryptographicError( - "invalid ciphertext".to_string(), + "invalid ciphertext".to_owned(), )) } let nonce_bytes = &ciphertext[..Aes256Gcm::NONCE_LENGTH]; diff --git a/crate/server/src/database/redis/permissions.rs b/crate/server/src/database/redis/permissions.rs index 3f0a730f..7ea7c46a 100644 --- a/crate/server/src/database/redis/permissions.rs +++ b/crate/server/src/database/redis/permissions.rs @@ -29,8 +29,8 @@ pub(crate) struct Triple { impl Triple { pub(crate) fn new(obj_uid: &str, user_id: &str, permission: ObjectOperationType) -> Self { Self { - obj_uid: obj_uid.to_string(), - user_id: user_id.to_string(), + obj_uid: obj_uid.to_owned(), + user_id: user_id.to_owned(), permission, } } @@ -82,8 +82,8 @@ impl TryFrom<&Location> for Triple { KmsError::ConversionError(format!("invalid permissions triple: {parts:?}")) })?; Ok(Self { - obj_uid: uid.to_string(), - user_id: user_id.to_string(), + obj_uid: uid.to_owned(), + user_id: user_id.to_owned(), permission: serde_json::from_str(permission)?, }) } diff --git a/crate/server/src/database/redis/redis_with_findex.rs b/crate/server/src/database/redis/redis_with_findex.rs index e407f8b5..3f338fdb 100644 --- a/crate/server/src/database/redis/redis_with_findex.rs +++ b/crate/server/src/database/redis/redis_with_findex.rs @@ -128,7 +128,7 @@ impl RedisWithFindex { }; // the database object to index and store let db_object = - RedisDbObject::new(object.clone(), owner.to_string(), state, Some(tags.clone())); + RedisDbObject::new(object.clone(), owner.to_owned(), state, Some(tags.clone())); // extract the keywords index_additions.insert( IndexedValue::Location(Location::from(uid.as_bytes())), @@ -179,7 +179,7 @@ impl RedisWithFindex { .objects_db .object_get(uid) .await? - .ok_or_else(|| KmsError::ItemNotFound(uid.to_string()))?; + .ok_or_else(|| KmsError::ItemNotFound(uid.to_owned()))?; db_object.object = object.clone(); if tags.is_some() { db_object.tags = tags.cloned(); @@ -215,7 +215,7 @@ impl RedisWithFindex { .objects_db .object_get(uid) .await? - .ok_or_else(|| KmsError::ItemNotFound(uid.to_string()))?; + .ok_or_else(|| KmsError::ItemNotFound(uid.to_owned()))?; db_object.state = state; // The state is not indexed, so no updates there Ok(db_object) @@ -228,6 +228,10 @@ impl Database for RedisWithFindex { None } + async fn migrate(&self, _params: Option<&ExtraDatabaseParams>) -> KResult<()> { + unimplemented!("Redis-with-Findex does not support migrate operation"); + } + /// Insert the given Object in the database. /// /// A new UUID will be created if none is supplier. @@ -283,12 +287,13 @@ impl Database for RedisWithFindex { locations .into_iter() .map(|location| { - String::from_utf8(location.to_vec()).map_err(|_| kms_error!("Invalid uid")) + String::from_utf8(location.to_vec()) + .map_err(|e| kms_error!(format!("Invalid uid. Error: {e:?}"))) }) .collect::>>()? } else { // it is an UID - HashSet::from([uid_or_tags.to_string()]) + HashSet::from([uid_or_tags.to_owned()]) }; // now retrieve the object @@ -381,7 +386,7 @@ impl Database for RedisWithFindex { async fn upsert( &self, uid: &str, - owner: &str, + user: &str, object: &Object, _attributes: &Attributes, tags: Option<&HashSet>, @@ -389,7 +394,7 @@ impl Database for RedisWithFindex { params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { let db_object = self - .prepare_object_for_upsert(uid, owner, object, tags, state, params) + .prepare_object_for_upsert(uid, user, object, tags, state, params) .await?; // upsert the object @@ -501,7 +506,7 @@ impl Database for RedisWithFindex { .objects_db .object_get(uid) .await? - .ok_or_else(|| KmsError::ItemNotFound(uid.to_string()))?; + .ok_or_else(|| KmsError::ItemNotFound(uid.to_owned()))?; Ok(object.owner == owner) } @@ -549,7 +554,8 @@ impl Database for RedisWithFindex { let uids = locations .into_iter() .map(|location| { - String::from_utf8(location.to_vec()).map_err(|_| kms_error!("Invalid uid")) + String::from_utf8(location.to_vec()) + .map_err(|e| kms_error!(format!("Invalid uid. Error: {e:?}"))) }) .collect::>>()?; trace!("find: uids before permissions: {:?}", uids); @@ -614,7 +620,7 @@ impl Database for RedisWithFindex { async fn atomic( &self, - owner: &str, + user: &str, operations: &[AtomicOperation], params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { @@ -624,20 +630,13 @@ impl Database for RedisWithFindex { AtomicOperation::Upsert((uid, object, _attributes, tags, state)) => { //TODO: this operation contains a non atomic retrieve_tags. It will be hard to make this whole method atomic let db_object = self - .prepare_object_for_upsert( - uid, - owner, - object, - tags.as_ref(), - *state, - params, - ) + .prepare_object_for_upsert(uid, user, object, tags.as_ref(), *state, params) .await?; redis_operations.push(RedisOperation::Upsert(uid.clone(), db_object)); } AtomicOperation::Create((uid, object, _attributes, tags)) => { let (uid, db_object) = self - .prepare_object_for_create(Some(uid.clone()), owner, object, tags) + .prepare_object_for_create(Some(uid.clone()), user, object, tags) .await?; redis_operations.push(RedisOperation::Create(uid, db_object)); } diff --git a/crate/server/src/database/sqlite.rs b/crate/server/src/database/sqlite.rs index cd0b603e..05ddf855 100644 --- a/crate/server/src/database/sqlite.rs +++ b/crate/server/src/database/sqlite.rs @@ -5,6 +5,7 @@ use std::{ }; use async_trait::async_trait; +use clap::crate_version; use cosmian_kmip::kmip::{ kmip_objects::Object, kmip_operations::ErrorReason, @@ -23,13 +24,28 @@ use super::object_with_metadata::ObjectWithMetadata; use crate::{ core::extra_database_params::ExtraDatabaseParams, database::{ - database_trait::AtomicOperation, query_from_attributes, state_from_string, DBObject, - Database, SqlitePlaceholder, SQLITE_QUERIES, + database_trait::AtomicOperation, migrate::do_migration, query_from_attributes, + state_from_string, DBObject, Database, SqlitePlaceholder, + KMS_VERSION_BEFORE_MIGRATION_SUPPORT, SQLITE_QUERIES, }, kms_bail, kms_error, result::{KResult, KResultHelper}, }; +#[macro_export] +macro_rules! get_sqlite_query { + ($name:literal) => { + SQLITE_QUERIES + .get($name) + .ok_or_else(|| kms_error!("{} SQL query can't be found", $name))? + }; + ($name:expr) => { + SQLITE_QUERIES + .get($name) + .ok_or_else(|| kms_error!("{} SQL query can't be found", $name))? + }; +} + pub(crate) struct SqlitePool { pool: Pool, } @@ -51,35 +67,29 @@ impl SqlitePool { .connect_with(options) .await?; - sqlx::query( - SQLITE_QUERIES - .get("create-table-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_sqlite_query!("create-table-context")) + .execute(&pool) + .await?; - sqlx::query( - SQLITE_QUERIES - .get("create-table-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_sqlite_query!("create-table-objects")) + .execute(&pool) + .await?; - sqlx::query( - SQLITE_QUERIES - .get("create-table-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .execute(&pool) - .await?; + sqlx::query(get_sqlite_query!("create-table-read_access")) + .execute(&pool) + .await?; + + sqlx::query(get_sqlite_query!("create-table-tags")) + .execute(&pool) + .await?; if clear_database { clear_database_(&pool).await?; } - Ok(Self { pool }) + let sqlite_pool = Self { pool }; + sqlite_pool.migrate(None).await?; + Ok(sqlite_pool) } } @@ -89,17 +99,58 @@ impl Database for SqlitePool { None } + async fn migrate(&self, _params: Option<&ExtraDatabaseParams>) -> KResult<()> { + trace!("Migrate database"); + // Get the context rows + match sqlx::query(get_sqlite_query!("select-context")) + .fetch_optional(&self.pool) + .await? + { + None => { + trace!("No context row found, migrating from scratch"); + return migrate_( + &self.pool, + KMS_VERSION_BEFORE_MIGRATION_SUPPORT, + "insert-context", + ) + .await; + } + Some(context_row) => { + let last_kms_version_run = context_row.get::(0); + let state = context_row.get::(1); + trace!( + "Context row found, migrating from version {last_kms_version_run} (state: \ + {state})" + ); + let current_kms_version = crate_version!(); + debug!( + "[state={state}] Last KMS version run: {last_kms_version_run}, Current KMS \ + version: {current_kms_version}" + ); + + if do_migration(&last_kms_version_run, current_kms_version, &state)? { + return migrate_(&self.pool, current_kms_version, "update-context").await; + } + } + } + + Ok(()) + } + async fn create( &self, uid: Option, - user: &str, + owner: &str, object: &Object, attributes: &Attributes, tags: &HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } let mut tx = self.pool.begin().await?; - let uid = match create_(uid, user, object, attributes, tags, &mut tx).await { + let uid = match create_(uid, owner, object, attributes, tags, &mut tx).await { Ok(uid) => uid, Err(e) => { tx.rollback().await.context("transaction failed")?; @@ -114,10 +165,10 @@ impl Database for SqlitePool { &self, uid_or_tags: &str, user: &str, - operation_type: ObjectOperationType, + query_access_grant: ObjectOperationType, _params: Option<&ExtraDatabaseParams>, ) -> KResult> { - retrieve_(uid_or_tags, user, operation_type, &self.pool).await + retrieve_(uid_or_tags, user, query_access_grant, &self.pool).await } async fn retrieve_tags( @@ -136,6 +187,10 @@ impl Database for SqlitePool { tags: Option<&HashSet>, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match update_object_(uid, object, attributes, tags, &mut tx).await { Ok(()) => { @@ -155,6 +210,10 @@ impl Database for SqlitePool { state: StateEnumeration, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match update_state_(uid, state, &mut tx).await { Ok(()) => { @@ -178,6 +237,10 @@ impl Database for SqlitePool { state: StateEnumeration, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match upsert_(uid, user, object, attributes, tags, state, &mut tx).await { Ok(()) => { @@ -197,6 +260,10 @@ impl Database for SqlitePool { user: &str, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; match delete_(uid, user, &mut tx).await { Ok(()) => { @@ -229,30 +296,38 @@ impl Database for SqlitePool { async fn grant_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { - insert_access_(uid, userid, operation_types, &self.pool).await + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + + insert_access_(uid, user, operation_types, &self.pool).await } async fn remove_access( &self, uid: &str, - userid: &str, + user: &str, operation_types: HashSet, _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { - remove_access_(uid, userid, operation_types, &self.pool).await + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + + remove_access_(uid, user, operation_types, &self.pool).await } async fn is_object_owned_by( &self, uid: &str, - userid: &str, + owner: &str, _params: Option<&ExtraDatabaseParams>, ) -> KResult { - is_object_owned_by_(uid, userid, &self.pool).await + is_object_owned_by_(uid, owner, &self.pool).await } async fn find( @@ -276,21 +351,25 @@ impl Database for SqlitePool { async fn list_user_access_rights_on_object( &self, uid: &str, - userid: &str, + user: &str, no_inherited_access: bool, _params: Option<&ExtraDatabaseParams>, ) -> KResult> { - list_user_access_rights_on_object_(uid, userid, no_inherited_access, &self.pool).await + list_user_access_rights_on_object_(uid, user, no_inherited_access, &self.pool).await } async fn atomic( &self, - owner: &str, + user: &str, operations: &[AtomicOperation], _params: Option<&ExtraDatabaseParams>, ) -> KResult<()> { + if is_migration_in_progress_(&self.pool).await? { + kms_bail!("Migration in progress. Please retry later"); + } + let mut tx = self.pool.begin().await?; - match atomic_(owner, operations, &mut tx).await { + match atomic_(user, operations, &mut tx).await { Ok(()) => { tx.commit().await?; Ok(()) @@ -325,30 +404,22 @@ pub(crate) async fn create_( // If the uid is not provided, generate a new one let uid = uid.unwrap_or_else(|| Uuid::new_v4().to_string()); - sqlx::query( - SQLITE_QUERIES - .get("insert-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid.clone()) - .bind(object_json) - .bind(attributes_json) - .bind(StateEnumeration::Active.to_string()) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("insert-objects")) + .bind(uid.clone()) + .bind(object_json) + .bind(attributes_json) + .bind(StateEnumeration::Active.to_string()) + .bind(owner) + .execute(&mut **executor) + .await?; // Insert the tags for tag in tags { - sqlx::query( - SQLITE_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid.clone()) - .bind(tag) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("insert-tags")) + .bind(uid.clone()) + .bind(tag) + .execute(&mut **executor) + .await?; } trace!("Created in DB: {uid} / {owner}"); @@ -380,9 +451,7 @@ where .join(", "); // Build the raw SQL query - let raw_sql = SQLITE_QUERIES - .get("select-from-tags") - .context("SQL query can't be found")? + let raw_sql = get_sqlite_query!("select-from-tags") .replace("@TAGS", &tags_params) .replace("@LEN", &format!("${}", tags.len() + 1)) .replace("@USER", &format!("${}", tags.len() + 2)); @@ -395,21 +464,17 @@ where query = query.bind(tag); } // Bind the tags len and the user - query = query.bind(tags.len() as i16).bind(user); + query = query.bind(i16::try_from(tags.len())?).bind(user); // Execute the query query.fetch_all(executor).await? } else { - sqlx::query( - SQLITE_QUERIES - .get("select-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid_or_tags) - .bind(user) - .fetch_optional(executor) - .await? - .map_or(vec![], |row| vec![row]) + sqlx::query(get_sqlite_query!("select-object")) + .bind(uid_or_tags) + .bind(user) + .fetch_optional(executor) + .await? + .map_or(vec![], |row| vec![row]) }; // process the rows and find the tags @@ -448,14 +513,10 @@ pub(crate) async fn retrieve_tags_<'e, E>(uid: &str, executor: E) -> KResult + Copy, { - let rows: Vec = sqlx::query( - SQLITE_QUERIES - .get("select-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .fetch_all(executor) - .await?; + let rows: Vec = sqlx::query(get_sqlite_query!("select-tags")) + .bind(uid) + .fetch_all(executor) + .await?; let tags = rows.iter().map(|r| r.get(0)).collect::>(); @@ -480,38 +541,26 @@ pub(crate) async fn update_object_( .context("failed serializing the attributes to JSON") .reason(ErrorReason::Internal_Server_Error)?; - sqlx::query( - SQLITE_QUERIES - .get("update-object-with-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(object_json) - .bind(attributes_json) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("update-object-with-object")) + .bind(object_json) + .bind(attributes_json) + .bind(uid) + .execute(&mut **executor) + .await?; // Insert the new tags if any if let Some(tags) = tags { // delete the existing tags - sqlx::query( - SQLITE_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; - for tag in tags { - sqlx::query( - SQLITE_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + sqlx::query(get_sqlite_query!("delete-tags")) .bind(uid) - .bind(tag) .execute(&mut **executor) .await?; + for tag in tags { + sqlx::query(get_sqlite_query!("insert-tags")) + .bind(uid) + .bind(tag) + .execute(&mut **executor) + .await?; } } @@ -524,15 +573,11 @@ pub(crate) async fn update_state_( state: StateEnumeration, executor: &mut Transaction<'_, Sqlite>, ) -> KResult<()> { - sqlx::query( - SQLITE_QUERIES - .get("update-object-with-state") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(state.to_string()) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("update-object-with-state")) + .bind(state.to_string()) + .bind(uid) + .execute(&mut **executor) + .await?; trace!("Updated in DB: {uid}"); Ok(()) } @@ -543,25 +588,17 @@ pub(crate) async fn delete_( executor: &mut Transaction<'_, Sqlite>, ) -> KResult<()> { // delete the object - sqlx::query( - SQLITE_QUERIES - .get("delete-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("delete-object")) + .bind(uid) + .bind(owner) + .execute(&mut **executor) + .await?; // delete the tags - sqlx::query( - SQLITE_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("delete-tags")) + .bind(uid) + .execute(&mut **executor) + .await?; trace!("Deleted in DB: {uid}"); Ok(()) @@ -587,41 +624,29 @@ pub(crate) async fn upsert_( .context("failed serializing the attributes to JSON") .reason(ErrorReason::Internal_Server_Error)?; - sqlx::query( - SQLITE_QUERIES - .get("upsert-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(object_json) - .bind(attributes_json) - .bind(state.to_string()) - .bind(owner) - .execute(&mut **executor) - .await?; + sqlx::query(get_sqlite_query!("upsert-object")) + .bind(uid) + .bind(object_json) + .bind(attributes_json) + .bind(state.to_string()) + .bind(owner) + .execute(&mut **executor) + .await?; // Insert the new tags if present if let Some(tags) = tags { // delete the existing tags - sqlx::query( - SQLITE_QUERIES - .get("delete-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .execute(&mut **executor) - .await?; - // insert the new ones - for tag in tags { - sqlx::query( - SQLITE_QUERIES - .get("insert-tags") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) + sqlx::query(get_sqlite_query!("delete-tags")) .bind(uid) - .bind(tag) .execute(&mut **executor) .await?; + // insert the new ones + for tag in tags { + sqlx::query(get_sqlite_query!("insert-tags")) + .bind(uid) + .bind(tag) + .execute(&mut **executor) + .await?; } } @@ -637,14 +662,10 @@ where E: Executor<'e, Database = Sqlite> + Copy, { debug!("Uid = {}", uid); - let list = sqlx::query( - SQLITE_QUERIES - .get("select-rows-read_access-with-object-id") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .fetch_all(executor) - .await?; + let list = sqlx::query(get_sqlite_query!("select-rows-read_access-with-object-id")) + .bind(uid) + .fetch_all(executor) + .await?; let mut ids: HashMap> = HashMap::with_capacity(list.len()); for row in list { ids.insert( @@ -665,15 +686,11 @@ pub(crate) async fn list_user_granted_access_rights_<'e, E>( where E: Executor<'e, Database = Sqlite> + Copy, { - debug!("Owner = {}", user); - let list = sqlx::query( - SQLITE_QUERIES - .get("select-objects-access-obtained") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(user) - .fetch_all(executor) - .await?; + debug!("user = {}", user); + let list = sqlx::query(get_sqlite_query!("select-objects-access-obtained")) + .bind(user) + .fetch_all(executor) + .await?; let mut ids: HashMap)> = HashMap::with_capacity(list.len()); for row in list { @@ -711,15 +728,11 @@ async fn perms<'e, E>(uid: &str, userid: &str, executor: E) -> KResult + Copy, { - let row: Option = sqlx::query( - SQLITE_QUERIES - .get("select-user-accesses-for-object") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .fetch_optional(executor) - .await?; + let row: Option = sqlx::query(get_sqlite_query!("select-user-accesses-for-object")) + .bind(uid) + .bind(userid) + .fetch_optional(executor) + .await?; row.map_or(Ok(HashSet::::new()), |row| { let perms_raw = row.get::, _>(0); @@ -753,16 +766,12 @@ where .reason(ErrorReason::Internal_Server_Error)?; // Upsert the DB - sqlx::query( - SQLITE_QUERIES - .get("upsert-row-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .bind(json) - .execute(executor) - .await?; + sqlx::query(get_sqlite_query!("upsert-row-read_access")) + .bind(uid) + .bind(userid) + .bind(json) + .execute(executor) + .await?; trace!("Insert read access right in DB: {uid} / {userid}"); Ok(()) } @@ -785,15 +794,11 @@ where // No remaining permissions, delete the row if perms.is_empty() { - sqlx::query( - SQLITE_QUERIES - .get("delete-rows-read_access") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .execute(executor) - .await?; + sqlx::query(get_sqlite_query!("delete-rows-read_access")) + .bind(uid) + .bind(userid) + .execute(executor) + .await?; return Ok(()) } @@ -803,16 +808,12 @@ where .reason(ErrorReason::Internal_Server_Error)?; // Update the DB - sqlx::query( - SQLITE_QUERIES - .get("update-rows-read_access-with-permission") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(userid) - .bind(json) - .execute(executor) - .await?; + sqlx::query(get_sqlite_query!("update-rows-read_access-with-permission")) + .bind(uid) + .bind(userid) + .bind(json) + .execute(executor) + .await?; trace!("Deleted in DB: {uid} / {userid}"); Ok(()) } @@ -821,15 +822,11 @@ pub(crate) async fn is_object_owned_by_<'e, E>(uid: &str, owner: &str, executor: where E: Executor<'e, Database = Sqlite> + Copy, { - let row: Option = sqlx::query( - SQLITE_QUERIES - .get("has-row-objects") - .ok_or_else(|| kms_error!("SQL query can't be found"))?, - ) - .bind(uid) - .bind(owner) - .fetch_optional(executor) - .await?; + let row: Option = sqlx::query(get_sqlite_query!("has-row-objects")) + .bind(uid) + .bind(owner) + .fetch_optional(executor) + .await?; Ok(row.is_some()) } @@ -885,30 +882,22 @@ pub(crate) async fn clear_database_<'e, E>(executor: E) -> KResult<()> where E: Executor<'e, Database = Sqlite> + Copy, { + // Erase `context` table + sqlx::query(get_sqlite_query!("clean-table-context")) + .execute(executor) + .await?; // Erase `objects` table - sqlx::query( - SQLITE_QUERIES - .get("clean-table-objects") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_sqlite_query!("clean-table-objects")) + .execute(executor) + .await?; // Erase `read_access` table - sqlx::query( - SQLITE_QUERIES - .get("clean-table-read_access") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_sqlite_query!("clean-table-read_access")) + .execute(executor) + .await?; // Erase `tags` table - sqlx::query( - SQLITE_QUERIES - .get("clean-table-tags") - .expect("SQL query can't be found"), - ) - .execute(executor) - .await?; + sqlx::query(get_sqlite_query!("clean-table-tags")) + .execute(executor) + .await?; Ok(()) } @@ -952,3 +941,140 @@ pub(crate) async fn atomic_( } Ok(()) } + +pub(crate) async fn is_migration_in_progress_<'e, E>(executor: E) -> KResult +where + E: Executor<'e, Database = Sqlite> + Copy, +{ + match sqlx::query(get_sqlite_query!("select-context")) + .fetch_optional(executor) + .await? + { + Some(context_row) => { + let state = context_row.get::(1); + Ok(state == "upgrading") + } + None => Ok(false), + } +} + +pub(crate) async fn migrate_( + executor: &Pool, + last_version_run: &str, + query_name: &str, +) -> KResult<()> { + trace!("Set status to upgrading and last version run: {last_version_run}"); + let upsert_context = get_sqlite_query!(query_name); + trace!("{query_name}: {upsert_context}"); + match query_name { + "insert-context" => { + sqlx::query(upsert_context) + .bind(last_version_run) + .bind("upgrading") + .execute(executor) + .await + } + "update-context" => { + sqlx::query(upsert_context) + .bind(last_version_run) + .bind("upgrading") + .bind("upgrading") + .execute(executor) + .await + } + _ => kms_bail!("Unknown query name: {query_name}"), + }?; + + trace!("Migrate data from version {last_version_run}"); + + // Process migration for each KMS version + let current_kms_version = crate_version!(); + if last_version_run == KMS_VERSION_BEFORE_MIGRATION_SUPPORT { + migrate_from_4_12_0_to_4_13_0(executor).await?; + } else { + trace!("No migration needed between {last_version_run} and {current_kms_version}"); + } + + // Set the current running version + trace!("Set status to ready and last version run: {current_kms_version}"); + sqlx::query(get_sqlite_query!("update-context")) + .bind(current_kms_version) + .bind("ready") + .bind("upgrading") + .execute(executor) + .await?; + + Ok(()) +} + +/// Before the version 4.13.0, the KMIP attributes were stored in the objects table (via the objects themselves). +/// The new column attributes allows to store the KMIP attributes in a dedicated column even for KMIP objects that do not have KMIP attributes (such as Certificates). +pub(crate) async fn migrate_from_4_12_0_to_4_13_0(executor: &Pool) -> KResult<()> { + trace!("Migrating from 4.12.0 to 4.13.0"); + + // Add the column attributes to the objects table + if (sqlx::query("SELECT attributes from objects") + .execute(executor) + .await) + .is_ok() + { + trace!("Column attributes already exists, nothing to do"); + return Ok(()); + } + + trace!("Column attributes does not exist, adding it"); + sqlx::query(get_sqlite_query!("add-column-attributes")) + .execute(executor) + .await?; + + // Select all objects and extract the KMIP attributes to be stored in the new column + let rows = sqlx::query("SELECT * FROM objects") + .fetch_all(executor) + .await?; + + let mut operations = Vec::with_capacity(rows.len()); + for row in rows { + let uid = row.get::(0); + let db_object: DBObject = serde_json::from_slice(&row.get::, _>(1)) + .context("migrate: failed deserializing the object") + .reason(ErrorReason::Internal_Server_Error)?; + let object = Object::post_fix(db_object.object_type, db_object.object); + trace!( + "migrate_from_4_12_0_to_4_13_0: object (type: {})={:?}", + object.object_type(), + uid + ); + let attributes = match object.attributes() { + Ok(attrs) => attrs.clone(), + Err(_error) => { + // For example, Certificate object has no KMIP-attribute + Attributes::default() + } + }; + let tags = retrieve_tags_(&uid, executor).await?; + operations.push(AtomicOperation::UpdateObject(( + uid, + object, + attributes, + Some(tags), + ))); + } + + let mut tx = executor.begin().await?; + match atomic_( + "this user is not used to update objects", + &operations, + &mut tx, + ) + .await + { + Ok(()) => { + tx.commit().await?; + Ok(()) + } + Err(e) => { + tx.rollback().await.context("transaction failed")?; + Err(e) + } + } +} diff --git a/crate/server/src/database/tests/additional_redis_findex_tests.rs b/crate/server/src/database/tests/additional_redis_findex_tests.rs index f05ed564..b62c55ca 100644 --- a/crate/server/src/database/tests/additional_redis_findex_tests.rs +++ b/crate/server/src/database/tests/additional_redis_findex_tests.rs @@ -29,7 +29,7 @@ use crate::{ result::KResult, }; -struct DummyDB {} +struct DummyDB; #[async_trait] impl RemovedLocationsFinder for DummyDB { async fn find_removed_locations( @@ -73,7 +73,7 @@ pub(crate) async fn test_objects_db() -> KResult<()> { uid, &RedisDbObject::new( object.clone(), - "owner".to_string(), + "owner".to_owned(), StateEnumeration::Active, Some(HashSet::new()), ), @@ -127,8 +127,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("O1")); assert_eq!( - permissions.get("O1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O1"], + HashSet::from([ObjectOperationType::Encrypt]) ); //find the permission for the object O1 @@ -138,8 +138,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("U1")); assert_eq!( - permissions.get("U1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U1"], + HashSet::from([ObjectOperationType::Encrypt]) ); // add the permission Decrypt to user U1 for object O1 @@ -160,8 +160,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("O1")); assert_eq!( - permissions.get("O1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt, ObjectOperationType::Decrypt]) + permissions["O1"], + HashSet::from([ObjectOperationType::Encrypt, ObjectOperationType::Decrypt]) ); //find the permission for the object O1 @@ -171,8 +171,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("U1")); assert_eq!( - permissions.get("U1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt, ObjectOperationType::Decrypt]) + permissions["U1"], + HashSet::from([ObjectOperationType::Encrypt, ObjectOperationType::Decrypt]) ); // the situation now is that we have @@ -194,8 +194,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("O1")); assert_eq!( - permissions.get("O1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O1"], + HashSet::from([ObjectOperationType::Encrypt]) ); //find the permission for the object O1 @@ -205,13 +205,13 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 2); assert!(permissions.contains_key("U1")); assert_eq!( - permissions.get("U1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt, ObjectOperationType::Decrypt]) + permissions["U1"], + HashSet::from([ObjectOperationType::Encrypt, ObjectOperationType::Decrypt]) ); assert!(permissions.contains_key("U2")); assert_eq!( - permissions.get("U2").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U2"], + HashSet::from([ObjectOperationType::Encrypt]) ); // the situation now is that we have @@ -234,13 +234,13 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 2); assert!(permissions.contains_key("O1")); assert_eq!( - permissions.get("O1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O1"], + HashSet::from([ObjectOperationType::Encrypt]) ); assert!(permissions.contains_key("O2")); assert_eq!( - permissions.get("O2").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O2"], + HashSet::from([ObjectOperationType::Encrypt]) ); //find the permission for the object O2 @@ -250,8 +250,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("U2")); assert_eq!( - permissions.get("U2").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U2"], + HashSet::from([ObjectOperationType::Encrypt]) ); // the situation now is that we have @@ -275,8 +275,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("O1")); assert_eq!( - permissions.get("O1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O1"], + HashSet::from([ObjectOperationType::Encrypt]) ); //find the permission for the object O1 @@ -286,13 +286,13 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 2); assert!(permissions.contains_key("U1")); assert_eq!( - permissions.get("U1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U1"], + HashSet::from([ObjectOperationType::Encrypt]) ); assert!(permissions.contains_key("U2")); assert_eq!( - permissions.get("U2").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U2"], + HashSet::from([ObjectOperationType::Encrypt]) ); // let us remove the permission Encrypt on object O1 for user U1 @@ -316,8 +316,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("U2")); assert_eq!( - permissions.get("U2").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U2"], + HashSet::from([ObjectOperationType::Encrypt]) ); // let us remove the permission Encrypt on object O1 for user U2 @@ -335,8 +335,8 @@ pub(crate) async fn test_permissions_db() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("O2")); assert_eq!( - permissions.get("O2").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O2"], + HashSet::from([ObjectOperationType::Encrypt]) ); //find the permission for the object O1 @@ -398,8 +398,8 @@ pub(crate) async fn test_corner_case() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("O1")); assert_eq!( - permissions.get("O1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["O1"], + HashSet::from([ObjectOperationType::Encrypt]) ); // test there is one permission for object O1 @@ -409,8 +409,8 @@ pub(crate) async fn test_corner_case() -> KResult<()> { assert_eq!(permissions.len(), 1); assert!(permissions.contains_key("U1")); assert_eq!( - permissions.get("U1").unwrap(), - &HashSet::from([ObjectOperationType::Encrypt]) + permissions["U1"], + HashSet::from([ObjectOperationType::Encrypt]) ); // remove the permission again diff --git a/crate/server/src/database/tests/database_tests.rs b/crate/server/src/database/tests/database_tests.rs index 46d2d6b3..5ca92ac1 100644 --- a/crate/server/src/database/tests/database_tests.rs +++ b/crate/server/src/database/tests/database_tests.rs @@ -11,6 +11,7 @@ use cosmian_kmip::{ }, }; use cosmian_kms_client::access::ObjectOperationType; +use cosmian_logger::log_utils::log_init; use uuid::Uuid; use crate::{ @@ -26,7 +27,7 @@ use crate::{ pub(crate) async fn tx_and_list( db_and_params: &(DB, Option), ) -> KResult<()> { - cosmian_logger::log_utils::log_init(None); + log_init(None); let db = &db_and_params.0; let db_params = db_and_params.1.as_ref(); @@ -113,7 +114,7 @@ pub(crate) async fn tx_and_list( pub(crate) async fn atomic( db_and_params: &(DB, Option), ) -> KResult<()> { - cosmian_logger::log_utils::log_init(None); + log_init(None); let db = &db_and_params.0; let db_params = db_and_params.1.as_ref(); @@ -234,7 +235,7 @@ pub(crate) async fn atomic( pub(crate) async fn upsert( db_and_params: &(DB, Option), ) -> KResult<()> { - cosmian_logger::log_utils::log_init(None); + log_init(None); let db = &db_and_params.0; let db_params = db_and_params.1.as_ref(); @@ -277,7 +278,7 @@ pub(crate) async fn upsert( let attributes = symmetric_key.attributes_mut()?; attributes.link = Some(vec![Link { link_type: LinkType::PreviousLink, - linked_object_identifier: LinkedObjectIdentifier::TextString("foo".to_string()), + linked_object_identifier: LinkedObjectIdentifier::TextString("foo".to_owned()), }]); db.upsert( @@ -300,11 +301,15 @@ pub(crate) async fn upsert( 1 => { assert_eq!(StateEnumeration::PreActive, objs_[0].state); assert_eq!( - objs_[0].object.attributes()?.link.as_ref().ok_or_else( - || KmsError::ServerError("links should not be empty".to_string()) - )?[0] - .linked_object_identifier, - LinkedObjectIdentifier::TextString("foo".to_string()) + objs_[0] + .object + .attributes()? + .link + .as_ref() + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))? + [0] + .linked_object_identifier, + LinkedObjectIdentifier::TextString("foo".to_owned()) ); } _ => kms_bail!("There should be only one object"), @@ -326,7 +331,7 @@ pub(crate) async fn upsert( pub(crate) async fn crud( db_and_params: &(DB, Option), ) -> KResult<()> { - cosmian_logger::log_utils::log_init(None); + log_init(None); let db = &db_and_params.0; let db_params = db_and_params.1.as_ref(); @@ -385,7 +390,7 @@ pub(crate) async fn crud( let attributes = symmetric_key.attributes_mut()?; attributes.link = Some(vec![Link { link_type: LinkType::PreviousLink, - linked_object_identifier: LinkedObjectIdentifier::TextString("foo".to_string()), + linked_object_identifier: LinkedObjectIdentifier::TextString("foo".to_owned()), }]); db.update_object( @@ -407,11 +412,15 @@ pub(crate) async fn crud( 1 => { assert_eq!(StateEnumeration::Active, objs_[0].state); assert_eq!( - objs_[0].object.attributes()?.link.as_ref().ok_or_else( - || KmsError::ServerError("links should not be empty".to_string()) - )?[0] - .linked_object_identifier, - LinkedObjectIdentifier::TextString("foo".to_string()) + objs_[0] + .object + .attributes()? + .link + .as_ref() + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))? + [0] + .linked_object_identifier, + LinkedObjectIdentifier::TextString("foo".to_owned()) ); } _ => kms_bail!("There should be only one object"), diff --git a/crate/server/src/database/tests/find_attributes_test.rs b/crate/server/src/database/tests/find_attributes_test.rs index 9969a1c7..c271a9a0 100644 --- a/crate/server/src/database/tests/find_attributes_test.rs +++ b/crate/server/src/database/tests/find_attributes_test.rs @@ -46,7 +46,7 @@ pub(crate) async fn find_attributes( // Define the link vector let link = vec![Link { link_type: LinkType::ParentLink, - linked_object_identifier: LinkedObjectIdentifier::TextString("foo".to_string()), + linked_object_identifier: LinkedObjectIdentifier::TextString("foo".to_owned()), }]; let attributes = symmetric_key.attributes_mut()?; @@ -77,7 +77,7 @@ pub(crate) async fn find_attributes( assert_eq!(&symmetric_key, &objs_[0].object); assert_eq!( objs_[0].object.attributes()?.link.as_ref().unwrap()[0].linked_object_identifier, - LinkedObjectIdentifier::TextString("foo".to_string()) + LinkedObjectIdentifier::TextString("foo".to_owned()) ); } _ => kms_bail!("There should be one object"), @@ -103,7 +103,7 @@ pub(crate) async fn find_attributes( // Define a link vector not present in any database objects let link = vec![Link { link_type: LinkType::ParentLink, - linked_object_identifier: LinkedObjectIdentifier::TextString("bar".to_string()), + linked_object_identifier: LinkedObjectIdentifier::TextString("bar".to_owned()), }]; let researched_attributes = Some(Attributes { diff --git a/crate/server/src/database/tests/mod.rs b/crate/server/src/database/tests/mod.rs index f3a5dad9..04fd6838 100644 --- a/crate/server/src/database/tests/mod.rs +++ b/crate/server/src/database/tests/mod.rs @@ -1,6 +1,10 @@ +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::path::PathBuf; use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; +use cosmian_logger::log_utils::log_init; +use tempfile::TempDir; use self::{ additional_redis_findex_tests::{test_corner_case, test_objects_db, test_permissions_db}, @@ -35,7 +39,7 @@ fn get_redis_url() -> String { if let Ok(var_env) = std::env::var("REDIS_HOST") { format!("redis://{var_env}:6379") } else { - "redis://localhost:6379".to_string() + "redis://localhost:6379".to_owned() } } @@ -131,8 +135,8 @@ pub(crate) async fn test_sql_cipher() -> KResult<()> { #[tokio::test] pub(crate) async fn test_sqlite() -> KResult<()> { - find_attributes(&get_sqlite().await?).await?; json_access(&get_sqlite().await?).await?; + find_attributes(&get_sqlite().await?).await?; owner(&get_sqlite().await?).await?; permissions(&get_sqlite().await?).await?; tags(&get_sqlite().await?, true).await?; @@ -159,14 +163,35 @@ pub(crate) async fn test_pgsql() -> KResult<()> { #[tokio::test] pub(crate) async fn test_mysql() -> KResult<()> { - crud(&get_mysql().await?).await?; - upsert(&get_mysql().await?).await?; - tx_and_list(&get_mysql().await?).await?; - atomic(&get_mysql().await?).await?; + log_init(None); json_access(&get_mysql().await?).await?; find_attributes(&get_mysql().await?).await?; owner(&get_mysql().await?).await?; permissions(&get_mysql().await?).await?; tags(&get_mysql().await?, true).await?; + tx_and_list(&get_mysql().await?).await?; + atomic(&get_mysql().await?).await?; + upsert(&get_mysql().await?).await?; + crud(&get_mysql().await?).await?; + Ok(()) +} + +#[tokio::test] +pub(crate) async fn test_migrate_sqlite() -> KResult<()> { + log_init(None); + for sqlite_path in [ + "src/tests/migrate/kms_4.12.0.sqlite", + "src/tests/migrate/kms_4.16.0.sqlite", + "src/tests/migrate/kms_4.17.0.sqlite", + ] { + let tmp_dir = TempDir::new().unwrap(); + let tmp_path = tmp_dir.path(); + let tmp_file_path = tmp_path.join("kms.db"); + if tmp_file_path.exists() { + std::fs::remove_file(&tmp_file_path)?; + } + std::fs::copy(sqlite_path, &tmp_file_path)?; + SqlitePool::instantiate(&tmp_file_path, false).await?; + } Ok(()) } diff --git a/crate/server/src/database/tests/owner_test.rs b/crate/server/src/database/tests/owner_test.rs index 852f2a83..762802e6 100644 --- a/crate/server/src/database/tests/owner_test.rs +++ b/crate/server/src/database/tests/owner_test.rs @@ -144,8 +144,8 @@ pub(crate) async fn owner( .list_user_granted_access_rights(user_id_2, db_params) .await?; assert_eq!( - objects.get(&uid).unwrap(), - &( + objects[&uid], + ( String::from(owner), StateEnumeration::Active, vec![ObjectOperationType::Get].into_iter().collect(), diff --git a/crate/server/src/error.rs b/crate/server/src/error.rs index f2d982ba..85ee116b 100644 --- a/crate/server/src/error.rs +++ b/crate/server/src/error.rs @@ -301,6 +301,7 @@ macro_rules! kms_bail { }; } +#[allow(clippy::expect_used)] #[cfg(test)] mod tests { use super::KmsError; @@ -312,16 +313,10 @@ mod tests { assert_eq!("Unexpected server error: interpolate 42", err.to_string()); let err = bail(); - assert_eq!( - "Unexpected server error: interpolate 43", - err.unwrap_err().to_string() - ); + err.expect_err("Unexpected server error: interpolate 43"); let err = ensure(); - assert_eq!( - "Unexpected server error: interpolate 44", - err.unwrap_err().to_string() - ); + err.expect_err("Unexpected server error: interpolate 44"); } fn bail() -> Result<(), KmsError> { diff --git a/crate/server/src/kms_server.rs b/crate/server/src/kms_server.rs index 9a943818..4d397bda 100644 --- a/crate/server/src/kms_server.rs +++ b/crate/server/src/kms_server.rs @@ -171,6 +171,10 @@ async fn start_https_kms_server( * Returns a `Result` type that contains a `Server` instance if successful, or an error if * something went wrong. * + * # Errors + * + * This function can return the following errors: + * - `KmsError::ServerError` - If there is an error in the server configuration or preparation. */ pub async fn prepare_kms_server( kms_server: Arc, @@ -227,7 +231,7 @@ pub async fn prepare_kms_server( let google_cse_jwt_config = if enable_google_cse { let Some(jwks_manager) = jwks_manager else { return Err(KmsError::ServerError( - "No JWKS manager to handle Google CSE JWT authorization".to_string(), + "No JWKS manager to handle Google CSE JWT authorization".to_owned(), )); }; Some(GoogleCseConfig { diff --git a/crate/server/src/lib.rs b/crate/server/src/lib.rs index 269d254d..5a22a580 100644 --- a/crate/server/src/lib.rs +++ b/crate/server/src/lib.rs @@ -10,29 +10,46 @@ let_underscore, rust_2024_compatibility, unreachable_pub, + unused, clippy::all, clippy::suspicious, clippy::complexity, clippy::perf, clippy::style, clippy::pedantic, - clippy::cargo + clippy::cargo, + + // restriction lints + clippy::unwrap_used, + clippy::get_unwrap, + clippy::expect_used, + // clippy::indexing_slicing, + clippy::unwrap_in_result, + clippy::assertions_on_result_states, + clippy::panic, + clippy::panic_in_result_fn, + clippy::renamed_function_params, + clippy::verbose_file_reads, + clippy::str_to_string, + clippy::string_to_string, + clippy::unreachable, + clippy::as_conversions, + clippy::print_stdout, + clippy::empty_structs_with_brackets, + clippy::unseparated_literal_suffix, + clippy::map_err_ignore, )] #![allow( clippy::module_name_repetitions, clippy::similar_names, - clippy::missing_errors_doc, - clippy::missing_panics_doc, clippy::too_many_lines, - clippy::cast_possible_wrap, - clippy::cast_sign_loss, - clippy::cast_possible_truncation, clippy::cargo_common_metadata, clippy::multiple_crate_versions )] pub mod config; pub mod core; +#[allow(clippy::expect_used)] pub mod database; pub mod error; pub mod kms_server; @@ -43,5 +60,6 @@ pub mod telemetry; pub use database::KMSServer; +#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)] #[cfg(test)] mod tests; diff --git a/crate/server/src/middlewares/api_token_auth.rs b/crate/server/src/middlewares/api_token_auth.rs index 9d8fa9ec..0c3cfb02 100644 --- a/crate/server/src/middlewares/api_token_auth.rs +++ b/crate/server/src/middlewares/api_token_auth.rs @@ -26,12 +26,8 @@ pub(crate) async fn manage_api_token_request( where S: Service, Error = Error>, { - trace!("API Token Authentication..."); match manage_api_token(kms_server, &req).await { - Ok(()) => { - trace!("API Token Authentication successful"); - Ok(service.call(req).await?.map_into_left_body()) - } + Ok(()) => Ok(service.call(req).await?.map_into_left_body()), Err(e) => { error!("{:?} {} 401 unauthorized: {e:?}", req.method(), req.path(),); Ok(req @@ -86,21 +82,15 @@ async fn get_api_token(kms_server: &Arc, api_token_id: &str) -> KResult, req: &ServiceRequest) -> KResult<()> { - trace!( - "Token authentication using this API token ID: {:?}", - kms_server.params.api_token_id - ); - match &kms_server.params.api_token_id { Some(api_token_id) => { + trace!("Token authentication using this API token ID: {api_token_id}"); let api_token = get_api_token(&kms_server, api_token_id.as_str()).await?; let client_token = req .headers() .get(header::AUTHORIZATION) - .ok_or_else(|| { - KmsError::InvalidRequest("Missing Authorization header".to_string()) - })? + .ok_or_else(|| KmsError::InvalidRequest("Missing Authorization header".to_owned()))? .to_str() .map_err(|e| { KmsError::InvalidRequest(format!( @@ -131,7 +121,7 @@ async fn manage_api_token(kms_server: Arc, req: &ServiceRequest) -> KResult req.path(), ); Err(KmsError::Unauthorized( - "Client and server authentication tokens mismatch".to_string(), + "Client and server authentication tokens mismatch".to_owned(), )) } } diff --git a/crate/server/src/middlewares/jwks.rs b/crate/server/src/middlewares/jwks.rs index a7e9cf73..8d9ea013 100644 --- a/crate/server/src/middlewares/jwks.rs +++ b/crate/server/src/middlewares/jwks.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::RwLock}; use alcoholic_jwt::{JWK, JWKS}; use chrono::{DateTime, Duration, Utc}; -use crate::result::KResult; +use crate::{error::KmsError, result::KResult}; static REFRESH_INTERVAL: i64 = 60; // in secs @@ -27,19 +27,23 @@ impl JwksManager { } /// Lock `jwks` to replace it - fn set_jwks(&self, new_jwks: HashMap) { - let mut jwks = self.jwks.write().expect("cannot lock JWKS for write"); + fn set_jwks(&self, new_jwks: HashMap) -> KResult<()> { + let mut jwks = self.jwks.write().map_err(|e| { + KmsError::ServerError(format!("cannot lock JWKS for write. Error: {e:?}")) + })?; *jwks = new_jwks; + Ok(()) } /// Find the key identifier `kid` in each registered JWKS - pub fn find(&self, kid: &str) -> Option { - self.jwks + pub fn find(&self, kid: &str) -> KResult> { + Ok(self + .jwks .read() - .expect("cannot lock JWKS for read") + .map_err(|e| KmsError::ServerError(format!("cannot lock JWKS for read. Error: {e:?}")))? .iter() .find_map(|(_, jwks)| jwks.find(kid)) - .cloned() + .cloned()) } /// Fetch again all JWKS using the `uris`. @@ -47,10 +51,9 @@ impl JwksManager { /// The threshold to refresh JWKS is set to `REFRESH_INTERVAL`. pub async fn refresh(&self) -> KResult<()> { let refresh_is_allowed = { - let mut last_update = self - .last_update - .write() - .expect("cannot lock last_update for write"); + let mut last_update = self.last_update.write().map_err(|e| { + KmsError::ServerError(format!("cannot lock last_update for write. Error: {e:?}")) + })?; let can_be_refreshed = last_update.map_or(true, |lu| { (lu + Duration::seconds(REFRESH_INTERVAL)) < Utc::now() @@ -65,7 +68,7 @@ impl JwksManager { if refresh_is_allowed { tracing::info!("Refreshing JWKS"); let refreshed_jwks = Self::fetch_all(&self.uris).await; - self.set_jwks(refreshed_jwks); + self.set_jwks(refreshed_jwks)?; } Ok(()) diff --git a/crate/server/src/middlewares/jwt.rs b/crate/server/src/middlewares/jwt.rs index 4b200591..e0a0196a 100644 --- a/crate/server/src/middlewares/jwt.rs +++ b/crate/server/src/middlewares/jwt.rs @@ -68,12 +68,12 @@ impl JwtConfig { ); tracing::trace!( "validating authentication token, expected JWT issuer: {}", - self.jwt_issuer_uri.to_string() + self.jwt_issuer_uri ); let mut validations = vec![ #[cfg(not(test))] - alcoholic_jwt::Validation::Issuer(self.jwt_issuer_uri.to_string()), + alcoholic_jwt::Validation::Issuer(self.jwt_issuer_uri.clone()), alcoholic_jwt::Validation::SubjectPresent, #[cfg(not(feature = "insecure"))] alcoholic_jwt::Validation::NotExpired, @@ -88,14 +88,14 @@ impl JwtConfig { // needs to be fetched from the token headers. let kid = token_kid(token) .map_err(|e| KmsError::Unauthorized(format!("Failed to decode kid: {e}")))? - .ok_or_else(|| KmsError::Unauthorized("No 'kid' claim present in token".to_string()))?; + .ok_or_else(|| KmsError::Unauthorized("No 'kid' claim present in token".to_owned()))?; tracing::trace!("looking for kid `{kid}` JWKS:\n{:?}", self.jwks); let jwk = self .jwks - .find(&kid) - .ok_or_else(|| KmsError::Unauthorized("Specified key not found in set".to_string()))?; + .find(&kid)? + .ok_or_else(|| KmsError::Unauthorized("Specified key not found in set".to_owned()))?; tracing::trace!("JWK has been found:\n{jwk:?}"); diff --git a/crate/server/src/middlewares/jwt_token_auth.rs b/crate/server/src/middlewares/jwt_token_auth.rs index f0b223b4..e8f864ca 100644 --- a/crate/server/src/middlewares/jwt_token_auth.rs +++ b/crate/server/src/middlewares/jwt_token_auth.rs @@ -21,7 +21,7 @@ pub(crate) async fn manage_jwt_request( where S: Service, Error = Error>, { - trace!("JWT Authentication..."); + trace!("Starting JWT Authentication..."); match manage_jwt(configs, &req).await { Ok(auth_claim) => { req.extensions_mut().insert(auth_claim); diff --git a/crate/server/src/middlewares/main.rs b/crate/server/src/middlewares/main.rs index b5d2dfef..fb68944e 100644 --- a/crate/server/src/middlewares/main.rs +++ b/crate/server/src/middlewares/main.rs @@ -51,7 +51,6 @@ where type Transform = AuthMiddleware; fn new_transform(&self, service: S) -> Self::Future { - debug!("JWT/Token Authentication enabled"); ok(AuthMiddleware { service: Rc::new(service), jwt_configurations: self.jwt_configurations.clone(), @@ -76,8 +75,8 @@ where type Future = Pin>>>; type Response = ServiceResponse>; - fn poll_ready(&self, cx: &mut Context) -> Poll> { - self.service.poll_ready(cx) + fn poll_ready(&self, ctx: &mut Context) -> Poll> { + self.service.poll_ready(ctx) } fn call(&self, req: ServiceRequest) -> Self::Future { diff --git a/crate/server/src/middlewares/ssl_auth.rs b/crate/server/src/middlewares/ssl_auth.rs index e385fbbe..8bc2ea4d 100644 --- a/crate/server/src/middlewares/ssl_auth.rs +++ b/crate/server/src/middlewares/ssl_auth.rs @@ -97,9 +97,9 @@ where type Response = ServiceResponse>; /// Poll the `SslAuthMiddleware` for readiness. - fn poll_ready(&self, cx: &mut Context) -> Poll> { + fn poll_ready(&self, ctx: &mut Context) -> Poll> { // Poll the underlying service for readiness. - self.service.poll_ready(cx) + self.service.poll_ready(ctx) } /// Call the `SslAuthMiddleware`. diff --git a/crate/server/src/result.rs b/crate/server/src/result.rs index 87b69ca2..d23d3164 100644 --- a/crate/server/src/result.rs +++ b/crate/server/src/result.rs @@ -4,9 +4,27 @@ use crate::error::KmsError; pub type KResult = Result; +/// A helper trait for `KResult` that provides additional methods for error handling. pub trait KResultHelper { + /// Sets the reason for the error. + /// + /// # Errors + /// + /// Returns a `KResult` with the specified `ErrorReason` if the original result is an error. fn reason(self, reason: ErrorReason) -> KResult; + + /// Sets the context for the error. + /// + /// # Errors + /// + /// Returns a `KResult` with the specified context if the original result is an error. fn context(self, context: &str) -> KResult; + + /// Sets the context for the error using a closure. + /// + /// # Errors + /// + /// Returns a `KResult` with the context returned by the closure if the original result is an error. fn with_context(self, op: O) -> KResult where O: FnOnce() -> String; @@ -34,7 +52,7 @@ where impl KResultHelper for Option { fn context(self, context: &str) -> KResult { - self.ok_or_else(|| KmsError::ServerError(context.to_string())) + self.ok_or_else(|| KmsError::ServerError(context.to_owned())) } fn with_context(self, op: O) -> KResult diff --git a/crate/server/src/routes/google_cse/jwt.rs b/crate/server/src/routes/google_cse/jwt.rs index 1226e028..a12c3343 100644 --- a/crate/server/src/routes/google_cse/jwt.rs +++ b/crate/server/src/routes/google_cse/jwt.rs @@ -47,8 +47,7 @@ fn jwt_authorization_config_application( )); let jwt_audience = Some( - std::env::var("KMS_GOOGLE_CSE_AUDIENCE") - .unwrap_or_else(|_| "cse-authorization".to_string()), + std::env::var("KMS_GOOGLE_CSE_AUDIENCE").unwrap_or_else(|_| "cse-authorization".to_owned()), ); Arc::new(JwtConfig { @@ -66,7 +65,7 @@ pub fn jwt_authorization_config( .iter() .map(|app| { ( - (*app).to_string(), + (*app).to_owned(), jwt_authorization_config_application(app, jwks_manager.clone()), ) }) @@ -97,24 +96,26 @@ pub(crate) fn decode_jwt_authorization_token( KmsError::ServerError( "JWT audience should be configured with Google Workspace client-side \ encryption" - .to_string(), + .to_owned(), ) })? .to_string(), ), - alcoholic_jwt::Validation::Issuer(jwt_config.jwt_issuer_uri.to_string()), + alcoholic_jwt::Validation::Issuer(jwt_config.jwt_issuer_uri.clone()), ]; // If a JWKS contains multiple keys, the correct KID first // needs to be fetched from the token headers. let kid = token_kid(token) - .map_err(|_| KmsError::Unauthorized("Failed to decode token headers".to_string()))? - .ok_or_else(|| KmsError::Unauthorized("No 'kid' claim present in token".to_string()))?; + .map_err(|e| { + KmsError::Unauthorized(format!("Failed to decode token headers. Error: {e:?}")) + })? + .ok_or_else(|| KmsError::Unauthorized("No 'kid' claim present in token".to_owned()))?; tracing::trace!("looking for kid `{kid}` JWKS:\n{:?}", jwt_config.jwks); - let jwk = &jwt_config.jwks.find(&kid).ok_or_else(|| { - KmsError::Unauthorized("[Google CSE auth] Specified key not found in set".to_string()) + let jwk = &jwt_config.jwks.find(&kid)?.ok_or_else(|| { + KmsError::Unauthorized("[Google CSE auth] Specified key not found in set".to_owned()) })?; tracing::trace!("JWK has been found:\n{jwk:?}"); @@ -155,7 +156,7 @@ pub(crate) async fn validate_tokens( let cse_config = cse_config.as_ref().ok_or_else(|| { KmsError::ServerError( "JWT authentication and authorization configurations for Google CSE are not set" - .to_string(), + .to_owned(), ) })?; @@ -188,26 +189,26 @@ pub(crate) async fn validate_tokens( // The emails should match (case insensitive) let authentication_email = authentication_token.email.ok_or_else(|| { - KmsError::Unauthorized("Authentication token should contain an email".to_string()) + KmsError::Unauthorized("Authentication token should contain an email".to_owned()) })?; let authorization_email = authorization_token.email.ok_or_else(|| { - KmsError::Unauthorized("Authorization token should contain an email".to_string()) + KmsError::Unauthorized("Authorization token should contain an email".to_owned()) })?; kms_ensure!( authorization_email == authentication_email, KmsError::Unauthorized( - "Authentication and authorization emails in tokens do not match".to_string() + "Authentication and authorization emails in tokens do not match".to_owned() ) ); if let Some(roles) = roles { let role = authorization_token.role.ok_or_else(|| { - KmsError::Unauthorized("Authorization token should contain a role".to_string()) + KmsError::Unauthorized("Authorization token should contain a role".to_owned()) })?; kms_ensure!( roles.contains(&role.as_str()), KmsError::Unauthorized( - "Authorization token should contain a role of writer or owner".to_string() + "Authorization token should contain a role of writer or owner".to_owned() ) ); } @@ -229,6 +230,7 @@ pub(crate) async fn validate_tokens( #[cfg(test)] mod tests { + #![allow(clippy::unwrap_used)] use std::sync::Arc; use tracing::info; @@ -276,8 +278,8 @@ mod tests { let client_id = std::env::var("TEST_GOOGLE_OAUTH_CLIENT_ID").unwrap(); // Test authentication let jwt_authentication_config = JwtAuthConfig { - jwt_issuer_uri: Some(vec![JWT_ISSUER_URI.to_string()]), - jwks_uri: Some(vec![JWKS_URI.to_string()]), + jwt_issuer_uri: Some(vec![JWT_ISSUER_URI.to_owned()]), + jwks_uri: Some(vec![JWKS_URI.to_owned()]), jwt_audience: Some(vec![client_id]), }; let jwt_authentication_config = JwtConfig { @@ -292,17 +294,17 @@ mod tests { info!("AUTHENTICATION token: {:?}", authentication_token); assert_eq!( authentication_token.iss, - Some("https://accounts.google.com".to_string()) + Some("https://accounts.google.com".to_owned()) ); assert_eq!( authentication_token.email, - Some("blue@cosmian.com".to_string()) + Some("blue@cosmian.com".to_owned()) ); assert_eq!( authentication_token.aud, Some( "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com" - .to_string() + .to_owned() ) ); @@ -317,7 +319,7 @@ mod tests { tracing::trace!("{jwt_authorization_config:#?}"); let (authorization_token, jwt_headers) = decode_jwt_authorization_token( - jwt_authorization_config.get("drive").unwrap(), + &jwt_authorization_config["drive"], &wrap_request.authorization, ) .unwrap(); @@ -326,14 +328,14 @@ mod tests { assert_eq!( authorization_token.email, - Some("blue@cosmian.com".to_string()) + Some("blue@cosmian.com".to_owned()) ); - // prev: Some("cse-authorization".to_string()) + // prev: Some("cse-authorization".to_owned()) assert_eq!( authorization_token.aud, Some( "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com" - .to_string() + .to_owned() ) ); } diff --git a/crate/server/src/routes/google_cse/mod.rs b/crate/server/src/routes/google_cse/mod.rs index 4f1e4db4..dd885135 100644 --- a/crate/server/src/routes/google_cse/mod.rs +++ b/crate/server/src/routes/google_cse/mod.rs @@ -29,7 +29,7 @@ impl CseErrorReply { fn from(e: &KmsError) -> Self { Self { code: e.status_code().as_u16(), - message: "A CSE request to the Cosmian KMS failed".to_string(), + message: "A CSE request to the Cosmian KMS failed".to_owned(), details: e.to_string(), } } diff --git a/crate/server/src/routes/google_cse/operations.rs b/crate/server/src/routes/google_cse/operations.rs index 413e65d2..56e156a5 100644 --- a/crate/server/src/routes/google_cse/operations.rs +++ b/crate/server/src/routes/google_cse/operations.rs @@ -41,26 +41,30 @@ pub struct StatusResponse { pub operations_supported: Vec, } +/// Returns the status of the server. +/// +/// # Returns +/// - `StatusResponse`: The status of the server. #[must_use] pub fn get_status() -> StatusResponse { debug!("get_status"); StatusResponse { - server_type: "KACLS".to_string(), - vendor_id: "Cosmian".to_string(), - version: crate_version!().to_string(), - name: "Cosmian KMS".to_string(), + server_type: "KACLS".to_owned(), + vendor_id: "Cosmian".to_owned(), + version: crate_version!().to_owned(), + name: "Cosmian KMS".to_owned(), operations_supported: vec![ - "digest".to_string(), - "privatekeydecrypt".to_string(), - "privatekeysign".to_string(), - "privilegedprivatekeydecrypt".to_string(), - "privilegedunwrap".to_string(), - "privilegedwrap".to_string(), - "rewrap".to_string(), - "status".to_string(), - "unwrap".to_string(), - "wrap".to_string(), - "wrapprivatekey".to_string(), + "digest".to_owned(), + "privatekeydecrypt".to_owned(), + "privatekeysign".to_owned(), + "privilegedprivatekeydecrypt".to_owned(), + "privilegedunwrap".to_owned(), + "privilegedwrap".to_owned(), + "rewrap".to_owned(), + "status".to_owned(), + "unwrap".to_owned(), + "wrap".to_owned(), + "wrapprivatekey".to_owned(), ], } } @@ -78,10 +82,21 @@ pub struct WrapResponse { pub wrapped_key: String, } -/// Returns encrypted Data Encryption Key (DEK) and associated data. +/// Wraps a Data Encryption Key (DEK) using the specified authentication and authorization tokens. /// /// See [doc](https://developers.google.com/workspace/cse/reference/wrap) and /// for more details, see [Encrypt & decrypt data](https://developers.google.com/workspace/cse/guides/encrypt-and-decrypt-data) +/// # Arguments +/// - `req_http`: The HTTP request. +/// - `wrap_request`: The wrap request. +/// - `cse_config`: The Google CSE configuration. +/// - `kms`: The KMS server. +/// +/// # Returns +/// - `WrapResponse`: The wrapped key. +/// +/// # Errors +/// This function can return an error if there is a problem with the encryption process or if the tokens validation fails. pub async fn wrap( req_http: HttpRequest, wrap_request: WrapRequest, @@ -125,7 +140,7 @@ pub async fn wrap( wrapping_method: kmip_types::WrappingMethod::Encrypt, encoding_option: Some(EncodingOption::NoEncoding), encryption_key_information: Some(kmip_types::EncryptionKeyInformation { - unique_identifier: UniqueIdentifier::TextString("[\"google_cse\"]".to_string()), + unique_identifier: UniqueIdentifier::TextString("[\"google_cse\"]".to_owned()), cryptographic_parameters: Some(Box::default()), }), ..Default::default() @@ -158,10 +173,21 @@ pub struct UnwrapResponse { pub key: String, } -/// Decrypt the Data Encryption Key (DEK) and associated data. +/// Unwraps a wrapped Data Encryption Key (DEK) using the specified authentication and authorization tokens. /// /// See [doc](https://developers.google.com/workspace/cse/reference/wrap) and /// for more details, see [Encrypt & decrypt data](https://developers.google.com/workspace/cse/guides/encrypt-and-decrypt-data) +/// # Arguments +/// - `req_http`: The HTTP request. +/// - `unwrap_request`: The unwrap request. +/// - `cse_config`: The Google CSE configuration. +/// - `kms`: The KMS server. +/// +/// # Returns +/// - `UnwrapResponse`: The unwrapped key. +/// +/// # Errors +/// This function can return an error if there is a problem with the decryption process or if the tokens validation fails. pub async fn unwrap( req_http: HttpRequest, unwrap_request: UnwrapRequest, @@ -201,7 +227,7 @@ pub async fn unwrap( wrapped_dek.key_block_mut()?.key_wrapping_data = Some(Box::new(KeyWrappingData { wrapping_method: kmip_types::WrappingMethod::Encrypt, encryption_key_information: Some(kmip_types::EncryptionKeyInformation { - unique_identifier: UniqueIdentifier::TextString("[\"google_cse\"]".to_string()), + unique_identifier: UniqueIdentifier::TextString("[\"google_cse\"]".to_owned()), cryptographic_parameters: None, }), encoding_option: Some(EncodingOption::NoEncoding), @@ -263,6 +289,17 @@ pub struct PrivateKeySignResponse { /// See Google documentation: /// - Private Key Sign endpoint: /// - S/MIME certificate profiles: +/// # Arguments +/// - `req_http`: The HTTP request. +/// - `request`: The private key sign request. +/// - `cse_config`: The Google CSE configuration. +/// - `kms`: The KMS server. +/// +/// # Returns +/// - `PrivateKeySignResponse`: The signature. +/// +/// # Errors +/// This function can return an error if there is a problem with the encryption process or if the tokens validation fails. pub async fn private_key_sign( req_http: HttpRequest, request: PrivateKeySignRequest, @@ -351,6 +388,18 @@ pub struct PrivateKeyDecryptResponse { /// /// See Google documentation: /// - Private Key Decrypt endpoint: +/// +/// # Arguments +/// - `req_http`: The HTTP request. +/// - `request`: The private key decrypt request. +/// - `cse_config`: The Google CSE configuration. +/// - `kms`: The KMS server. +/// +/// # Returns +/// - `PrivateKeyDecryptResponse`: The decrypted data encryption key. +/// +/// # Errors +/// This function can return an error if there is a problem with the decryption process or if the tokens validation fails. pub async fn private_key_decrypt( req_http: HttpRequest, request: PrivateKeyDecryptRequest, @@ -443,7 +492,7 @@ async fn cse_symmetric_unwrap( KeyWrappingData { wrapping_method: kmip_types::WrappingMethod::Encrypt, encryption_key_information: Some(kmip_types::EncryptionKeyInformation { - unique_identifier: UniqueIdentifier::TextString("google_cse".to_string()), + unique_identifier: UniqueIdentifier::TextString("google_cse".to_owned()), cryptographic_parameters: None, }), encoding_option: Some(EncodingOption::TTLVEncoding), diff --git a/crate/server/src/routes/kmip.rs b/crate/server/src/routes/kmip.rs index 14d586db..dad4234a 100644 --- a/crate/server/src/routes/kmip.rs +++ b/crate/server/src/routes/kmip.rs @@ -17,7 +17,7 @@ use crate::{ result::KResult, }; -/// Generate KMIP generic key pair +/// Generate KMIP JSON TTLV and send it to the KMIP server #[post("/kmip/2_1")] pub(crate) async fn kmip( req_http: HttpRequest, diff --git a/crate/server/src/routes/mod.rs b/crate/server/src/routes/mod.rs index 97699c3a..97a5a145 100644 --- a/crate/server/src/routes/mod.rs +++ b/crate/server/src/routes/mod.rs @@ -77,5 +77,5 @@ pub(crate) async fn get_version( kms: Data>, ) -> KResult> { info!("GET /version {}", kms.get_user(&req)); - Ok(Json(crate_version!().to_string())) + Ok(Json(crate_version!().to_owned())) } diff --git a/crate/server/src/routes/ms_dke/mod.rs b/crate/server/src/routes/ms_dke/mod.rs index 5955067d..a508b50c 100644 --- a/crate/server/src/routes/ms_dke/mod.rs +++ b/crate/server/src/routes/ms_dke/mod.rs @@ -90,7 +90,7 @@ pub(crate) async fn version( kms: Data>, ) -> KResult> { info!("GET /version {}", kms.get_user(&req_http)); - Ok(Json(crate_version!().to_string())) + Ok(Json(crate_version!().to_owned())) } #[get("/{key_name}")] @@ -101,7 +101,7 @@ pub(crate) async fn get_key( ) -> HttpResponse { let mut key_name = path.into_inner(); if key_name.is_empty() { - key_name = "dke_key".to_string(); + "dke_key".clone_into(&mut key_name); } match _get_key(&key_name, req_http, &kms).await { Ok(key_data) => { @@ -148,7 +148,7 @@ async fn _get_key(key_tag: &str, req_http: HttpRequest, kms: &Arc) -> is not supported" ) })?; - let mut existing_path = dke_service_url.path().to_string(); + let mut existing_path = dke_service_url.path().to_owned(); // remove the trailing / if any if existing_path.ends_with('/') { existing_path.pop(); @@ -205,10 +205,7 @@ pub(crate) async fn decrypt( kms: Data>, ) -> HttpResponse { let encrypted_data = wrap_request.into_inner(); - info!( - "Encrypted Data : {}", - serde_json::to_string(&encrypted_data).unwrap() - ); + info!("Encrypted Data : {encrypted_data:?}",); let (key_name, key_id) = path.into_inner(); // let _key_id = key_id.into_inner(); trace!("POST /{}/{}/Decrypt {:?}", key_name, key_id, encrypted_data); @@ -255,13 +252,14 @@ fn big_uint_to_u32(bu: &BigUint) -> u32 { let bytes = bu.to_bytes_be(); let len = bytes.len(); let min = std::cmp::min(4, len); - let mut padded = [0u8; 4]; + let mut padded = [0_u8; 4]; padded[4 - min..].copy_from_slice(&bytes[len - min..]); u32::from_be_bytes(padded) } #[cfg(test)] mod tests { + #![allow(clippy::unwrap_used)] use chrono::{DateTime, Utc}; use num_bigint_dig::BigUint; diff --git a/crate/server/src/telemetry/mod.rs b/crate/server/src/telemetry/mod.rs index 2cc2a5ba..45673d0f 100644 --- a/crate/server/src/telemetry/mod.rs +++ b/crate/server/src/telemetry/mod.rs @@ -20,6 +20,14 @@ pub struct TelemetryConfig { } /// Initialize the telemetry system +/// +/// # Arguments +/// +/// * `clap_config` - The `ClapConfig` object containing the telemetry configuration +/// +/// # Errors +/// +/// Returns an error if there is an issue initializing the telemetry system. pub fn initialize_telemetry(clap_config: &ClapConfig) -> KResult<()> { let config = &clap_config.telemetry; let (filter, _reload_handle) = diff --git a/crate/server/src/tests/cover_crypt_tests/integration_tests.rs b/crate/server/src/tests/cover_crypt_tests/integration_tests.rs index ce44fbc5..667f93c5 100644 --- a/crate/server/src/tests/cover_crypt_tests/integration_tests.rs +++ b/crate/server/src/tests/cover_crypt_tests/integration_tests.rs @@ -76,7 +76,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), Some(header_metadata.clone()), Some(authentication_data.clone()), @@ -135,7 +135,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -224,7 +224,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { &app, &Revoke { unique_identifier: Some(UniqueIdentifier::TextString( - user_decryption_key_identifier_1.to_string(), + user_decryption_key_identifier_1.to_owned(), )), revocation_reason: RevocationReason::TextString("Revocation test".to_owned()), compromise_occurrence_date: None, @@ -234,7 +234,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { // // Rekey all key pairs with matching access policy - let ap_to_edit = "Department::MKG".to_string(); + let ap_to_edit = "Department::MKG".to_owned(); let request = build_rekey_keypair_request( private_key_unique_identifier, RekeyEditAction::RekeyAccessPolicy(ap_to_edit.clone()), @@ -262,7 +262,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -286,7 +286,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { None, ); let post_ttlv_decrypt: KResult = test_utils::post(&app, &request).await; - assert!(post_ttlv_decrypt.is_err()); + post_ttlv_decrypt.unwrap_err(); // decrypt let request = build_decryption_request( @@ -316,7 +316,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { )?; let rekey_keypair_response: KResult = test_utils::post(&app, &request).await; - assert!(rekey_keypair_response.is_ok()); + rekey_keypair_response.unwrap(); // test user2 can no longer decrypt old message let request = build_decryption_request( @@ -328,7 +328,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { None, ); let post_ttlv_decrypt: KResult = test_utils::post(&app, &request).await; - assert!(post_ttlv_decrypt.is_err()); + post_ttlv_decrypt.unwrap_err(); // // Add new Attributes @@ -348,7 +348,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { )?; let rekey_keypair_response: KResult = test_utils::post(&app, &request).await; - assert!(rekey_keypair_response.is_ok()); + rekey_keypair_response.unwrap(); // Encrypt for new attribute let data = "New tech research data".as_bytes(); @@ -356,7 +356,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -366,13 +366,13 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { }), )?; let encrypt_response: KResult = test_utils::post(&app, &request).await; - assert!(encrypt_response.is_ok()); + encrypt_response.unwrap(); // // Rename Attributes let rename_policy_attributes_pair = vec![( Attribute::from(("Department", "HR")), - "HumanResources".to_string(), + "HumanResources".to_owned(), )]; let request = build_rekey_keypair_request( private_key_unique_identifier, @@ -380,7 +380,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { )?; let rekey_keypair_response: KResult = test_utils::post(&app, &request).await; - assert!(rekey_keypair_response.is_ok()); + rekey_keypair_response.unwrap(); // Encrypt for renamed attribute let data = "hr data".as_bytes(); @@ -388,7 +388,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -398,7 +398,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { }), )?; let encrypt_response: KResult = test_utils::post(&app, &request).await; - assert!(encrypt_response.is_ok()); + encrypt_response.unwrap(); // // Disable ABE Attribute @@ -409,7 +409,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { )?; let rekey_keypair_response: KResult = test_utils::post(&app, &request).await; - assert!(rekey_keypair_response.is_ok()); + rekey_keypair_response.unwrap(); // Encrypt with disabled ABE attribute will fail let authentication_data = b"cc the uid".to_vec(); @@ -418,7 +418,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -428,7 +428,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { }), )?; let encrypt_response: KResult = test_utils::post(&app, &request).await; - assert!(encrypt_response.is_err()); + encrypt_response.unwrap_err(); // // Delete attribute @@ -439,7 +439,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { )?; let rekey_keypair_response: KResult = test_utils::post(&app, &request).await; - assert!(rekey_keypair_response.is_ok()); + rekey_keypair_response.unwrap(); // Encrypt for removed attribute will fail let data = "New hr data".as_bytes(); @@ -447,7 +447,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { let request = build_encryption_request( public_key_unique_identifier, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -457,7 +457,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { }), )?; let encrypt_response: KResult = test_utils::post(&app, &request).await; - assert!(encrypt_response.is_err()); + encrypt_response.unwrap_err(); // // Destroy user decryption key diff --git a/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs b/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs index a201bbf2..ee21cc87 100644 --- a/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs +++ b/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs @@ -47,7 +47,7 @@ async fn test_re_key_with_tags() -> KResult<()> { // Re_key all key pairs with matching access policy let request = build_rekey_keypair_request( &mkp_json_tag, - RekeyEditAction::RekeyAccessPolicy("Department::MKG".to_string()), + RekeyEditAction::RekeyAccessPolicy("Department::MKG".to_owned()), )?; let rekey_keypair_response: ReKeyKeyPairResponse = test_utils::post(&app, &request).await?; assert_eq!( @@ -65,7 +65,7 @@ async fn test_re_key_with_tags() -> KResult<()> { let encryption_policy = "Level::Confidential && Department::MKG"; let request = build_encryption_request( &mkp_json_tag, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -127,7 +127,7 @@ async fn integration_tests_with_tags() -> KResult<()> { let request = build_encryption_request( &mkp_json_tag, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), Some(header_metadata.clone()), Some(authentication_data.clone()), @@ -178,7 +178,7 @@ async fn integration_tests_with_tags() -> KResult<()> { let request = build_encryption_request( &mkp_json_tag, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -253,7 +253,7 @@ async fn integration_tests_with_tags() -> KResult<()> { let _revoke_response: RevokeResponse = test_utils::post( &app, &Revoke { - unique_identifier: Some(UniqueIdentifier::TextString(udk1_json_tag.to_string())), + unique_identifier: Some(UniqueIdentifier::TextString(udk1_json_tag.clone())), revocation_reason: RevocationReason::TextString("Revocation test".to_owned()), compromise_occurrence_date: None, }, @@ -264,7 +264,7 @@ async fn integration_tests_with_tags() -> KResult<()> { // Rekey all key pairs with matching access policy let request = build_rekey_keypair_request( &mkp_json_tag, - RekeyEditAction::RekeyAccessPolicy("Department::MKG".to_string()), + RekeyEditAction::RekeyAccessPolicy("Department::MKG".to_owned()), )?; let rekey_keypair_response: ReKeyKeyPairResponse = test_utils::post(&app, &request).await?; assert_eq!( @@ -282,7 +282,7 @@ async fn integration_tests_with_tags() -> KResult<()> { let encryption_policy = "Level::Confidential && Department::MKG"; let request = build_encryption_request( &mkp_json_tag, - Some(encryption_policy.to_string()), + Some(encryption_policy.to_owned()), data.to_vec(), None, Some(authentication_data.clone()), @@ -303,7 +303,7 @@ async fn integration_tests_with_tags() -> KResult<()> { None, ); let post_ttlv_decrypt: KResult = test_utils::post(&app, &request).await; - assert!(post_ttlv_decrypt.is_err()); + post_ttlv_decrypt.unwrap_err(); // decrypt let request = build_decryption_request( diff --git a/crate/server/src/tests/cover_crypt_tests/unit_tests.rs b/crate/server/src/tests/cover_crypt_tests/unit_tests.rs index 0d939122..39314b93 100644 --- a/crate/server/src/tests/cover_crypt_tests/unit_tests.rs +++ b/crate/server/src/tests/cover_crypt_tests/unit_tests.rs @@ -138,7 +138,7 @@ async fn test_cover_crypt_keys() -> KResult<()> { }, object: pk.clone(), }; - assert!(kms.import(request, owner, None).await.is_err()); + kms.import(request, owner, None).await.unwrap_err(); // re-import public key - should succeed let request = Import { @@ -312,7 +312,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { None, ) .await; - assert!(er.is_err()); + er.unwrap_err(); // encrypt a resource FIN + Secret let secret_authentication_data = b"cc the uid secret".to_vec(); @@ -355,7 +355,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { None, ) .await; - assert!(er.is_err()); + er.unwrap_err(); // Create a user decryption key MKG | FIN + secret let secret_mkg_fin_access_policy = "(Department::MKG || Department::FIN) && Level::secret"; @@ -416,7 +416,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { None, ) .await; - assert!(dr.is_err()); + dr.unwrap_err(); // decrypt resource FIN + Secret let dr = kms @@ -459,7 +459,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { None, ) .await; - assert!(dr.is_err()); + dr.unwrap_err(); Ok(()) } diff --git a/crate/server/src/tests/curve_25519_tests.rs b/crate/server/src/tests/curve_25519_tests.rs index 461e01f8..d1d5ddfd 100644 --- a/crate/server/src/tests/curve_25519_tests.rs +++ b/crate/server/src/tests/curve_25519_tests.rs @@ -10,6 +10,7 @@ use cosmian_kmip::{ CURVE_25519_Q_LENGTH_BITS, }, kmip::{ + extra::tagging::EMPTY_TAGS, kmip_messages::{Message, MessageBatchItem, MessageHeader}, kmip_objects::{Object, ObjectType}, kmip_operations::{ErrorReason, Import, Operation}, @@ -37,7 +38,7 @@ async fn test_curve_25519_key_pair() -> KResult<()> { let owner = "eyJhbGciOiJSUzI1Ni"; // request key pair creation - let request = create_ec_key_pair_request(&[] as &[&str], RecommendedCurve::CURVE25519)?; + let request = create_ec_key_pair_request(EMPTY_TAGS, RecommendedCurve::CURVE25519)?; let response = kms.create_key_pair(request, owner, None).await?; // check that the private and public key exist // check secret key @@ -84,14 +85,14 @@ async fn test_curve_25519_key_pair() -> KResult<()> { attributes .link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))? + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))? .len(), 1 ); let link = &attributes .link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))?[0]; + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))?[0]; assert_eq!(link.link_type, LinkType::PublicKeyLink); assert_eq!( link.linked_object_identifier, @@ -138,14 +139,14 @@ async fn test_curve_25519_key_pair() -> KResult<()> { attributes .link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))? + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))? .len(), 1 ); let link = &attributes .link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))?[0]; + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))?[0]; assert_eq!(link.link_type, LinkType::PrivateKeyLink); assert_eq!( link.linked_object_identifier, @@ -156,7 +157,7 @@ async fn test_curve_25519_key_pair() -> KResult<()> { assert_eq!(pk_bytes.len(), X25519_PUBLIC_KEY_LENGTH); let pk = to_ec_public_key( &pk_bytes, - CURVE_25519_Q_LENGTH_BITS as u32, + u32::try_from(CURVE_25519_Q_LENGTH_BITS)?, sk_uid, RecommendedCurve::CURVE25519, Some(CryptographicAlgorithm::ECDH), @@ -210,7 +211,7 @@ async fn test_curve_25519_multiple() -> KResult<()> { }, items: vec![ MessageBatchItem::new(Operation::CreateKeyPair(create_ec_key_pair_request( - &[] as &[&str], + EMPTY_TAGS, RecommendedCurve::CURVE25519, )?)), MessageBatchItem::new(Operation::Locate( @@ -234,19 +235,19 @@ async fn test_curve_25519_multiple() -> KResult<()> { }, items: vec![ MessageBatchItem::new(Operation::CreateKeyPair(create_ec_key_pair_request( - &[] as &[&str], + EMPTY_TAGS, RecommendedCurve::CURVE25519, )?)), MessageBatchItem::new(Operation::CreateKeyPair(create_ec_key_pair_request( - &[] as &[&str], + EMPTY_TAGS, RecommendedCurve::CURVEED25519, )?)), MessageBatchItem::new(Operation::CreateKeyPair(create_ec_key_pair_request( - &[] as &[&str], + EMPTY_TAGS, RecommendedCurve::SECP256K1, )?)), MessageBatchItem::new(Operation::CreateKeyPair(create_ec_key_pair_request( - &[] as &[&str], + EMPTY_TAGS, RecommendedCurve::CURVEED25519, )?)), ], @@ -294,7 +295,7 @@ async fn test_curve_25519_multiple() -> KResult<()> { response.items[2].result_message, Some( "Not Supported: Generation of Key Pair for curve: SECP256K1, is not supported" - .to_string() + .to_owned() ) ); diff --git a/crate/server/src/tests/google_cse/mod.rs b/crate/server/src/tests/google_cse/mod.rs index 66a75217..2d0d61cb 100644 --- a/crate/server/src/tests/google_cse/mod.rs +++ b/crate/server/src/tests/google_cse/mod.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unwrap_used, clippy::print_stdout, clippy::panic_in_result_fn)] + use std::{ fs::File, io::Read, @@ -70,7 +72,7 @@ fn import_google_cse_symmetric_key() -> Import { let object = read_object_from_json_ttlv_bytes(&symmetric_key).unwrap(); let request = Import { - unique_identifier: UniqueIdentifier::TextString("google_cse".to_string()), + unique_identifier: UniqueIdentifier::TextString("google_cse".to_owned()), object_type: object.object_type(), replace_existing: Some(false), key_wrap_type: None, @@ -140,7 +142,7 @@ async fn test_cse_private_key_sign() -> KResult<()> { let jwt = generate_google_jwt().await; - let app = test_utils::test_app(Some("http://127.0.0.1/".to_string())).await; + let app = test_utils::test_app(Some("http://127.0.0.1/".to_owned())).await; let import_request = import_google_cse_symmetric_key(); tracing::debug!("import_request created"); @@ -150,8 +152,8 @@ async fn test_cse_private_key_sign() -> KResult<()> { tracing::debug!("grant post"); let access = Access { - unique_identifier: Some(UniqueIdentifier::TextString("google_cse".to_string())), - user_id: "*".to_string(), + unique_identifier: Some(UniqueIdentifier::TextString("google_cse".to_owned())), + user_id: "*".to_owned(), operation_types: vec![ ObjectOperationType::Create, ObjectOperationType::Destroy, @@ -173,11 +175,11 @@ async fn test_cse_private_key_sign() -> KResult<()> { let pksr = PrivateKeySignRequest { authentication: jwt.clone(), authorization: jwt, - algorithm: "SHA256withRSA".to_string(), - digest: digest.to_string(), + algorithm: "SHA256withRSA".to_owned(), + digest: digest.to_owned(), rsa_pss_salt_length: None, - reason: "Gmail".to_string(), - wrapped_private_key: wrapped_private_key.to_string(), + reason: "Gmail".to_owned(), + wrapped_private_key: wrapped_private_key.to_owned(), }; tracing::debug!("private key sign request post"); @@ -215,7 +217,7 @@ async fn test_cse_private_key_decrypt() -> KResult<()> { let jwt = generate_google_jwt().await; - let app = test_utils::test_app(Some("http://127.0.0.1/".to_string())).await; + let app = test_utils::test_app(Some("http://127.0.0.1/".to_owned())).await; let path = std::env::current_dir()?; println!("The current directory is {}", path.display()); @@ -251,8 +253,8 @@ async fn test_cse_private_key_decrypt() -> KResult<()> { tracing::debug!("grant post"); let access = Access { - unique_identifier: Some(UniqueIdentifier::TextString("google_cse".to_string())), - user_id: "*".to_string(), + unique_identifier: Some(UniqueIdentifier::TextString("google_cse".to_owned())), + user_id: "*".to_owned(), operation_types: vec![ ObjectOperationType::Create, ObjectOperationType::Destroy, @@ -273,12 +275,12 @@ async fn test_cse_private_key_decrypt() -> KResult<()> { let request = PrivateKeyDecryptRequest { authentication: jwt.clone(), authorization: jwt, - algorithm: "RSA/ECB/PKCS1Padding".to_string(), + algorithm: "RSA/ECB/PKCS1Padding".to_owned(), encrypted_data_encryption_key: general_purpose::STANDARD .encode(encrypted_data_encryption_key), rsa_oaep_label: None, - reason: "Gmail".to_string(), - wrapped_private_key: wrapped_private_key.to_string(), + reason: "Gmail".to_owned(), + wrapped_private_key: wrapped_private_key.to_owned(), }; tracing::debug!("private key decrypt request post"); diff --git a/crate/server/src/tests/google_cse/utils.rs b/crate/server/src/tests/google_cse/utils.rs index 853986ca..bbadfa1c 100644 --- a/crate/server/src/tests/google_cse/utils.rs +++ b/crate/server/src/tests/google_cse/utils.rs @@ -57,7 +57,7 @@ pub(crate) async fn google_cse_auth() -> KResult { let jwks_manager = Arc::new(JwksManager::new(uris).await?); let jwt_config = JwtConfig { - jwt_issuer_uri: GOOGLE_JWT_ISSUER_URI.to_string(), + jwt_issuer_uri: GOOGLE_JWT_ISSUER_URI.to_owned(), jwks: jwks_manager.clone(), jwt_audience: None, }; @@ -65,6 +65,6 @@ pub(crate) async fn google_cse_auth() -> KResult { Ok(GoogleCseConfig { authentication: vec![jwt_config].into(), authorization: google_cse::jwt_authorization_config(&jwks_manager), - kacls_url: "http://0.0.0.0:9998/google_cse".to_string(), + kacls_url: "http://0.0.0.0:9998/google_cse".to_owned(), }) } diff --git a/crate/server/src/tests/kmip_messages.rs b/crate/server/src/tests/kmip_messages.rs index 02c77797..22b89baf 100644 --- a/crate/server/src/tests/kmip_messages.rs +++ b/crate/server/src/tests/kmip_messages.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use cosmian_kmip::{ crypto::elliptic_curves::kmip_requests::create_ec_key_pair_request, kmip::{ + extra::tagging::EMPTY_TAGS, kmip_messages::{Message, MessageBatchItem, MessageHeader}, kmip_operations::{Decrypt, ErrorReason, Locate, Operation}, kmip_types::{ @@ -26,15 +27,14 @@ async fn test_kmip_messages() -> KResult<()> { let owner = "eyJhbGciOiJSUzI1Ni"; // request key pair creation - let ec_create_request = - create_ec_key_pair_request(&[] as &[&str], RecommendedCurve::CURVE25519)?; + let ec_create_request = create_ec_key_pair_request(EMPTY_TAGS, RecommendedCurve::CURVE25519)?; // prepare and send the single message let items = vec![ MessageBatchItem::new(Operation::CreateKeyPair(ec_create_request)), MessageBatchItem::new(Operation::Locate(Locate::default())), MessageBatchItem::new(Operation::Decrypt(Decrypt { - unique_identifier: Some(UniqueIdentifier::TextString("id_12345".to_string())), + unique_identifier: Some(UniqueIdentifier::TextString("id_12345".to_owned())), data: Some(b"decrypted_data".to_vec()), ..Default::default() })), @@ -112,7 +112,7 @@ async fn test_kmip_messages() -> KResult<()> { Some( "Get Key: no available key found (must be an active symmetric key or private key) for \ object identifier id_12345" - .to_string() + .to_owned() ) ); assert!(response.items[2].response_payload.is_none()); diff --git a/crate/server/src/tests/kmip_server_tests.rs b/crate/server/src/tests/kmip_server_tests.rs index cc27d746..9cd0938d 100644 --- a/crate/server/src/tests/kmip_server_tests.rs +++ b/crate/server/src/tests/kmip_server_tests.rs @@ -13,6 +13,7 @@ use cosmian_kmip::{ symmetric::symmetric_key_create_request, }, kmip::{ + extra::tagging::EMPTY_TAGS, kmip_data_structures::{KeyBlock, KeyMaterial, KeyValue, KeyWrappingData}, kmip_objects::{Object, ObjectType}, kmip_operations::{Get, Import}, @@ -42,7 +43,7 @@ async fn test_curve_25519_key_pair() -> KResult<()> { let owner = "eyJhbGciOiJSUzI1Ni"; // request key pair creation - let request = create_ec_key_pair_request(&[] as &[&str], RecommendedCurve::CURVE25519)?; + let request = create_ec_key_pair_request(EMPTY_TAGS, RecommendedCurve::CURVE25519)?; let response = kms.create_key_pair(request, owner, None).await?; // check that the private and public key exist // check secret key @@ -88,14 +89,14 @@ async fn test_curve_25519_key_pair() -> KResult<()> { assert_eq!( attr.link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))? + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))? .len(), 1 ); let link = &attr .link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))?[0]; + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))?[0]; assert_eq!(link.link_type, LinkType::PublicKeyLink); assert_eq!( link.linked_object_identifier, @@ -141,14 +142,14 @@ async fn test_curve_25519_key_pair() -> KResult<()> { assert_eq!( attr.link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))? + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))? .len(), 1 ); let link = &attr .link .as_ref() - .ok_or_else(|| KmsError::ServerError("links should not be empty".to_string()))?[0]; + .ok_or_else(|| KmsError::ServerError("links should not be empty".to_owned()))?[0]; assert_eq!(link.link_type, LinkType::PrivateKeyLink); assert_eq!( link.linked_object_identifier, @@ -159,7 +160,7 @@ async fn test_curve_25519_key_pair() -> KResult<()> { assert_eq!(pk_bytes.len(), X25519_PUBLIC_KEY_LENGTH); let pk = to_ec_public_key( &pk_bytes, - CURVE_25519_Q_LENGTH_BITS as u32, + u32::try_from(CURVE_25519_Q_LENGTH_BITS)?, sk_uid, RecommendedCurve::CURVE25519, Some(CryptographicAlgorithm::ECDH), @@ -218,7 +219,7 @@ async fn test_import_wrapped_symmetric_key() -> KResult<()> { attributes: None, }, cryptographic_algorithm: Some(CryptographicAlgorithm::AES), - cryptographic_length: Some(wrapped_symmetric_key.len() as i32 * 8), + cryptographic_length: Some(i32::try_from(wrapped_symmetric_key.len())? * 8), key_wrapping_data: Some(Box::new(KeyWrappingData { wrapping_method: WrappingMethod::Encrypt, iv_counter_nonce: Some(aesgcm_nonce.to_vec()), @@ -237,7 +238,7 @@ async fn test_import_wrapped_symmetric_key() -> KResult<()> { attributes: Attributes { object_type: Some(ObjectType::SymmetricKey), cryptographic_algorithm: Some(CryptographicAlgorithm::AES), - cryptographic_length: Some(wrapped_symmetric_key.len() as i32), + cryptographic_length: Some(i32::try_from(wrapped_symmetric_key.len())?), key_format_type: Some(KeyFormatType::TransparentSymmetricKey), ..Attributes::default() }, @@ -261,7 +262,7 @@ async fn test_create_transparent_symmetric_key() -> KResult<()> { let owner = "eyJhbGciOiJSUzI1Ni"; let request = - symmetric_key_create_request(256, CryptographicAlgorithm::AES, &[] as &[&str]).unwrap(); + symmetric_key_create_request(256, CryptographicAlgorithm::AES, EMPTY_TAGS).unwrap(); trace!("request: {:?}", request); let response = kms.create(request, owner, None).await?; @@ -305,7 +306,7 @@ async fn test_database_user_tenant() -> KResult<()> { let owner = "eyJhbGciOiJSUzI1Ni"; // request key pair creation - let request = create_ec_key_pair_request(&[] as &[&str], RecommendedCurve::CURVE25519)?; + let request = create_ec_key_pair_request(EMPTY_TAGS, RecommendedCurve::CURVE25519)?; let response = kms.create_key_pair(request, owner, None).await?; // check that we can get the private and public key @@ -350,7 +351,7 @@ async fn test_database_user_tenant() -> KResult<()> { None, ) .await; - assert!(sk_response.is_err()); + sk_response.unwrap_err(); let pk_response = kms .get( @@ -364,7 +365,7 @@ async fn test_database_user_tenant() -> KResult<()> { None, ) .await; - assert!(pk_response.is_err()); + pk_response.unwrap_err(); Ok(()) } diff --git a/crate/server/src/tests/migrate/kms_4.12.0.sqlite b/crate/server/src/tests/migrate/kms_4.12.0.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..e2819f8fc33ff9eaf73e289a2a9f0204cf0532c6 GIT binary patch literal 98304 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 Y*AqGW$G~g+|DFKAG8yJ?dj9|a12QG?&j0`b literal 0 HcmV?d00001 diff --git a/crate/server/src/tests/migrate/kms_4.16.0.sqlite b/crate/server/src/tests/migrate/kms_4.16.0.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..3e92b7e98ec34ef611f11643b6707e6a22a0d372 GIT binary patch literal 86016 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 Wdfyn$d*u!v)FR`N!GHf4&Ho1+OB5mi literal 0 HcmV?d00001 diff --git a/crate/server/src/tests/migrate/kms_4.17.0.sqlite b/crate/server/src/tests/migrate/kms_4.17.0.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..60138905d2e196bbe0458cc63748ced769accbba GIT binary patch literal 122880 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 b+%Eu}) -> Cl ClapConfig { http: HttpConfig { https_p12_file: Some(PathBuf::from("src/tests/kmserver.acme.com.p12")), - https_p12_password: Some("password".to_string()), + https_p12_password: Some("password".to_owned()), ..Default::default() }, db: DBConfig { - database_type: Some("sqlite".to_string()), + database_type: Some("sqlite".to_owned()), database_url: None, sqlite_path, clear_database: true, @@ -104,7 +106,7 @@ where if res.status() != StatusCode::OK { kms_bail!( "{}", - String::from_utf8(read_body(res).await.to_vec()).unwrap_or_else(|_| "[N/A".to_string()) + String::from_utf8(read_body(res).await.to_vec()).unwrap_or_else(|_| "[N/A".to_owned()) ); } let body = read_body(res).await; @@ -130,7 +132,7 @@ where if res.status() != StatusCode::OK { kms_bail!( "{}", - String::from_utf8(read_body(res).await.to_vec()).unwrap_or_else(|_| "[N/A".to_string()) + String::from_utf8(read_body(res).await.to_vec()).unwrap_or_else(|_| "[N/A".to_owned()) ); } println!("OK before bytes"); diff --git a/crate/server/src/tests/test_validate.rs b/crate/server/src/tests/test_validate.rs index 8d0177e3..cdf65c4e 100644 --- a/crate/server/src/tests/test_validate.rs +++ b/crate/server/src/tests/test_validate.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unwrap_used)] + use std::{fs, path, sync::Arc}; use cosmian_kmip::kmip::{ @@ -55,7 +57,7 @@ pub(crate) async fn test_validate_with_certificates_bytes() -> Result<(), KmsErr validity_time: None, }; let res = kms.validate(request, owner, None).await; - assert!(res.is_err()); + res.unwrap_err(); debug!("OK: Validate root/intermediate/leaf1 certificates - invalid (revoked)"); let request = Validate { certificate: Some( @@ -82,18 +84,18 @@ pub(crate) async fn test_validate_with_certificates_bytes() -> Result<(), KmsErr .to_vec(), ), unique_identifier: None, - validity_time: //Some(Asn1Time::days_from_now(3651).unwrap().to_string()), // this is supposed to work but it does not. - Some("4804152030Z".to_string()) + validity_time: //Some(Asn1Time::days_from_now(3651).unwrap().to_owned()), // this is supposed to work but it does not. + Some("4804152030Z".to_owned()) }; let res = kms.validate(request, owner, None).await; - assert!(res.is_err()); + res.unwrap_err(); debug!("OK: Validate root/intermediate/leaf2 certificates - invalid"); let request = Validate { certificate: Some([leaf2_cert.clone(), root_cert.clone()].to_vec()), unique_identifier: None, validity_time: None, }; - assert!(kms.validate(request, owner, None).await.is_err()); + kms.validate(request, owner, None).await.unwrap_err(); debug!("OK: Validate root/leaf2 certificates - missing intermediate"); Result::Ok(()) @@ -198,7 +200,7 @@ pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError validity_time: None, }; let res = kms.validate(request, owner, None).await; - assert!(res.is_err()); + res.unwrap_err(); debug!("OK: Validate root/intermediate/leaf1 certificates - invalid (revoked)"); // No certificate in chain @@ -208,7 +210,7 @@ pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError validity_time: None, }; let res = kms.validate(request, owner, None).await; - assert!(res.is_err()); + res.unwrap_err(); // Root and intermediate valid certificates. Leaf valid. Test returns valid. let request = Validate { @@ -247,11 +249,11 @@ pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError res_root.unique_identifier.clone(), ], ), - validity_time: //Some(Asn1Time::days_from_now(3651).unwrap().to_string()), // this is supposed to work but it does not. - Some("4804152030Z".to_string()) + validity_time: //Some(Asn1Time::days_from_now(3651).unwrap().to_owned()), // this is supposed to work but it does not. + Some("4804152030Z".to_owned()) }; let res = kms.validate(request, owner, None).await; - assert!(res.is_err()); + res.unwrap_err(); debug!( "OK: Validate root/intermediate/leaf2 certificates - invalid (won't be valid in the \ future)" @@ -263,7 +265,7 @@ pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError unique_identifier: Some(vec![res_root.unique_identifier.clone()]), validity_time: None, }; - assert!(kms.validate(request, owner, None).await.is_err()); + kms.validate(request, owner, None).await.unwrap_err(); debug!("OK: Validate root/leaf2 certificates - invalid (missing intermediate)"); // Root certificate not provided. Intermediate and leaf are valid certificates. Return is Invalid. @@ -272,7 +274,7 @@ pub(crate) async fn test_validate_with_certificates_ids() -> Result<(), KmsError unique_identifier: Some([res_intermediate.unique_identifier.clone()].to_vec()), validity_time: None, }; - assert!(kms.validate(request, owner, None).await.is_err()); + kms.validate(request, owner, None).await.unwrap_err(); debug!("OK: Validate root/leaf2 certificates - invalid (missing root)"); Result::Ok(()) diff --git a/docker-compose.yml b/docker-compose.yml index 75459e0c..54f58464 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: - POSTGRES_DB=kms - POSTGRES_PASSWORD=kms - PGDATA=/tmp/postgres2 - maria: + mariadb: image: mariadb ports: - 3306:3306 diff --git a/documentation/docs/cli/main_commands.md b/documentation/docs/cli/main_commands.md index f33e4d50..85bf4daf 100644 --- a/documentation/docs/cli/main_commands.md +++ b/documentation/docs/cli/main_commands.md @@ -571,7 +571,7 @@ Encrypt a file using Covercrypt ### Arguments ` ` The files to encrypt -` ` The encryption policy to encrypt the file with Example: "department::marketing && level::confidential"` +` ` The encryption policy to encrypt the file with Example: "`department::marketing` && `level::confidential`" `--key-id [-k] ` The public key unique identifier. If not specified, tags should be specified diff --git a/documentation/docs/database.md b/documentation/docs/database.md new file mode 100644 index 00000000..34830887 --- /dev/null +++ b/documentation/docs/database.md @@ -0,0 +1,120 @@ +# Database + +## Selecting the database + +The KMS server has support for PostgreSQL, Maria DB, and MySQL, as well as Redis, using the Redis-with-Findex configuration. + +Redis with Findex offers the ability to use Redis as a database with application-level encryption: all data is encrypted (using AES 256 GCM) by the KMS servers before being sent to Redis. [Findex](https://github.com/Cosmian/findex/) is a Cosmian cryptographic algorithm used to build encrypted indexes on encrypted data, also stored in Redis. This allows the KMS to perform fast encrypted queries on encrypted data. Redis with Findex offers post-quantum resistance on encrypted data and encrypted indexes. + +Redis-with-Findex is most useful when: + +- KMS servers are run inside a confidential VM or an enclave. In this case, the secret used to encrypt the Redis data and indexes is protected by the VM or enclave and cannot be recovered at runtime by inspecting the KMS servers' memory. +- KMS servers are run by a trusted party but the Redis backend is managed by an untrusted third party. + +Redis-with-Findex should be selected to [run the Cosmian KMS in the cloud or any other zero-trust environment](./zero_trust.md). + +## Configuring the database + +The database parameters may be configured either: + +- using options on the command line that is used to start the KMS server + +### Configuring the database via the command line + +For + +- PostgreSQL, use: + +```sh +docker run --rm -p 9998:9998 \ + --name kms ghcr.io/cosmian/kms:4.17.0 \ + --database-type=postgresql \ + --database-url=postgres://kms_user:kms_password@pgsql-server:5432/kms +``` + +- MySQL or MariaDB, use: + +```sh +docker run --rm -p 9998:9998 \ + --name kms ghcr.io/cosmian/kms:4.17.0 \ + --database-type=mysql \ + --database-url=mysql://kms_user:kms_password@mariadb:3306/kms +``` + +- Redis (with-Findex), use: + +For Redis with Findex, the `--redis-master-password` and `--redis-findex-label` options must also be specified: + +- the `redis-master-password` is the password from which keys will be derived (using Argon 2) to encrypt the Redis data and indexes. +- the `redis-findex-label` is a public arbitrary label that can be changed to rotate the Findex ciphertexts without changing the password/key. + +```sh +docker run --rm -p 9998:9998 \ + --name kms ghcr.io/cosmian/kms:4.17.0 \ + --database-type=redis-findex \ + --database-url=redis://localhost:6379 \ + --redis-master-password password \ + --redis-findex-label label +``` + +The `redis-master-password` is the password from which a key will be derived (using Argon 2) to encrypt the Redis data and indexes. + +The `redis-findex-label` is a public arbitrary label that can be changed to rotate the Findex ciphertexts without changing the password/key. + +!!!info "Setting up a PostgreSQL database" + Before running the server, a dedicated database with a dedicated user should be created on the PostgreSQL instance. These sample instructions create a database called `kms` owned by a user `kms_user` with password `kms_password`: + + 1. Connect to psql under user `postgres` + ```sh + sudo -u postgres psql # or `psql -U postgres` + ``` + + 2. Create user `kms_user` with password `kms_password` + ```psql + create user kms_user with encrypted password 'kms_password'; + ``` + + 3. Create database `kms` under owner `kms_user` + ```psql + create database kms owner=kms_user; + ``` + +## Using a certificate to authenticate to MySQL or Maria DB + +Use a certificate to authenticate to MySQL or Maria DB with the `--mysql-user-cert-file` option on the command line. Specify the certificate file name and mount the file to docker. + +Say the certificate is called `cert.p12` and is in a directory called `/certificate` on the host disk. + +```sh +docker run --rm -p 9998:9998 \ + --name kms ghcr.io/cosmian/kms:4.17.0 \ + -v /certificate/cert.p12:/root/cosmian-kms/cert.p12 \ + --database-type=mysql \ + --database-url=mysql://mysql_server:3306/kms \ + --mysql-user-cert-file=cert.p12 +``` + +## Database migration + +Depending on the KMS database evolution, a migration can happen between 2 versions of the KMS server. It will be clearly written in the CHANGELOG.md. In that case, a generic database upgrade mechanism is run on startup. + +At first, the table `context` is responsible for storing the version of the software run and the state of the database. The state can be one of the following: + +- `ready`: the database is ready to be used +- `upgrading`: the database is being upgraded + +On server startup: + +- the server checks if the software version is greater than the last version run: + + - if no, it simply starts; + - if yes: + + - it looks for all upgrades to apply in order from the last version run to this version; + - if there is any to run, it sets an upgrading flag on the db state field in the context table; + - it runs all the upgrades in order; + - it sets the flag from upgrading to ready; + +On every call to the database, a check is performed on the db state field to check if the database is upgrading. If yes, calls fail. + +Upgrades resist to being interrupted in the middle and resumed from start if that happens. diff --git a/documentation/docs/high_availability_mode.md b/documentation/docs/high_availability_mode.md index d7f23d84..ce212492 100644 --- a/documentation/docs/high_availability_mode.md +++ b/documentation/docs/high_availability_mode.md @@ -13,94 +13,3 @@ When the Cosmian KMS servers are configured to export an HTTPS port (as is the c - all the Cosmian KMS servers should expose the same server certificate on their HTTPS port - and the load balancer should be configured as an SSL load balancer (HAProxy is a good example of a high-performance SSL load balancer) -### Selecting the database - -The KMS server has support for PostgreSQL, Maria DB, and MySQL, as well as Redis, using the Redis-with-Findex configuration. - -Redis with Findex offers the ability to use Redis as a database with application-level encryption: all data is encrypted (using AES 256 GCM) by the KMS servers before being sent to Redis. [Findex](https://github.com/Cosmian/findex/) is a Cosmian cryptographic algorithm used to build encrypted indexes on encrypted data, also stored in Redis. This allows the KMS to perform fast encrypted queries on encrypted data. Redis with Findex offers post-quantum resistance on encrypted data and encrypted indexes. - -Redis-with-Findex is most useful when: - -- KMS servers are run inside a confidential VM or an enclave. In this case, the secret used to encrypt the Redis data and indexes is protected by the VM or enclave and cannot be recovered at runtime by inspecting the KMS servers' memory. -- KMS servers are run by a trusted party but the Redis backend is managed by an untrusted third party. - -Redis-with-Findex should be selected to [run the Cosmian KMS in the cloud or any other zero-trust environment](./zero_trust.md). - -### Configuring the database - -The database parameters may be configured either: - -- using options on the command line that is used to start the KMS server - -#### Configuring the database via the command line - -For - -- PostgreSQL, use `--database-type=postgresql` -- MySQL or MariaDB, use `--database-type=mysql` -- Redis (with-Findex), use `--database-type=redis-findex` - -and specify the database URL with the `--database-url` option. - -e.g. - -```sh -docker run --rm -p 9998:9998 \ - --name kms ghcr.io/cosmian/kms:4.17.0 \ - --database-type=postgresql \ - --database-url=postgres://kms_user:kms_password@pgsql-server:5432/kms - -``` - -For Redis with Findex, the `--redis-master-password` and `--redis-findex-label` options must also be specified: - -- the `redis-master-password` is the password from which keys will be derived (using Argon 2) to encrypt the Redis data and indexes. -- the `redis-findex-label` is a public arbitrary label that can be changed to rotate the Findex ciphertexts without changing the password/key. - -Example: - -```sh -docker run --rm -p 9998:9998 \ - --name kms ghcr.io/cosmian/kms:4.17.0 \ - --database-type=redis-findex \ - --database-url=redis://localhost:6379 \ - --redis-master-password password \ - --redis-findex-label label -``` - -The `redis-master-password` is the password from which a key will be derived (using Argon 2) to encrypt the Redis data and indexes. - -The `redis-findex-label` is a public arbitrary label that can be changed to rotate the Findex ciphertexts without changing the password/key. - -!!!info "Setting up a PostgreSQL database" - Before running the server, a dedicated database with a dedicated user should be created on the PostgreSQL instance. These sample instructions create a database called `kms` owned by a user `kms_user` with password `kms_password`: - - 1. Connect to psql under user `postgres` - ```sh - sudo -u postgres psql # or `psql -U postgres` - ``` - - 2. Create user `kms_user` with password `kms_password` - ```psql - create user kms_user with encrypted password 'kms_password'; - ``` - - 3. Create database `kms` under owner `kms_user` - ```psql - create database kms owner=kms_user; - ``` - -### Using a certificate to authenticate to MySQL or Maria DB - -Use a certificate to authenticate to MySQL or Maria DB with the `--mysql-user-cert-file` option on the command line. Specify the certificate file name and mount the file to docker. - -Say the certificate is called `cert.p12` and is in a directory called `/certificate` on the host disk. - -```sh -docker run --rm -p 9998:9998 \ - --name kms ghcr.io/cosmian/kms:4.17.0 \ - -v /certificate/cert.p12:/root/cosmian-kms/cert.p12 \ - --database-type=mysql \ - --database-url=mysql://mysql_server:3306/kms \ - --mysql-user-cert-file=cert.p12 -``` diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 0a6e2fc6..319c5c0a 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -52,6 +52,7 @@ nav: - Cryptographic algorithms: algorithms.md - Enabling TLS: tls.md - Logging and telemetry: logging.md + - Configuring database: database.md - Deploying in single server mode: single_server_mode.md - Deploying for high-availability: high_availability_mode.md - Running in the cloud or any zero-trust environment: zero_trust.md