diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3797039..69ca600 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,11 +15,11 @@ jobs: cargo-lint: uses: ./.github/workflows/clippy.yml with: - toolchain: stable-2024-10-17 + toolchain: nightly-2024-06-09 build_tests: uses: ./.github/workflows/build_all.yml secrets: inherit with: - toolchain: stable-2024-10-17 + toolchain: nightly-2024-06-09 debug_or_release: debug diff --git a/.github/workflows/main_release.yml b/.github/workflows/main_release.yml index 41818bf..30a1c58 100644 --- a/.github/workflows/main_release.yml +++ b/.github/workflows/main_release.yml @@ -22,11 +22,11 @@ jobs: cargo-lint: uses: ./.github/workflows/clippy.yml with: - toolchain: stable-2024-10-17 + toolchain: nightly-2024-06-09 build: uses: ./.github/workflows/build_all.yml secrets: inherit with: - toolchain: stable-2024-10-17 + toolchain: nightly-2024-06-09 debug_or_release: release diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c571df5..80cf1b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -121,7 +121,7 @@ repos: args: [--skip-string-normalization] - repo: https://github.com/Cosmian/git-hooks.git - rev: v1.0.31 + rev: v1.0.32 hooks: # - id: nightly-cargo-format - id: cargo-format @@ -130,7 +130,7 @@ repos: # - id: cargo-update - id: cargo-machete - id: docker-compose-up - - id: cargo-build-kms + - id: cargo-build-all - id: cargo-test - id: clippy-autofix-unreachable-pub - id: clippy-autofix-all-targets-all-features diff --git a/Cargo.lock b/Cargo.lock index f7fbf28..0953fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,10 +48,12 @@ dependencies = [ "ahash 0.8.11", "base64 0.22.1", "bitflags 2.6.0", + "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", + "flate2", "futures-core", "h2", "http", @@ -69,6 +71,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "zstd", ] [[package]] @@ -106,6 +109,7 @@ dependencies = [ "bytestring", "cfg-if", "http", + "regex", "regex-lite", "serde", "tracing", @@ -228,6 +232,7 @@ dependencies = [ "mime", "once_cell", "pin-project-lite", + "regex", "regex-lite", "serde", "serde_json", @@ -347,6 +352,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -542,6 +562,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.10.0" @@ -592,6 +633,8 @@ version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -812,18 +855,17 @@ version = "0.1.0" dependencies = [ "actix-rt", "actix-server", - "actix-web", "assert_cmd", "base64 0.21.7", "clap", "cloudproof_findex", "const-oid", + "cosmian_findex_client", + "cosmian_findex_structs", "cosmian_logger", - "cosmian_rest_client", "csv", "der", "hex", - "oauth2", "openssl", "pem", "predicates", @@ -841,6 +883,38 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "cosmian_findex_client" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "cosmian_findex_config", + "cosmian_findex_structs", + "cosmian_http_client", + "der", + "pem", + "reqwest", + "serde", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "cosmian_findex_config" +version = "0.1.0" +dependencies = [ + "base64 0.21.7", + "cosmian_http_client", + "cosmian_logger", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", +] + [[package]] name = "cosmian_findex_server" version = "0.1.0" @@ -856,6 +930,7 @@ dependencies = [ "chrono", "clap", "cloudproof_findex", + "cosmian_findex_structs", "cosmian_logger", "dotenvy", "futures", @@ -875,30 +950,41 @@ dependencies = [ ] [[package]] -name = "cosmian_logger" +name = "cosmian_findex_structs" version = "0.1.0" dependencies = [ - "tracing", - "tracing-subscriber", + "base64 0.21.7", + "cloudproof_findex", + "thiserror", + "url", + "uuid", ] [[package]] -name = "cosmian_rest_client" +name = "cosmian_http_client" version = "0.1.0" +source = "git+https://www.github.com/Cosmian/http_client_server?branch=add_http_client#49d4de89865f23d14bc1948d6db48fd690239eec" dependencies = [ - "base64 0.21.7", - "clap", - "der", - "faker_rand", - "pem", - "rand", + "actix-web", + "oauth2", "reqwest", + "rustls", "serde", "serde_json", "thiserror", - "tracing", + "tokio", "url", - "uuid", + "webpki-roots 0.22.6", + "x509-cert", +] + +[[package]] +name = "cosmian_logger" +version = "0.1.0" +source = "git+https://www.github.com/Cosmian/http_client_server?branch=add_http_client#49d4de89865f23d14bc1948d6db48fd690239eec" +dependencies = [ + "tracing", + "tracing-subscriber", ] [[package]] @@ -925,6 +1011,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.5.1" @@ -1036,6 +1131,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "der_derive", + "flagset", "pem-rfc7468", "zeroize", ] @@ -1054,6 +1151,17 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1076,12 +1184,6 @@ dependencies = [ "syn", ] -[[package]] -name = "deunicode" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" - [[package]] name = "difflib" version = "0.4.0" @@ -1179,23 +1281,28 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "faker_rand" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300d2ddbf2245b5b5e723995e0961033121b4fc2be9045fb661af82bd739ffb6" -dependencies = [ - "deunicode", - "lazy_static", - "rand", -] - [[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -1665,6 +1772,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.72" @@ -2382,7 +2498,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg", ] @@ -3035,10 +3151,12 @@ name = "test_findex_server" version = "0.1.0" dependencies = [ "actix-server", + "cosmian_findex_client", "cosmian_findex_server", "cosmian_logger", - "cosmian_rest_client", "criterion", + "serde", + "serde_json", "tokio", "tracing", "zeroize", @@ -3139,6 +3257,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.41.0" @@ -3553,6 +3692,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -3776,6 +3934,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + [[package]] name = "x509-parser" version = "0.16.0" @@ -3834,3 +4004,31 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 2293817..74cb1e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,9 @@ [workspace] default-members = ["crate/server", "crate/cli"] members = [ + "crate/structs", "crate/server", - "crate/logger", + "crate/config", "crate/client", "crate/cli", "crate/test_server", @@ -12,7 +13,7 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" -rust-version = "1.82.0" +rust-version = "1.71.0" authors = [ "Emmanuel Coste", "Hatem M'naouer ", @@ -45,6 +46,9 @@ actix-server = { version = "2.5", default-features = false } actix-web = { version = "4.9.0", default-features = false } base64 = "0.21" clap = { version = "4.5", default-features = false } +cosmian_http_client = { git = "https://www.github.com/Cosmian/http_client_server", branch = "add_http_client" } +# cosmian_http_client = { path ="../../core/client_server/crate/http_client" } +cosmian_logger = { git = "https://www.github.com/Cosmian/http_client_server", branch = "add_http_client" } # cloudproof_findex = { path = "../cloudproof_rust/crates/findex" } cloudproof_findex = { git = "https://www.github.com/Cosmian/cloudproof_rust", branch = "feat/add_basic_findex_rest_client" } der = { version = "0.7", default-features = false } diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml index ab318ca..83a1643 100644 --- a/crate/cli/Cargo.toml +++ b/crate/cli/Cargo.toml @@ -21,7 +21,6 @@ doctest = false [features] [dependencies] -actix-web = { workspace = true, features = ["macros"] } base64 = { workspace = true } clap = { workspace = true, features = [ "help", @@ -33,12 +32,12 @@ clap = { workspace = true, features = [ "cargo", ] } cloudproof_findex = { workspace = true, features = ["rest-interface"] } -cosmian_logger = { path = "../logger" } -cosmian_rest_client = { path = "../client" } +cosmian_logger = { workspace = true } +cosmian_findex_client = { path = "../client" } +cosmian_findex_structs = { path = "../structs" } csv = "1.3.0" der = { workspace = true, features = ["pem"] } hex = "0.4" -oauth2 = { version = "4.4", features = ["reqwest"] } pem = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crate/cli/src/actions/findex/add_or_delete.rs b/crate/cli/src/actions/findex/add_or_delete.rs index 2bc6649..b59cc97 100644 --- a/crate/cli/src/actions/findex/add_or_delete.rs +++ b/crate/cli/src/actions/findex/add_or_delete.rs @@ -8,7 +8,7 @@ use clap::Parser; use cloudproof_findex::reexport::cosmian_findex::{ Data, IndexedValue, IndexedValueToKeywordsMap, Keyword, }; -use cosmian_rest_client::RestClient; +use cosmian_findex_client::FindexClient; use tracing::{instrument, trace}; use super::FindexParameters; @@ -72,7 +72,7 @@ impl AddOrDeleteAction { /// - There is an error converting the CSV file to a hashmap. /// - There is an error adding the data to the Findex index. /// - There is an error writing the result to the console. - pub async fn add(&self, rest_client: RestClient) -> CliResult<()> { + pub async fn add(&self, rest_client: FindexClient) -> CliResult<()> { let keywords = instantiate_findex(rest_client, &self.findex_parameters.index_id) .await? .add( @@ -100,7 +100,7 @@ impl AddOrDeleteAction { /// - There is an error converting the CSV file to a hashmap. /// - There is an error deleting the data from the Findex index. /// - There is an error writing the result to the console. - pub async fn delete(&self, rest_client: RestClient) -> CliResult<()> { + pub async fn delete(&self, rest_client: FindexClient) -> CliResult<()> { let keywords = instantiate_findex(rest_client, &self.findex_parameters.index_id) .await? .delete( diff --git a/crate/cli/src/actions/findex/mod.rs b/crate/cli/src/actions/findex/mod.rs index 30e07a9..7dbc7fc 100644 --- a/crate/cli/src/actions/findex/mod.rs +++ b/crate/cli/src/actions/findex/mod.rs @@ -6,7 +6,7 @@ use cloudproof_findex::{ }, Configuration, InstantiatedFindex, }; -use cosmian_rest_client::RestClient; +use cosmian_findex_client::FindexClient; use tracing::debug; use uuid::Uuid; @@ -17,7 +17,7 @@ pub mod search; #[derive(Parser, Debug)] #[clap(verbatim_doc_comment)] -pub(crate) struct FindexParameters { +pub struct FindexParameters { /// The user findex key used (to add, search, delete and compact). /// The key is a 16 bytes hex string. #[clap(long, short = 'k')] @@ -31,24 +31,32 @@ pub(crate) struct FindexParameters { } impl FindexParameters { - pub(crate) fn user_key(&self) -> CliResult { + /// Returns the user key decoded from hex. + /// # Errors + /// This function will return an error if the key is not a valid hex string. + pub fn user_key(&self) -> CliResult { Ok(UserKey::try_from_slice(&hex::decode(self.key.clone())?)?) } - pub(crate) fn label(&self) -> Label { + /// Returns the label. + pub fn label(&self) -> Label { Label::from(self.label.as_str()) } } #[allow(clippy::future_not_send)] -pub(crate) async fn instantiate_findex( - rest_client: RestClient, +/// Instantiates a Findex client. +/// # Errors +/// This function will return an error if there is an error instantiating the +/// Findex client. +pub async fn instantiate_findex( + rest_client: FindexClient, index_id: &Uuid, ) -> CliResult { let config = Configuration::Rest( - rest_client.client, - rest_client.server_url.clone(), - rest_client.server_url, + rest_client.client.client, + rest_client.client.server_url.clone(), + rest_client.client.server_url, index_id.to_string(), ); let findex = InstantiatedFindex::new(config).await?; diff --git a/crate/cli/src/actions/findex/search.rs b/crate/cli/src/actions/findex/search.rs index 722f6be..080d148 100644 --- a/crate/cli/src/actions/findex/search.rs +++ b/crate/cli/src/actions/findex/search.rs @@ -1,6 +1,6 @@ use clap::Parser; use cloudproof_findex::reexport::cosmian_findex::{Keyword, Keywords}; -use cosmian_rest_client::RestClient; +use cosmian_findex_client::FindexClient; use tracing::trace; use super::FindexParameters; @@ -34,7 +34,7 @@ impl SearchAction { /// Returns an error if the version query fails or if there is an issue /// writing to the console. #[allow(clippy::future_not_send)] // todo(manu): to remove this, changes must be done on `findex` repository - pub async fn process(&self, rest_client: RestClient) -> CliResult<()> { + pub async fn process(&self, rest_client: FindexClient) -> CliResult<()> { let findex = instantiate_findex(rest_client, &self.findex_parameters.index_id).await?; let results = findex .search( diff --git a/crate/cli/src/actions/login.rs b/crate/cli/src/actions/login.rs index e4c0cdb..c3538d9 100644 --- a/crate/cli/src/actions/login.rs +++ b/crate/cli/src/actions/login.rs @@ -1,42 +1,17 @@ -use std::{ - collections::HashMap, - convert::TryFrom, - path::PathBuf, - sync::mpsc::{self, Sender}, - thread, -}; - -use actix_web::{ - get, - web::{self, Data}, - App, HttpResponse, HttpServer, -}; use clap::Parser; -use cosmian_rest_client::ClientConf; -use oauth2::{ - basic::BasicClient, - http::{ - self, - header::{ACCEPT, CONTENT_TYPE}, - HeaderMap, HeaderValue, StatusCode, - }, - AuthUrl, ClientId, ClientSecret, CsrfToken, HttpRequest, PkceCodeChallenge, PkceCodeVerifier, - RedirectUrl, Scope, TokenUrl, +use cosmian_findex_client::reexport::{ + cosmian_findex_config::{FindexClientConfig, FindexConfigError}, + cosmian_http_client::LoginState, }; -use serde::Deserialize; -use url::Url; -use crate::{ - cli_bail, - error::{result::CliResult, CliError}, -}; +use crate::error::{result::CliResult, CliError}; /// Login to the Identity Provider of the Findex server using the `OAuth2` /// authorization code flow. /// /// This command will open a browser window and ask you to login to the Identity /// Provider. Once you have logged in, the access token will be saved in the -/// findex configuration file. +/// cosmian-findex-cli configuration file. /// /// The configuration file must contain an `oauth2_conf` object with the /// following fields: @@ -85,344 +60,34 @@ impl LoginAction { /// * The token exchange response cannot be parsed. /// * The client configuration cannot be updated or saved. #[allow(clippy::print_stdout)] - 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(|| { + pub async fn process(&self, conf: &FindexClientConfig) -> CliResult<()> { + let mut conf = conf.clone(); + let login_config = conf.http_config.oauth2_conf.as_ref().ok_or_else(|| { CliError::Default(format!( "The `login` command (only used for JWT authentication) requires an Identity \ - Provider (IdP) that MUST be configured in the oauth2_conf object in {conf_path:?}" + Provider (IdP) that MUST be configured in the oauth2_conf object in {:?}", + conf.conf_path )) })?; - let login_config = Oauth2LoginConfig { - client_id: oauth2_conf.client_id.clone(), - client_secret: oauth2_conf.client_secret.clone(), - authorize_url: oauth2_conf.authorize_url.clone(), - token_url: oauth2_conf.token_url.clone(), - scopes: oauth2_conf.scopes.clone(), - }; - let state = LoginState::try_from(login_config)?; + let state = LoginState::try_from(login_config.clone())?; println!("Browse to: {}", state.auth_url); let access_token = state.finalize().await?; // update the configuration and save it - conf.findex_access_token = Some(access_token); - conf.save(conf_path)?; + conf.http_config.access_token = Some(access_token); + let conf_path = conf.conf_path.clone().ok_or_else(|| { + CliError::ConfigError(FindexConfigError::Default( + "Configuration path `conf_path` must be filled".to_owned(), + )) + })?; + conf.save(&conf_path)?; println!( - "\nSuccess! The access token was saved in the Findex configuration file: {conf_path:?}" + "\nSuccess! The access token was saved in the Findex CLI configuration file: {:?}", + conf.conf_path ); Ok(()) } } - -pub struct Oauth2LoginConfig { - /// The client ID of your application. - pub client_id: String, - /// The client secret of your application. - pub client_secret: String, - /// The authorization URL of the provider. - /// For example, for Google it is `https://accounts.google.com/o/oauth2/v2/auth`. - pub authorize_url: String, - /// The token URL of the provider. - /// For example, for Google it is `https://oauth2.googleapis.com/token`. - pub token_url: String, - /// The scopes to request. - /// For example, for Google it is `["openid", "email"]`. - pub scopes: Vec, -} - -/// This struct holds the state of the login process. -/// It is used to generate the authorization URL and to store the PKCE verifier -/// and the CSRF token. -/// -/// The user should browse to the authorization URL and follow the instructions -/// to authenticate. The Url can be recovered by accessing to field `auth_url`. -/// -/// The CSRF token is used to verify that the authorization code received on the -/// redirect URL matches the one generated by the client. -/// The PKCE verifier is used to verify that the authorization code received on -/// the redirect URL matches the one generated by the client. -/// See [RFC 7636](https://tools.ietf.org/html/rfc7636) for more details. -/// See [PKCE](https://oauth.net/2/pkce/) for more details. -/// See [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) for more details. -/// See [OAuth2](https://oauth.net/2/) for more details. -/// See [OAuth2 RFC](https://tools.ietf.org/html/rfc6749) for more details. -pub struct LoginState { - login_config: Oauth2LoginConfig, - redirect_url: Url, - pub(crate) auth_url: Url, - pkce_verifier: PkceCodeVerifier, - csrf_token: CsrfToken, -} - -/// This function initializes the login process. -/// It returns a `LoginState` that holds the state of the login process. -/// -/// The process should be completed by instructing the user to browse to the -/// authorization URL and follow the instructions to authenticate -/// and immediately after calling `finalize()` with the returned `LoginState`. -/// -/// The Url can be recovered by accessing `auth_url` on the returned -/// `LoginState` struct. -impl TryFrom for LoginState { - type Error = CliError; - - fn try_from(login_config: Oauth2LoginConfig) -> Result { - let mut redirect_url = Url::parse("http://localhost:17899/authorization")?; - // if the port is specified in the environment variable, use it - if let Ok(port_s) = std::env::var("FINDEX_CLI_OAUTH2_REDIRECT_URL_PORT") { - let port = port_s.parse::().map_err(|e| { - CliError::Default(format!( - "Invalid FINDEX_CLI_OAUTH2_REDIRECT_URL_PORT: {e:?}" - )) - })?; - redirect_url.set_port(Some(port)).map_err(|e| { - CliError::Default(format!( - "Invalid FINDEX_CLI_OAUTH2_REDIRECT_URL_PORT: {e:?}" - )) - })?; - } - - // Create an OAuth2 client by specifying the client ID, client secret, - // authorization URL and token URL. - let client = BasicClient::new( - ClientId::new(login_config.client_id.clone()), - Some(ClientSecret::new(login_config.client_secret.clone())), - AuthUrl::new(login_config.authorize_url.clone())?, - Some(TokenUrl::new(login_config.token_url.clone())?), - ) - // Set the URL the user will be redirected to after the authorization process. - .set_redirect_uri(RedirectUrl::new(redirect_url.to_string())?); - - // Generate a PKCE challenge. - let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); - - let scopes = login_config - .scopes - .iter() - .map(|s| Scope::new(s.clone())) - .collect::>(); - - // Generate the full authorization URL. - let (auth_url, csrf_token) = client - .authorize_url(CsrfToken::new_random) - // Set the desired scopes. - .add_scopes(scopes) - // Set the PKCE code challenge. - .set_pkce_challenge(pkce_challenge) - .url(); - - // This is the URL you should redirect the user to, - // in order to trigger the authorization process. - Ok(Self { - login_config, - redirect_url, - auth_url, - pkce_verifier, - csrf_token, - }) - } -} - -impl LoginState { - /// This function finalizes the login process. - /// It returns the access token. - /// - /// 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()?; - - // Once the user has been redirected to the redirect URL, you'll have access to - // the authorization code. For security reasons, your code should verify - // that the `state` parameter returned by the server matches - // `csrf_state`. - let received_state = auth_parameters - .get("state") - .ok_or_else(|| CliError::Default("state not received on authentication".to_owned()))?; - if received_state != self.csrf_token.secret() { - return Err(CliError::Default( - "state received on authentication does not match".to_owned(), - )); - } - - // extract the authorization code - let authorization_code = auth_parameters - .get("code") - .ok_or_else(|| CliError::Default("code not received on authentication".to_owned()))?; - - // Now you can trade it for an access token. - - // TODO: unfortunately, the following does not work because Google return the - // JWT token in the `id_token` field, not the access_token field - // let token_result = login_state - // .client - // .exchange_code(AuthorizationCode::new(authorization_code.to_string())) - // // Set the PKCE code verifier. - // .set_pkce_verifier(login_state.pkce_verifier) - // .request_async(async_http_client) - // .await - // .map_err(|e| CliError::Default(format!("token exchange failed: {:?}", - // e)))?; - - let token_result = request_token( - &self.login_config, - &self.redirect_url, - &self.pkce_verifier, - authorization_code, - ) - .await?; - - Ok(match token_result.id_token { - // this is where Google returns the JWT token - Some(id_token) => id_token, - None => token_result.access_token, - }) - } - - /// This function starts the 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 authorization code is - /// returned. - #[allow(clippy::unwrap_used)] // hard to remove - fn receive_authorization_parameters() -> CliResult> { - let (auth_params_tx, auth_params_rx) = mpsc::channel::>(); - // Spawn the server into a runtime - let tokio_handle = tokio::runtime::Handle::current(); - let _task = thread::spawn(move || { - tokio_handle.block_on({ - // server.await - #[get("/authorization")] - async fn authorization_handler( - auth_params: web::Query>, - auth_params_tx: Data>>, - ) -> HttpResponse { - auth_params_tx - .into_inner() - .send(auth_params.into_inner()) - .unwrap(); - HttpResponse::Ok().body("You can now close this window.") - } - - HttpServer::new(move || { - App::new() - .app_data(Data::new(auth_params_tx.clone())) - .service(authorization_handler) - }) - .bind(("127.0.0.1", 17899))? - .run() - }) - }); - auth_params_rx - .recv() - .map_err(|e| CliError::Default(format!("authorization code not received: {e:?}"))) - } -} - -#[derive(Deserialize, Debug)] -pub struct OAuthResponse { - pub access_token: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub id_token: Option, - #[serde(skip)] - pub expires_in: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_token: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub scope: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub token_type: Option, -} - -/// This function requests the access token from the Identity Provider. -/// -/// This function was rewritten because Google returns the JWT token in the -/// `id_token` field, 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, - pkce_verifier: &PkceCodeVerifier, - authorization_code: &str, -) -> CliResult { - let params = vec![ - ("grant_type", "authorization_code"), - ("redirect_uri", redirect_url.as_str()), - ("client_id", login_config.client_id.as_str()), - ("code", authorization_code), - ("client_secret", login_config.client_secret.as_str()), - ("code_verifier", pkce_verifier.secret()), - ]; - - let mut headers = HeaderMap::new(); - headers.append(ACCEPT, HeaderValue::from_static("application/json")); - headers.append( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - let body = url::form_urlencoded::Serializer::new(String::new()) - .extend_pairs(params) - .finish() - .into_bytes(); - - let request = HttpRequest { - url: Url::parse(&login_config.token_url)?, - method: http::method::Method::POST, - headers, - body, - }; - - let response = oauth2::reqwest::async_http_client(request) - .await - .map_err(|e| CliError::Default(format!("failed issuing token exchange request: {e:?}")))?; - - if response.status_code != StatusCode::OK { - cli_bail!( - "failed token exchange: {}", - String::from_utf8_lossy(response.body.as_slice()) - ) - } - - let response_body = response.body.as_slice(); - - serde_json::from_slice(response_body) - .map_err(|e| CliError::Default(format!("failed parsing token exchange response: {e:?}"))) -} diff --git a/crate/cli/src/actions/logout.rs b/crate/cli/src/actions/logout.rs index bc30173..4fae791 100644 --- a/crate/cli/src/actions/logout.rs +++ b/crate/cli/src/actions/logout.rs @@ -1,9 +1,9 @@ -use std::path::PathBuf; - use clap::Parser; -use cosmian_rest_client::ClientConf; +use cosmian_findex_client::reexport::cosmian_findex_config::{ + FindexClientConfig, FindexConfigError, +}; -use crate::error::result::CliResult; +use crate::error::{result::CliResult, CliError}; /// Logout from the Identity Provider. /// @@ -24,14 +24,19 @@ impl LogoutAction { /// Returns an error if there is an issue loading or saving the /// configuration file. #[allow(clippy::print_stdout)] - pub fn process(&self, conf_path: &PathBuf) -> CliResult<()> { - let mut conf = ClientConf::load(conf_path)?; - conf.findex_access_token = None; - conf.save(conf_path)?; + pub fn process(&self, conf: &FindexClientConfig) -> CliResult<()> { + let mut conf = conf.clone(); + conf.http_config.access_token = None; + let conf_path = conf.conf_path.clone().ok_or_else(|| { + CliError::ConfigError(FindexConfigError::Default( + "Configuration path `conf_path` must be filled".to_owned(), + )) + })?; + conf.save(&conf_path)?; println!( - "\nThe access token was removed from the Findex server configuration file: \ - {conf_path:?}" + "\nThe access token was removed from the Findex CLI configuration file: {:?}", + conf.conf_path ); Ok(()) diff --git a/crate/cli/src/actions/permissions.rs b/crate/cli/src/actions/permissions.rs index b2a6420..110e626 100644 --- a/crate/cli/src/actions/permissions.rs +++ b/crate/cli/src/actions/permissions.rs @@ -1,5 +1,6 @@ use clap::Parser; -use cosmian_rest_client::{Permission, RestClient}; +use cosmian_findex_client::FindexClient; +use cosmian_findex_structs::Permission; use uuid::Uuid; use crate::{ @@ -25,7 +26,7 @@ impl PermissionsAction { /// # Errors /// /// Returns an error if there was a problem running the action. - pub async fn process(&self, rest_client: RestClient) -> CliResult<()> { + pub async fn process(&self, rest_client: FindexClient) -> CliResult<()> { match self { Self::Create(action) => action.run(rest_client).await?, Self::Grant(action) => action.run(rest_client).await?, @@ -58,7 +59,7 @@ impl CreateIndex { /// # Errors /// /// Returns an error if the query execution on the Findex server fails. - pub async fn run(&self, rest_client: RestClient) -> CliResult { + pub async fn run(&self, rest_client: FindexClient) -> CliResult { let response = rest_client .create_index_id() .await @@ -103,7 +104,7 @@ impl GrantPermission { /// # Errors /// /// Returns an error if the query execution on the Findex server fails. - pub async fn run(&self, rest_client: RestClient) -> CliResult { + pub async fn run(&self, rest_client: FindexClient) -> CliResult { let response = rest_client .grant_permission(&self.user, &self.permission, &self.index_id) .await @@ -140,7 +141,7 @@ impl RevokePermission { /// # Errors /// /// Returns an error if the query execution on the Findex server fails. - pub async fn run(&self, rest_client: RestClient) -> CliResult { + pub async fn run(&self, rest_client: FindexClient) -> CliResult { let response = rest_client .revoke_permission(&self.user, &self.index_id) .await diff --git a/crate/cli/src/actions/version.rs b/crate/cli/src/actions/version.rs index c5ea090..aeee619 100644 --- a/crate/cli/src/actions/version.rs +++ b/crate/cli/src/actions/version.rs @@ -1,5 +1,5 @@ use clap::Parser; -use cosmian_rest_client::RestClient; +use cosmian_findex_client::FindexClient; use super::console; use crate::error::result::{CliResult, CliResultHelper}; @@ -21,7 +21,7 @@ impl ServerVersionAction { /// /// Returns an error if the version query fails or if there is an issue /// writing to the console. - pub async fn process(&self, rest_client: RestClient) -> CliResult<()> { + pub async fn process(&self, rest_client: FindexClient) -> CliResult<()> { let version = rest_client .version() .await diff --git a/crate/cli/src/commands.rs b/crate/cli/src/commands.rs new file mode 100644 index 0000000..b9924bc --- /dev/null +++ b/crate/cli/src/commands.rs @@ -0,0 +1,107 @@ +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; +use cosmian_findex_client::{reexport::cosmian_findex_config::FindexClientConfig, FindexClient}; +use cosmian_logger::log_init; +use tracing::info; + +use crate::{ + actions::{ + findex::{add_or_delete::AddOrDeleteAction, search::SearchAction}, + login::LoginAction, + logout::LogoutAction, + permissions::PermissionsAction, + version::ServerVersionAction, + }, + error::result::CliResult, +}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: CoreFindexActions, + + /// Configuration file location + /// + /// This is an alternative to the env variable `FINDEX_CLI_CONF`. + /// Takes precedence over `FINDEX_CLI_CONF` env variable. + #[arg(short, long)] + conf: Option, + + /// The URL of the Findex + #[arg(long, action)] + pub(crate) url: Option, + + /// Allow to connect using a self-signed cert or untrusted cert chain + /// + /// `accept_invalid_certs` is useful if the CLI needs to connect to an HTTPS + /// Findex server running an invalid or insecure SSL certificate + #[arg(long)] + pub(crate) accept_invalid_certs: Option, +} + +#[derive(Subcommand)] +pub enum CoreFindexActions { + /// Index new keywords. + Add(AddOrDeleteAction), + /// Delete indexed keywords + Delete(AddOrDeleteAction), + Search(SearchAction), + ServerVersion(ServerVersionAction), + Login(LoginAction), + Logout(LogoutAction), + #[command(subcommand)] + Permissions(PermissionsAction), +} + +impl CoreFindexActions { + /// Process the command line arguments + /// # Errors + /// - If the configuration file is not found or invalid + #[allow(clippy::future_not_send)] + pub async fn run(&self, findex_client: FindexClient) -> CliResult<()> { + match self { + Self::Login(action) => action.process(&findex_client.conf).await, + Self::Logout(action) => action.process(&findex_client.conf), + Self::Add(action) => action.add(findex_client).await, + Self::Delete(action) => action.delete(findex_client).await, + Self::Search(action) => action.process(findex_client).await, + Self::ServerVersion(action) => action.process(findex_client).await, + Self::Permissions(action) => action.process(findex_client).await, + } + } +} + +/// Main function for the Findex CLI +/// # Errors +/// - If the configuration file is not found or invalid +/// - If the command line arguments are invalid +#[allow(clippy::future_not_send)] +pub async fn findex_cli_main() -> CliResult<()> { + log_init(None); + let opts = Cli::parse(); + + // Load configuration file and override with command line options + let conf_path = FindexClientConfig::location(opts.conf)?; + let mut conf = FindexClientConfig::load(&conf_path)?; + if opts.url.is_some() { + info!("Override URL from configuration file with: {:?}", opts.url); + conf.http_config.server_url = opts.url.unwrap_or_default(); + } + if opts.accept_invalid_certs.is_some() { + info!( + "Override accept_invalid_certs from configuration file with: {:?}", + opts.accept_invalid_certs + ); + conf.http_config.accept_invalid_certs = opts.accept_invalid_certs.unwrap_or_default(); + } + + // Instantiate the Findex REST client + let rest_client = FindexClient::new(conf)?; + + // Process the command + opts.command.run(rest_client).await?; + + Ok(()) +} diff --git a/crate/cli/src/error/mod.rs b/crate/cli/src/error/mod.rs index 1a63e5c..266d65a 100644 --- a/crate/cli/src/error/mod.rs +++ b/crate/cli/src/error/mod.rs @@ -6,7 +6,10 @@ use cloudproof_findex::{ db_interfaces::DbInterfaceError, reexport::{cosmian_crypto_core::CryptoCoreError, cosmian_findex}, }; -use cosmian_rest_client::ClientError; +use cosmian_findex_client::{ + reexport::{cosmian_findex_config::FindexConfigError, cosmian_http_client::HttpClientError}, + FindexClientError, +}; use hex::FromHexError; use pem::PemError; use thiserror::Error; @@ -54,7 +57,7 @@ pub enum CliError { // When the Findex server client returns an error #[error("{0}")] - KmsClientError(String), + FindexClientError(String), // Other errors #[error("invalid options: {0}")] @@ -71,6 +74,9 @@ pub enum CliError { // When an error occurs fetching Gmail API #[error("Error interacting with Gmail API: {0}")] GmailApiError(String), + + #[error(transparent)] + ConfigError(#[from] FindexConfigError), } impl From for CliError { @@ -140,9 +146,9 @@ impl From for CliError { } } -impl From for CliError { - fn from(e: ClientError) -> Self { - Self::KmsClientError(e.to_string()) +impl From for CliError { + fn from(e: FindexClientError) -> Self { + Self::FindexClientError(e.to_string()) } } @@ -188,6 +194,12 @@ impl From for CliError { } } +impl From for CliError { + fn from(e: HttpClientError) -> Self { + Self::FindexClientError(e.to_string()) + } +} + /// Return early with an error if a condition is not satisfied. /// /// This macro is equivalent to `if !$cond { return Err(From::from($err)); }`. diff --git a/crate/cli/src/lib.rs b/crate/cli/src/lib.rs index cd81fe6..51948c0 100644 --- a/crate/cli/src/lib.rs +++ b/crate/cli/src/lib.rs @@ -46,7 +46,10 @@ clippy::redundant_pub_crate )] pub mod actions; +pub mod commands; pub mod error; +pub use commands::{findex_cli_main, CoreFindexActions}; + #[cfg(test)] mod tests; diff --git a/crate/cli/src/main.rs b/crate/cli/src/main.rs index 878f67f..d973083 100644 --- a/crate/cli/src/main.rs +++ b/crate/cli/src/main.rs @@ -1,107 +1,12 @@ -use std::{path::PathBuf, process}; +use std::process; -use clap::{CommandFactory, Parser, Subcommand}; -use cosmian_findex_cli::{ - actions::{ - findex::{add_or_delete::AddOrDeleteAction, search::SearchAction}, - login::LoginAction, - logout::LogoutAction, - markdown::MarkdownAction, - permissions::PermissionsAction, - version::ServerVersionAction, - }, - error::result::CliResult, -}; -use cosmian_logger::log_utils::log_init; -use cosmian_rest_client::ClientConf; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Cli { - #[command(subcommand)] - command: CliCommands, - - /// Configuration file location - /// - /// This is an alternative to the env variable `FINDEX_CLI_CONF`. - /// Takes precedence over `FINDEX_CLI_CONF` env variable. - #[arg(short, long)] - conf: Option, - - /// The URL of the Findex - #[arg(long, action)] - pub(crate) url: Option, - - /// Allow to connect using a self-signed cert or untrusted cert chain - /// - /// `accept_invalid_certs` is useful if the CLI needs to connect to an HTTPS - /// Findex server running an invalid or insecure SSL certificate - #[arg(long)] - pub(crate) accept_invalid_certs: Option, -} - -#[derive(Subcommand)] -enum CliCommands { - /// Index new keywords. - Add(AddOrDeleteAction), - /// Delete indexed keywords - Delete(AddOrDeleteAction), - Search(SearchAction), - ServerVersion(ServerVersionAction), - Login(LoginAction), - Logout(LogoutAction), - #[command(subcommand)] - Permissions(PermissionsAction), - - /// Action to auto-generate doc in Markdown format - /// Run `cargo run --bin findex -- markdown - /// documentation/docs/cli/main_commands.md` - #[clap(hide = true)] - Markdown(MarkdownAction), -} +use cosmian_findex_cli::findex_cli_main; #[tokio::main] #[allow(clippy::needless_return)] async fn main() { - if let Some(err) = main_().await.err() { + if let Some(err) = findex_cli_main().await.err() { eprintln!("ERROR: {err}"); process::exit(1); } } - -async fn main_() -> CliResult<()> { - log_init(None); - let opts = Cli::parse(); - - if let CliCommands::Markdown(action) = opts.command { - let command = ::command(); - action.process(&command)?; - return Ok(()); - } - - let conf_path = ClientConf::location(opts.conf)?; - - match opts.command { - CliCommands::Login(action) => action.process(&conf_path).await?, - CliCommands::Logout(action) => action.process(&conf_path)?, - - command => { - let conf = ClientConf::load(&conf_path)?; - let rest_client = - conf.initialize_findex_client(opts.url.as_deref(), opts.accept_invalid_certs)?; - - match command { - CliCommands::Add(action) => action.add(rest_client).await?, - CliCommands::Delete(action) => action.delete(rest_client).await?, - CliCommands::Search(action) => action.process(rest_client).await?, - CliCommands::ServerVersion(action) => action.process(rest_client).await?, - CliCommands::Permissions(action) => action.process(rest_client).await?, - _ => { - tracing::error!("unexpected command"); - } - } - } - } - - Ok(()) -} diff --git a/crate/cli/src/tests/auth_tests.rs b/crate/cli/src/tests/auth_tests.rs index 23f8301..db22fbc 100644 --- a/crate/cli/src/tests/auth_tests.rs +++ b/crate/cli/src/tests/auth_tests.rs @@ -3,8 +3,7 @@ use std::{env, path::PathBuf, process::Command}; use assert_cmd::prelude::*; use base64::Engine; -use cosmian_logger::log_utils::log_init; -use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use cosmian_logger::log_init; use tempfile::TempDir; use test_findex_server::{ start_test_server_with_options, AuthenticationOptions, DBConfig, DatabaseType, TestsContext, diff --git a/crate/cli/src/tests/findex/add_or_delete.rs b/crate/cli/src/tests/findex/add_or_delete.rs index a7097bd..fc5aa58 100644 --- a/crate/cli/src/tests/findex/add_or_delete.rs +++ b/crate/cli/src/tests/findex/add_or_delete.rs @@ -1,7 +1,7 @@ use std::process::Command; use assert_cmd::prelude::*; -use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use cosmian_findex_client::reexport::cosmian_findex_config::FINDEX_CLI_CONF_ENV; use tracing::debug; use crate::{ diff --git a/crate/cli/src/tests/findex/mod.rs b/crate/cli/src/tests/findex/mod.rs index acd19c5..da7b216 100644 --- a/crate/cli/src/tests/findex/mod.rs +++ b/crate/cli/src/tests/findex/mod.rs @@ -1,6 +1,6 @@ use add_or_delete::add_or_delete_cmd; -use cosmian_logger::log_utils::log_init; -use cosmian_rest_client::Permission; +use cosmian_findex_structs::Permission; +use cosmian_logger::log_init; use search::search_cmd; use test_findex_server::{ start_default_test_findex_server, start_default_test_findex_server_with_cert_auth, diff --git a/crate/cli/src/tests/findex/search.rs b/crate/cli/src/tests/findex/search.rs index a093819..1a2c363 100644 --- a/crate/cli/src/tests/findex/search.rs +++ b/crate/cli/src/tests/findex/search.rs @@ -1,7 +1,7 @@ use std::process::Command; use assert_cmd::prelude::*; -use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use cosmian_findex_client::reexport::cosmian_findex_config::FINDEX_CLI_CONF_ENV; use tracing::debug; use crate::{ diff --git a/crate/cli/src/tests/permissions.rs b/crate/cli/src/tests/permissions.rs index 55e5a17..aee555e 100644 --- a/crate/cli/src/tests/permissions.rs +++ b/crate/cli/src/tests/permissions.rs @@ -1,7 +1,7 @@ use std::process::Command; use assert_cmd::prelude::*; -use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use cosmian_findex_client::reexport::cosmian_findex_config::FINDEX_CLI_CONF_ENV; use regex::{Regex, RegexBuilder}; use tracing::{debug, trace}; use uuid::Uuid; diff --git a/crate/client/Cargo.toml b/crate/client/Cargo.toml index 9e66772..65b4fcd 100644 --- a/crate/client/Cargo.toml +++ b/crate/client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cosmian_rest_client" +name = "cosmian_findex_client" version.workspace = true authors.workspace = true edition.workspace = true @@ -16,17 +16,14 @@ doctest = false [dependencies] base64 = { workspace = true } -clap = { workspace = true } +cosmian_http_client = { workspace = true } +cosmian_findex_config = { path = "../config" } +cosmian_findex_structs = { path = "../structs" } der = { workspace = true } pem = { workspace = true } reqwest = { workspace = true, features = ["default", "json", "native-tls"] } serde = { workspace = true } -serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } -uuid = { workspace = true, features = ["v4"] } - -[dev-dependencies] -faker_rand = "0.1" -rand = "0.8" +uuid = { workspace = true } diff --git a/crate/client/README.md b/crate/client/README.md index e1628a6..d4d992c 100644 --- a/crate/client/README.md +++ b/crate/client/README.md @@ -1,3 +1,3 @@ # Cosmian Findex Client -The `cosmian_rest_client` forges and sends post query to the `cosmian_findex_server`. +The `cosmian_findex_client` forges and sends post query to the `cosmian_findex_server`. diff --git a/crate/client/src/datasets.rs b/crate/client/src/datasets.rs new file mode 100644 index 0000000..635b9c3 --- /dev/null +++ b/crate/client/src/datasets.rs @@ -0,0 +1,99 @@ +use cosmian_findex_structs::{EncryptedEntries, Uuids}; +use tracing::{instrument, trace}; +use uuid::Uuid; + +use crate::{ + error::{result::FindexClientResult, FindexClientError}, + findex_rest_client::SuccessResponse, + handle_error, FindexClient, +}; + +impl FindexClient { + // #[instrument(ret(Display), err, skip(self))] + pub async fn add_entries( + // todo(manu): revisit function names (prefix with dataset_, findex_, permissions) + &self, + index_id: &Uuid, + encrypted_entries: &EncryptedEntries, + ) -> FindexClientResult { + let endpoint = format!("/datasets/{index_id}/add_entries"); + let server_url = format!("{}{endpoint}", self.client.server_url); + trace!("POST: {server_url}"); + let encrypted_entries = encrypted_entries.serialize()?; + let response = self + .client + .client + .post(server_url) + .body(encrypted_entries) + .send() + .await?; + + trace!("Response: {response:?}"); + let status_code = response.status(); + if status_code.is_success() { + return Ok(response.json::().await?); + } + + // process error + let p = handle_error(&endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } + + #[instrument(ret(Display), err, skip(self))] + pub async fn delete_entries( + &self, + index_id: &Uuid, + uuids: &Uuids, + ) -> FindexClientResult { + let endpoint = format!("/datasets/{index_id}/delete_entries"); + let server_url = format!("{}{endpoint}", self.client.server_url); + trace!("POST: {server_url}"); + + let uuids = uuids.serialize(); + let response = self + .client + .client + .post(server_url) + .body(uuids) + .send() + .await?; + let status_code = response.status(); + if status_code.is_success() { + return Ok(response.json::().await?); + } + + // process error + let p = handle_error(&endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } + + #[instrument(ret(Display), err, skip(self))] + pub async fn get_entries( + &self, + index_id: &Uuid, + uuids: &Uuids, + ) -> FindexClientResult { + let endpoint = format!("/datasets/{index_id}/get_entries"); + let server_url = format!("{}{endpoint}", self.client.server_url); + trace!("POST: {server_url}"); + + let uuids = uuids.serialize(); + let response = self + .client + .client + .post(server_url) + .body(uuids) + .send() + .await?; + let status_code = response.status(); + if status_code.is_success() { + let response_bytes = response.bytes().await.map(|r| r.to_vec())?; + let encrypted_entries = EncryptedEntries::deserialize(&response_bytes)?; + return Ok(encrypted_entries); + } + + // process error + let p = handle_error(&endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } +} diff --git a/crate/client/src/error/mod.rs b/crate/client/src/error/mod.rs index 4b53777..7a7ec93 100644 --- a/crate/client/src/error/mod.rs +++ b/crate/client/src/error/mod.rs @@ -1,11 +1,12 @@ use std::io; +use cosmian_findex_structs::StructsError; use thiserror::Error; pub(crate) mod result; #[derive(Error, Debug)] -pub enum ClientError { +pub enum FindexClientError { #[error(transparent)] Base64DecodeError(#[from] base64::DecodeError), @@ -38,56 +39,47 @@ pub enum ClientError { #[error(transparent)] UrlError(#[from] url::ParseError), -} -impl From for ClientError { - fn from(e: reqwest::Error) -> Self { - Self::Default(format!("{e}: Details: {e:?}")) - } -} + #[error(transparent)] + StructsError(#[from] StructsError), -impl From for ClientError { - fn from(e: reqwest::header::InvalidHeaderValue) -> Self { - Self::Default(e.to_string()) - } -} + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), -impl From for ClientError { - fn from(e: io::Error) -> Self { - Self::Default(e.to_string()) - } -} + #[error(transparent)] + ReqwestHeaderError(#[from] reqwest::header::InvalidHeaderValue), -impl From for ClientError { - fn from(e: der::Error) -> Self { - Self::Default(e.to_string()) - } + #[error(transparent)] + IoError(#[from] io::Error), + + #[error(transparent)] + DerError(#[from] der::Error), } /// Construct a server error from a string. #[macro_export] -macro_rules! client_error { +macro_rules! findex_client_error { ($msg:literal) => { - $crate::ClientError::Default(::core::format_args!($msg).to_string()) + $crate::FindexClientError::Default(::core::format_args!($msg).to_string()) }; ($err:expr $(,)?) => ({ - $crate::ClientError::Default($err.to_string()) + $crate::FindexClientError::Default($err.to_string()) }); ($fmt:expr, $($arg:tt)*) => { - $crate::ClientError::Default(::core::format_args!($fmt, $($arg)*).to_string()) + $crate::FindexClientError::Default(::core::format_args!($fmt, $($arg)*).to_string()) }; } /// Return early with an error if a condition is not satisfied. #[macro_export] -macro_rules! client_bail { +macro_rules! findex_client_bail { ($msg:literal) => { - return ::core::result::Result::Err($crate::client_error!($msg)) + return ::core::result::Result::Err($crate::findex_client_error!($msg)) }; ($err:expr $(,)?) => { return ::core::result::Result::Err($err) }; ($fmt:expr, $($arg:tt)*) => { - return ::core::result::Result::Err($crate::client_error!($fmt, $($arg)*)) + return ::core::result::Result::Err($crate::findex_client_error!($fmt, $($arg)*)) }; } diff --git a/crate/client/src/error/result.rs b/crate/client/src/error/result.rs index ea3a844..a503be7 100644 --- a/crate/client/src/error/result.rs +++ b/crate/client/src/error/result.rs @@ -1,45 +1,45 @@ use std::fmt::Display; -use super::ClientError; +use super::FindexClientError; -pub(crate) type ClientResult = Result; +pub type FindexClientResult = Result; #[allow(dead_code)] -pub(crate) trait RestClientResultHelper { - fn context(self, context: &str) -> ClientResult; - fn with_context(self, op: O) -> ClientResult +pub(crate) trait FindexRestClientResultHelper { + fn context(self, context: &str) -> FindexClientResult; + fn with_context(self, op: O) -> FindexClientResult where D: Display + Send + Sync + 'static, O: FnOnce() -> D; } -impl RestClientResultHelper for Result +impl FindexRestClientResultHelper for Result where E: std::error::Error, { - fn context(self, context: &str) -> ClientResult { - self.map_err(|e| ClientError::Default(format!("{context}: {e}"))) + fn context(self, context: &str) -> FindexClientResult { + self.map_err(|e| FindexClientError::Default(format!("{context}: {e}"))) } - fn with_context(self, op: O) -> ClientResult + fn with_context(self, op: O) -> FindexClientResult where D: Display + Send + Sync + 'static, O: FnOnce() -> D, { - self.map_err(|e| ClientError::Default(format!("{}: {e}", op()))) + self.map_err(|e| FindexClientError::Default(format!("{}: {e}", op()))) } } -impl RestClientResultHelper for Option { - fn context(self, context: &str) -> ClientResult { - self.ok_or_else(|| ClientError::Default(context.to_owned())) +impl FindexRestClientResultHelper for Option { + fn context(self, context: &str) -> FindexClientResult { + self.ok_or_else(|| FindexClientError::Default(context.to_string())) } - fn with_context(self, op: O) -> ClientResult + fn with_context(self, op: O) -> FindexClientResult where D: Display + Send + Sync + 'static, O: FnOnce() -> D, { - self.ok_or_else(|| ClientError::Default(format!("{}", op()))) + self.ok_or_else(|| FindexClientError::Default(format!("{}", op()))) } } diff --git a/crate/client/src/file_utils.rs b/crate/client/src/file_utils.rs deleted file mode 100644 index 1545af9..0000000 --- a/crate/client/src/file_utils.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{ - fs::{self, File}, - io::Read, - path::Path, -}; - -use serde::{de::DeserializeOwned, Serialize}; - -use crate::{ - error::{result::ClientResult, ClientError}, - ClientResultHelper, -}; - -/// Read all bytes from a file -/// # Errors -/// It returns an error if the file cannot be opened or read -pub fn read_bytes_from_file(file: &impl AsRef) -> ClientResult> { - let mut buffer = Vec::new(); - File::open(file) - .with_context(|| format!("could not open the file {}", file.as_ref().display()))? - .read_to_end(&mut buffer) - .with_context(|| format!("could not read the file {}", file.as_ref().display()))?; - - Ok(buffer) -} - -/// Read an object T from a JSON file -/// # Errors -/// It returns an error if the file cannot be opened or read -pub fn read_from_json_file(file: &impl AsRef) -> Result -where - T: DeserializeOwned, -{ - let buffer = read_bytes_from_file(file)?; - serde_json::from_slice::(&buffer) - .with_context(|| "failed parsing the object from the json file") -} - -/// Write all bytes to a file -/// # Errors -/// It returns an error if the file cannot be written -pub fn write_bytes_to_file(bytes: &[u8], file: &impl AsRef) -> Result<(), ClientError> { - fs::write(file, bytes).with_context(|| { - format!( - "failed writing {} bytes to {:?}", - bytes.len(), - file.as_ref() - ) - }) -} - -/// Write a JSON object to a file -/// # Errors -/// It returns an error if the file cannot be written -pub fn write_json_object_to_file( - json_object: &T, - file: &impl AsRef, -) -> Result<(), ClientError> -where - T: Serialize, -{ - let bytes = serde_json::to_vec::(json_object) - .with_context(|| "failed parsing the object from the json file")?; - write_bytes_to_file(&bytes, file) -} diff --git a/crate/client/src/findex_rest_client.rs b/crate/client/src/findex_rest_client.rs new file mode 100644 index 0000000..c93850c --- /dev/null +++ b/crate/client/src/findex_rest_client.rs @@ -0,0 +1,85 @@ +use std::fmt::Display; + +use cosmian_findex_config::FindexClientConfig; +use cosmian_http_client::HttpClient; +use reqwest::{Response, StatusCode}; +use serde::{Deserialize, Serialize}; +use tracing::{instrument, trace}; + +use crate::error::{ + result::{FindexClientResult, FindexRestClientResultHelper}, + FindexClientError, +}; + +// Response for success +#[derive(Deserialize, Serialize, Debug)] // Debug is required by ok_json() +pub struct SuccessResponse { + pub success: String, +} + +impl Display for SuccessResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.success) + } +} + +#[derive(Clone)] +pub struct FindexClient { + pub client: HttpClient, + pub conf: FindexClientConfig, +} + +impl FindexClient { + /// Initialize a Findex REST client. + /// + /// Parameters `server_url` and `accept_invalid_certs` from the command line + /// will override the ones from the configuration file. + pub fn new(conf: FindexClientConfig) -> Result { + // Instantiate a Findex server REST client with the given configuration + let client = HttpClient::instantiate(&conf.http_config).with_context(|| { + format!( + "Unable to instantiate a Findex REST client to server at {}", + conf.http_config.server_url + ) + })?; + + Ok(Self { client, conf }) + } + + #[instrument(ret(Display), err, skip(self))] + pub async fn version(&self) -> FindexClientResult { + let endpoint = "/version"; + let server_url = format!("{}{endpoint}", self.client.server_url); + let response = self.client.client.get(server_url).send().await?; + let status_code = response.status(); + if status_code.is_success() { + return Ok(response.json::().await?); + } + + // process error + let p = handle_error(endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } +} + +/// Some errors are returned by the Middleware without going through our own +/// error manager. In that case, we make the error clearer here for the client. +pub async fn handle_error(endpoint: &str, response: Response) -> Result { + trace!("Error response received on {endpoint}: Response: {response:?}"); + let status = response.status(); + let text = response.text().await?; + + Ok(format!( + "{}: {}", + endpoint, + if text.is_empty() { + match status { + StatusCode::NOT_FOUND => "Findex server endpoint does not exist".to_owned(), + StatusCode::UNAUTHORIZED => "Bad authorization token".to_owned(), + _ => format!("{status} {text}"), + } + } else { + text + } + )) +} diff --git a/crate/client/src/lib.rs b/crate/client/src/lib.rs index 56ebea0..707712e 100644 --- a/crate/client/src/lib.rs +++ b/crate/client/src/lib.rs @@ -1,66 +1,11 @@ -#![deny( - nonstandard_style, - refining_impl_trait, - future_incompatible, - keyword_idents, - let_underscore, - // rust_2024_compatibility, - unreachable_pub, - unused, - unsafe_code, - clippy::all, - clippy::suspicious, - clippy::complexity, - clippy::perf, - clippy::style, - clippy::pedantic, - clippy::cargo, - clippy::nursery, +pub use error::{result::FindexClientResult, FindexClientError}; +pub use findex_rest_client::{handle_error, FindexClient}; - // 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, - clippy::redundant_clone, - clippy::todo -)] -#![allow( - clippy::module_name_repetitions, - clippy::similar_names, - clippy::cargo_common_metadata, - clippy::multiple_crate_versions, - clippy::redundant_pub_crate, - clippy::future_not_send, - clippy::significant_drop_tightening -)] - -pub use config::{ClientConf, FINDEX_CLI_CONF_ENV}; -pub use error::ClientError; -pub use file_utils::{ - read_bytes_from_file, read_from_json_file, write_bytes_to_file, write_json_object_to_file, -}; -pub use permission::Permission; -pub use rest_client::RestClient; -pub use result::{ClientResultHelper, RestClientResult}; - -mod config; +mod datasets; mod error; -mod file_utils; -mod permission; -mod rest_client; -mod result; +mod findex_rest_client; +mod permissions; +pub mod reexport { + pub use cosmian_findex_config; + pub use cosmian_http_client; +} diff --git a/crate/client/src/permission.rs b/crate/client/src/permission.rs deleted file mode 100644 index e1b3f73..0000000 --- a/crate/client/src/permission.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::fmt::Display; - -use clap::ValueEnum; - -#[derive(Clone, Debug, ValueEnum)] -pub enum Permission { - Read = 0, - Write = 1, - Admin = 2, -} - -impl Display for Permission { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - Self::Read => "read", - Self::Write => "write", - Self::Admin => "admin", - }; - write!(f, "{s}") - } -} diff --git a/crate/client/src/permissions.rs b/crate/client/src/permissions.rs new file mode 100644 index 0000000..1b2f23c --- /dev/null +++ b/crate/client/src/permissions.rs @@ -0,0 +1,69 @@ +use cosmian_findex_structs::Permission; +use tracing::{instrument, trace}; +use uuid::Uuid; + +use crate::{ + error::{result::FindexClientResult, FindexClientError}, + findex_rest_client::SuccessResponse, + handle_error, FindexClient, +}; + +impl FindexClient { + #[instrument(ret(Display), err, skip(self))] + pub async fn create_index_id(&self) -> FindexClientResult { + let endpoint = "/create/index".to_owned(); + let server_url = format!("{}{endpoint}", self.client.server_url); + trace!("POST: {server_url}"); + let response = self.client.client.post(server_url).send().await?; + trace!("Response: {response:?}"); + let status_code = response.status(); + if status_code.is_success() { + return Ok(response.json::().await?); + } + + // process error + let p = handle_error(&endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } + + #[instrument(ret(Display), err, skip(self))] + pub async fn grant_permission( + &self, + user_id: &str, + permission: &Permission, + index_id: &Uuid, + ) -> FindexClientResult { + let endpoint = format!("/permission/grant/{user_id}/{permission}/{index_id}"); + let server_url = format!("{}{endpoint}", self.client.server_url); + trace!("POST: {server_url}"); + let response = self.client.client.post(server_url).send().await?; + let status_code = response.status(); + if status_code.is_success() { + return Ok(response.json::().await?); + } + + // process error + let p = handle_error(&endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } + + #[instrument(ret(Display), err, skip(self))] + pub async fn revoke_permission( + &self, + user_id: &str, + index_id: &Uuid, + ) -> FindexClientResult { + let endpoint = format!("/permission/revoke/{user_id}/{index_id}"); + let server_url = format!("{}{endpoint}", self.client.server_url); + trace!("POST: {server_url}"); + let response = self.client.client.post(server_url).send().await?; + let status_code = response.status(); + if status_code.is_success() { + return Ok(response.json::().await?); + } + + // process error + let p = handle_error(&endpoint, response).await?; + Err(FindexClientError::RequestFailed(p)) + } +} diff --git a/crate/client/src/rest_client.rs b/crate/client/src/rest_client.rs deleted file mode 100644 index 92baf85..0000000 --- a/crate/client/src/rest_client.rs +++ /dev/null @@ -1,190 +0,0 @@ -use std::{ - fmt::Display, - fs::File, - io::{BufReader, Read}, - time::Duration, -}; - -use reqwest::{ - header::{HeaderMap, HeaderValue}, - Client, ClientBuilder, Identity, Response, StatusCode, -}; -use serde::{Deserialize, Serialize}; -use tracing::{instrument, trace}; -use uuid::Uuid; - -use crate::{ - error::{result::ClientResult, ClientError}, - ClientResultHelper, Permission, -}; - -#[derive(Clone)] -pub struct RestClient { - pub server_url: String, - pub client: Client, -} - -impl RestClient { - /// Instantiate a new Findex REST Client - /// # Errors - /// It returns an error if the client cannot be instantiated - pub fn instantiate( - server_url: &str, - bearer_token: Option<&str>, - ssl_client_pkcs12_path: Option<&str>, - ssl_client_pkcs12_password: Option<&str>, - accept_invalid_certs: bool, - ) -> Result { - let server_url = server_url - .strip_suffix('/') - .map_or_else(|| server_url.to_owned(), std::string::ToString::to_string); - - let mut headers = HeaderMap::new(); - if let Some(bearer_token) = bearer_token { - headers.insert( - "Authorization", - HeaderValue::from_str(format!("Bearer {bearer_token}").as_str())?, - ); - } - - // We deal with 4 scenarios: - // 1. HTTP: no TLS - // 2. HTTPS: a) self-signed: we want to remove the verifications b) signed in a - // non-tee context: we want classic TLS verification based on the root ca - let builder = ClientBuilder::new().danger_accept_invalid_certs(accept_invalid_certs); - // If a PKCS12 file is provided, use it to build the client - let builder = match ssl_client_pkcs12_path { - Some(ssl_client_pkcs12) => { - let mut pkcs12 = BufReader::new(File::open(ssl_client_pkcs12)?); - let mut pkcs12_bytes = vec![]; - pkcs12.read_to_end(&mut pkcs12_bytes)?; - let pkcs12 = Identity::from_pkcs12_der( - &pkcs12_bytes, - ssl_client_pkcs12_password.unwrap_or(""), - )?; - builder.identity(pkcs12) - } - None => builder, - }; - - // Build the client - Ok(Self { - client: builder - .default_headers(headers) - .tcp_keepalive(Duration::from_secs(60)) - .build() - .context("Reqwest client builder")?, - server_url, - }) - } - - /// This operation requests the server to create a new table. - /// The returned secrets could be shared between several users. - /// # Errors - /// It returns an error if the request fails - #[instrument(ret(Display), err, skip(self))] - pub async fn version(&self) -> ClientResult { - let endpoint = "/version"; - let server_url = format!("{}{endpoint}", self.server_url); - let response = self.client.get(server_url).send().await?; - let status_code = response.status(); - if status_code.is_success() { - return Ok(response.json::().await?); - } - - // process error - let p = handle_error(endpoint, response).await?; - Err(ClientError::RequestFailed(p)) - } - - #[instrument(ret(Display), err, skip(self))] - pub async fn create_index_id(&self) -> ClientResult { - let endpoint = "/create/index".to_owned(); - let server_url = format!("{}{endpoint}", self.server_url); - trace!("POST create_index_id: {server_url}"); - let response = self.client.post(server_url).send().await?; - trace!("Response: {response:?}"); - let status_code = response.status(); - if status_code.is_success() { - return Ok(response.json::().await?); - } - - // process error - let p = handle_error(&endpoint, response).await?; - Err(ClientError::RequestFailed(p)) - } - - #[instrument(ret(Display), err, skip(self))] - pub async fn grant_permission( - &self, - user_id: &str, - permission: &Permission, - index_id: &Uuid, - ) -> ClientResult { - let endpoint = format!("/permission/grant/{user_id}/{permission}/{index_id}"); - let server_url = format!("{}{endpoint}", self.server_url); - trace!("POST grant_permission: {server_url}"); - let response = self.client.post(server_url).send().await?; - let status_code = response.status(); - if status_code.is_success() { - return Ok(response.json::().await?); - } - - // process error - let p = handle_error(&endpoint, response).await?; - Err(ClientError::RequestFailed(p)) - } - - #[instrument(ret(Display), err, skip(self))] - pub async fn revoke_permission( - &self, - user_id: &str, - index_id: &Uuid, - ) -> ClientResult { - let endpoint = format!("/permission/revoke/{user_id}/{index_id}"); - let server_url = format!("{}{endpoint}", self.server_url); - trace!("POST revoke_permission: {server_url}"); - let response = self.client.post(server_url).send().await?; - let status_code = response.status(); - if status_code.is_success() { - return Ok(response.json::().await?); - } - - // process error - let p = handle_error(&endpoint, response).await?; - Err(ClientError::RequestFailed(p)) - } -} - -/// Some errors are returned by the Middleware without going through our own -/// error manager. In that case, we make the error clearer here for the client. -async fn handle_error(endpoint: &str, response: Response) -> Result { - trace!("Error response received on {endpoint}: Response: {response:?}"); - let status = response.status(); - let text = response.text().await?; - - Ok(format!( - "{}: {}", - endpoint, - if text.is_empty() { - match status { - StatusCode::NOT_FOUND => "Findex server endpoint does not exist".to_owned(), - StatusCode::UNAUTHORIZED => "Bad authorization token".to_owned(), - _ => format!("{status} {text}"), - } - } else { - text - } - )) -} - -#[derive(Deserialize, Serialize, Debug)] // Debug is required by ok_json() -pub struct SuccessResponse { - pub success: String, -} - -impl Display for SuccessResponse { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.success) - } -} diff --git a/crate/client/src/result.rs b/crate/client/src/result.rs deleted file mode 100644 index 82be409..0000000 --- a/crate/client/src/result.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::fmt::Display; - -use crate::error::ClientError; - -pub type RestClientResult = Result; - -pub trait ClientResultHelper { - /// Add a context to the error message - /// # Errors - /// It returns the error with the context added - fn context(self, context: &str) -> RestClientResult; - - /// Add a context to the error message - /// # Errors - /// It returns the error with the context added - fn with_context(self, op: O) -> RestClientResult - where - D: Display + Send + Sync + 'static, - O: FnOnce() -> D; -} - -impl ClientResultHelper for std::result::Result -where - E: std::error::Error, -{ - fn context(self, context: &str) -> RestClientResult { - self.map_err(|e| ClientError::Default(format!("{context}: {e}"))) - } - - fn with_context(self, op: O) -> RestClientResult - where - D: Display + Send + Sync + 'static, - O: FnOnce() -> D, - { - self.map_err(|e| ClientError::Default(format!("{}: {e}", op()))) - } -} - -impl ClientResultHelper for Option { - fn context(self, context: &str) -> RestClientResult { - self.ok_or_else(|| ClientError::Default(context.to_owned())) - } - - fn with_context(self, op: O) -> RestClientResult - where - D: Display + Send + Sync + 'static, - O: FnOnce() -> D, - { - self.ok_or_else(|| ClientError::Default(format!("{}", op()))) - } -} diff --git a/crate/client/test_data/configs/findex.bad b/crate/client/test_data/configs/findex.bad deleted file mode 100644 index f59dcb6..0000000 --- a/crate/client/test_data/configs/findex.bad +++ /dev/null @@ -1,4 +0,0 @@ -{ - "findex_access_token": - "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" -} diff --git a/crate/client/test_data/configs/findex.json b/crate/client/test_data/configs/findex.json deleted file mode 100644 index 6aa923b..0000000 --- a/crate/client/test_data/configs/findex.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "findex_server_url": "http://127.0.0.1:6666", - "findex_access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" -} diff --git a/crate/config/Cargo.toml b/crate/config/Cargo.toml new file mode 100644 index 0000000..b621398 --- /dev/null +++ b/crate/config/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cosmian_findex_config" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +# doc test linking as a separate binary is extremely slow +# and is not needed for internal lib +doctest = false + +[features] + +[dependencies] +base64 = { workspace = true } +cosmian_logger = { workspace = true } +cosmian_http_client = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +url = { workspace = true } diff --git a/crate/config/README.md b/crate/config/README.md new file mode 100644 index 0000000..c4e3941 --- /dev/null +++ b/crate/config/README.md @@ -0,0 +1,3 @@ +# Cosmian Findex CLI configuration + +The `cosmian_findex_config` contains the configuration of the Cosmian Findex CLI. It is a JSON file that contains the HTTP configuration. diff --git a/crate/client/src/config.rs b/crate/config/src/config.rs similarity index 55% rename from crate/client/src/config.rs rename to crate/config/src/config.rs index 424cc99..65960c4 100644 --- a/crate/client/src/config.rs +++ b/crate/config/src/config.rs @@ -5,18 +5,15 @@ use std::{ path::PathBuf, }; +use cosmian_http_client::HttpClientConfig; use serde::{Deserialize, Serialize}; +#[cfg(target_os = "linux")] use tracing::info; +use tracing::trace; #[cfg(target_os = "linux")] -use crate::client_bail; -use crate::{ - error::{ - result::{ClientResult, RestClientResultHelper}, - ClientError, - }, - RestClient, -}; +use crate::config_bail; +use crate::error::{result::ConfigResultHelper, FindexConfigError}; /// Returns the path to the current user's home folder. /// @@ -49,85 +46,48 @@ fn get_home_folder() -> Option { /// Returns the default configuration path /// or an error if the path cannot be determined -fn get_default_conf_path() -> Result { +fn get_default_conf_path() -> Result { get_home_folder() - .ok_or_else(|| ClientError::NotSupported("unable to determine the home folder".to_owned())) - .map(|home| home.join(".cosmian/findex_client.json")) -} - -/// Required for `serde` serialization -#[allow(clippy::trivially_copy_pass_by_ref)] -const fn not(b: &bool) -> bool { - !*b -} - -/// The configuration that is used by the Login command -/// to perform the `OAuth2` authorize code flow and obtain an access token. -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] -pub struct Oauth2Conf { - /// The client ID of the `OAuth2` application. - /// This is obtained from the `OAuth2` provider. - pub client_id: String, - /// The client secret of the `OAuth2` application. - /// This is obtained from the `OAuth2` provider. - pub client_secret: String, - /// The URL of the `OAuth2` provider's authorization endpoint. - /// For example, for Google, this is `https://accounts.google.com/o/oauth2/v2/auth`. - pub authorize_url: String, - /// The URL of the `OAuth2` provider's token endpoint. - /// For example, for Google, this is `https://oauth2.googleapis.com/token`. - pub token_url: String, - /// The scopes to request. - /// For example, for Google, this is `["openid", "profile"]`. - pub scopes: Vec, + .ok_or_else(|| { + FindexConfigError::NotSupported("unable to determine the home folder".to_owned()) + }) + .map(|home| home.join(".cosmian/findex.json")) } #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] -pub struct ClientConf { - // accept_invalid_certs is useful if the cli needs to connect to an HTTPS Findex server - // running an invalid or unsecure SSL certificate - #[serde(default)] - #[serde(skip_serializing_if = "not")] - pub accept_invalid_certs: bool, - pub findex_server_url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub verified_cert: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub findex_access_token: Option, +pub struct FindexClientConfig { #[serde(skip_serializing_if = "Option::is_none")] - pub ssl_client_pkcs12_path: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ssl_client_pkcs12_password: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub oauth2_conf: Option, + pub conf_path: Option, + pub http_config: HttpClientConfig, } -impl Default for ClientConf { +impl Default for FindexClientConfig { fn default() -> Self { Self { - accept_invalid_certs: false, - findex_server_url: "http://0.0.0.0:6666".to_owned(), - verified_cert: None, - findex_access_token: None, - ssl_client_pkcs12_path: None, - ssl_client_pkcs12_password: None, - oauth2_conf: None, + conf_path: None, + http_config: HttpClientConfig { + server_url: "http://0.0.0.0:6668".to_owned(), + ..HttpClientConfig::default() + }, } } } -/// This method is used to configure the Findex CLI by reading a JSON +/// This method is used to configure the FINDEX CLI by reading a JSON /// configuration file. /// /// The method looks for a JSON configuration file with the following structure: /// /// ```json /// { +/// "http_config": { /// "accept_invalid_certs": false, -/// "findex_server_url": "http://127.0.0.1:6666", -/// "findex_access_token": "AA...AAA", +/// "server_url": "http://127.0.0.1:9998", +/// "access_token": "AA...AAA", +/// "database_secret": "BB...BBB", /// "ssl_client_pkcs12_path": "/path/to/client.p12", /// "ssl_client_pkcs12_password": "password" +/// } /// } /// ``` /// The path to the configuration file is specified through the @@ -135,24 +95,22 @@ impl Default for ClientConf { /// set, a default path is used. If the configuration file does not exist at the /// path, a new file is created with default values. /// -/// This function returns a Findex client configured according to the settings +/// This function returns a FINDEX client configured according to the settings /// specified in the configuration file. pub const FINDEX_CLI_CONF_ENV: &str = "FINDEX_CLI_CONF"; #[cfg(target_os = "linux")] pub(crate) const FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH: &str = "/etc/cosmian/findex.json"; -impl ClientConf { - /// Returns the path to the configuration file. - /// # Errors - /// Returns an error if the configuration file does not exist. - pub fn location(conf: Option) -> ClientResult { +impl FindexClientConfig { + pub fn location(conf: Option) -> Result { + trace!("Getting configuration file location"); // Obtain the configuration file path from: // - the `--conf` arg // - the environment variable corresponding to `FINDEX_CLI_CONF_ENV` // - default to a pre-determined path if let Some(conf_path) = conf { if !conf_path.exists() { - return Err(ClientError::NotSupported(format!( + return Err(FindexConfigError::NotSupported(format!( "Configuration file {conf_path:?} from CLI arg does not exist" ))); } @@ -160,7 +118,7 @@ impl ClientConf { } else if let Ok(conf_path) = env::var(FINDEX_CLI_CONF_ENV).map(PathBuf::from) { // Error if the specified file does not exist if !conf_path.exists() { - return Err(ClientError::NotSupported(format!( + return Err(FindexConfigError::NotSupported(format!( "Configuration file {conf_path:?} specified in {FINDEX_CLI_CONF_ENV} \ environment variable does not exist" ))); @@ -169,6 +127,7 @@ impl ClientConf { } let user_conf_path = get_default_conf_path(); + trace!("User conf path is at: {user_conf_path:?}"); #[cfg(not(target_os = "linux"))] return user_conf_path; @@ -185,7 +144,7 @@ impl ClientConf { ); return Ok(default_system_path); } - client_bail!( + config_bail!( "no configuration found at {FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH}, and no \ current user, bailing out" ); @@ -212,11 +171,7 @@ impl ClientConf { } } - /// Save the configuration to a file. - /// # Errors - /// Returns an error if the configuration cannot be serialized or written to - /// the file. - pub fn save(&self, conf_path: &PathBuf) -> ClientResult<()> { + pub fn save(&self, conf_path: &PathBuf) -> Result<(), FindexConfigError> { fs::write( conf_path, serde_json::to_string_pretty(&self) @@ -229,12 +184,7 @@ impl ClientConf { Ok(()) } - /// Load the configuration from a file. - /// If the file does not exist, create it with default values. - /// # Errors - /// Returns an error if the configuration cannot be deserialized or read - /// from the file. - pub fn load(conf_path: &PathBuf) -> ClientResult { + pub fn load(conf_path: &PathBuf) -> Result { // Deserialize the configuration from the file, or create a default // configuration if none exists let conf = if conf_path.exists() { @@ -260,83 +210,60 @@ impl ClientConf { Ok(conf) } - - /// Initialize a Findex REST client. - /// - /// Parameters `findex_server_url` and `accept_invalid_certs` from the - /// command line will override the ones from the configuration file. - /// # Errors - /// Returns an error if the Findex REST client cannot be instantiated. - pub fn initialize_findex_client( - &self, - findex_server_url: Option<&str>, - accept_invalid_certs: Option, - ) -> Result { - let findex_server_url = findex_server_url.unwrap_or(&self.findex_server_url); - let accept_invalid_certs = accept_invalid_certs.unwrap_or(self.accept_invalid_certs); - - info!( - "Initializing Findex REST client with server URL: {findex_server_url}, \ - accept_invalid_certs: {accept_invalid_certs}" - ); - - // Instantiate a Findex server REST client with the given configuration - let rest_client = RestClient::instantiate( - findex_server_url, - self.findex_access_token.as_deref(), - self.ssl_client_pkcs12_path.as_deref(), - self.ssl_client_pkcs12_password.as_deref(), - accept_invalid_certs, - ) - .with_context(|| { - format!("Unable to instantiate a Findex REST client to server at {findex_server_url}") - })?; - - Ok(rest_client) - } } -#[allow(unsafe_code, clippy::unwrap_used)] #[cfg(test)] mod tests { use std::{env, fs, path::PathBuf}; - use super::{get_default_conf_path, ClientConf, FINDEX_CLI_CONF_ENV}; + use cosmian_logger::log_init; + + use super::{get_default_conf_path, FindexClientConfig, FINDEX_CLI_CONF_ENV}; #[test] pub(crate) fn test_load() { + log_init(None); // valid conf unsafe { env::set_var(FINDEX_CLI_CONF_ENV, "test_data/configs/findex.json"); } - let conf_path = ClientConf::location(None).unwrap(); - ClientConf::load(&conf_path).unwrap(); + let conf_path = FindexClientConfig::location(None).unwrap(); + assert!(FindexClientConfig::load(&conf_path).is_ok()); + + // another valid conf + unsafe { + env::set_var(FINDEX_CLI_CONF_ENV, "test_data/configs/findex_partial.json"); + } + let conf_path = FindexClientConfig::location(None).unwrap(); + assert!(FindexClientConfig::load(&conf_path).is_ok()); // Default conf file unsafe { env::remove_var(FINDEX_CLI_CONF_ENV); } - if get_default_conf_path().unwrap().exists() { - fs::remove_file(get_default_conf_path().unwrap()).unwrap(); - } - let conf_path = ClientConf::location(None).unwrap(); - ClientConf::load(&conf_path).unwrap(); + let _ = fs::remove_file(get_default_conf_path().unwrap()); + let conf_path = FindexClientConfig::location(None).unwrap(); + assert!(FindexClientConfig::load(&conf_path).is_ok()); assert!(get_default_conf_path().unwrap().exists()); // invalid conf unsafe { - env::set_var(FINDEX_CLI_CONF_ENV, "test_data/configs/findex.bad"); + env::set_var(FINDEX_CLI_CONF_ENV, "test_data/configs/findex.bad.json"); } - let conf_path = ClientConf::location(None).unwrap(); - let e = ClientConf::load(&conf_path).err().unwrap().to_string(); - assert!(e.contains("missing field `findex_server_url`")); + let conf_path = FindexClientConfig::location(None).unwrap(); + let e = FindexClientConfig::load(&conf_path) + .err() + .unwrap() + .to_string(); + assert!(e.contains("missing field `server_url`")); // with a file unsafe { env::remove_var(FINDEX_CLI_CONF_ENV); } let conf_path = - ClientConf::location(Some(PathBuf::from("test_data/configs/findex.json"))).unwrap(); - ClientConf::load(&conf_path).unwrap(); + FindexClientConfig::location(Some(PathBuf::from("test_data/configs/findex.json"))) + .unwrap(); + assert!(FindexClientConfig::load(&conf_path).is_ok()); } } diff --git a/crate/config/src/error/mod.rs b/crate/config/src/error/mod.rs new file mode 100644 index 0000000..7a2edc3 --- /dev/null +++ b/crate/config/src/error/mod.rs @@ -0,0 +1,60 @@ +use std::io; + +use thiserror::Error; + +pub(crate) mod result; + +#[derive(Error, Debug)] +pub enum FindexConfigError { + #[error(transparent)] + Base64DecodeError(#[from] base64::DecodeError), + + #[error("Invalid conversion: {0}")] + Conversion(String), + + #[error("{0}")] + Default(String), + + #[error("Not Supported: {0}")] + NotSupported(String), + + #[error("Unexpected Error: {0}")] + UnexpectedError(String), + + #[error(transparent)] + UrlError(#[from] url::ParseError), +} + +impl From for FindexConfigError { + fn from(e: io::Error) -> Self { + Self::Default(e.to_string()) + } +} + +/// Construct a server error from a string. +#[macro_export] +macro_rules! config_error { + ($msg:literal) => { + $crate::FindexConfigError::Default(::core::format_args!($msg).to_string()) + }; + ($err:expr $(,)?) => ({ + $crate::FindexConfigError::Default($err.to_string()) + }); + ($fmt:expr, $($arg:tt)*) => { + $crate::FindexConfigError::Default(::core::format_args!($fmt, $($arg)*).to_string()) + }; +} + +/// Return early with an error if a condition is not satisfied. +#[macro_export] +macro_rules! config_bail { + ($msg:literal) => { + return ::core::result::Result::Err($crate::config_error!($msg)) + }; + ($err:expr $(,)?) => { + return ::core::result::Result::Err($err) + }; + ($fmt:expr, $($arg:tt)*) => { + return ::core::result::Result::Err($crate::config_error!($fmt, $($arg)*)) + }; +} diff --git a/crate/config/src/error/result.rs b/crate/config/src/error/result.rs new file mode 100644 index 0000000..84d3f16 --- /dev/null +++ b/crate/config/src/error/result.rs @@ -0,0 +1,45 @@ +use std::fmt::Display; + +use super::FindexConfigError; + +pub(crate) type ConfigResult = Result; + +#[allow(dead_code)] +pub(crate) trait ConfigResultHelper { + fn context(self, context: &str) -> ConfigResult; + fn with_context(self, op: O) -> ConfigResult + where + D: Display + Send + Sync + 'static, + O: FnOnce() -> D; +} + +impl ConfigResultHelper for Result +where + E: std::error::Error, +{ + fn context(self, context: &str) -> ConfigResult { + self.map_err(|e| FindexConfigError::Default(format!("{context}: {e}"))) + } + + fn with_context(self, op: O) -> ConfigResult + where + D: Display + Send + Sync + 'static, + O: FnOnce() -> D, + { + self.map_err(|e| FindexConfigError::Default(format!("{}: {e}", op()))) + } +} + +impl ConfigResultHelper for Option { + fn context(self, context: &str) -> ConfigResult { + self.ok_or_else(|| FindexConfigError::Default(context.to_string())) + } + + fn with_context(self, op: O) -> ConfigResult + where + D: Display + Send + Sync + 'static, + O: FnOnce() -> D, + { + self.ok_or_else(|| FindexConfigError::Default(format!("{}", op()))) + } +} diff --git a/crate/config/src/lib.rs b/crate/config/src/lib.rs new file mode 100644 index 0000000..ecefe72 --- /dev/null +++ b/crate/config/src/lib.rs @@ -0,0 +1,5 @@ +pub use config::{FindexClientConfig, FINDEX_CLI_CONF_ENV}; +pub use error::FindexConfigError; + +mod config; +mod error; diff --git a/crate/config/test_data/configs/findex.bad.json b/crate/config/test_data/configs/findex.bad.json new file mode 100644 index 0000000..1738935 --- /dev/null +++ b/crate/config/test_data/configs/findex.bad.json @@ -0,0 +1,6 @@ +{ + "conf_path": "test_data/configs/finde.bad", + "http_config": { + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" + } +} diff --git a/crate/config/test_data/configs/findex.json b/crate/config/test_data/configs/findex.json new file mode 100644 index 0000000..ad70f27 --- /dev/null +++ b/crate/config/test_data/configs/findex.json @@ -0,0 +1,7 @@ +{ + "conf_path": "test_data/configs/findex.json", + "http_config": { + "server_url": "http://127.0.0.1:9990", + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" + } +} diff --git a/crate/config/test_data/configs/findex_partial.json b/crate/config/test_data/configs/findex_partial.json new file mode 100644 index 0000000..23d3b4a --- /dev/null +++ b/crate/config/test_data/configs/findex_partial.json @@ -0,0 +1,7 @@ +{ + "conf_path": "test_data/configs/findex_partial.json", + "http_config": { + "server_url": "http://127.0.0.1:9990", + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" + } +} diff --git a/crate/logger/Cargo.toml b/crate/logger/Cargo.toml deleted file mode 100644 index 0568c86..0000000 --- a/crate/logger/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "cosmian_logger" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -[dependencies] -tracing = { workspace = true } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs deleted file mode 100644 index bd08442..0000000 --- a/crate/logger/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod log_utils; - -pub mod reexport { - pub use tracing; - pub use tracing_subscriber; -} diff --git a/crate/logger/src/log_utils.rs b/crate/logger/src/log_utils.rs deleted file mode 100644 index 0ddfab1..0000000 --- a/crate/logger/src/log_utils.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::sync::Once; - -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; - -static LOG_INIT: Once = Once::new(); - -/// # Panics -/// -/// Will panic if we cannot set global tracing subscriber -pub fn log_init(default_value: Option<&str>) { - LOG_INIT.call_once(|| unsafe { - if let Ok(current_value) = std::env::var("RUST_LOG") { - std::env::set_var("RUST_LOG", current_value); - std::env::set_var("RUST_BACKTRACE", "full"); - tracing_setup(); - } else if let Some(input_value) = default_value { - std::env::set_var("RUST_LOG", input_value); - std::env::set_var("RUST_BACKTRACE", "full"); - tracing_setup(); - } - }); -} - -/// # Panics -/// -/// Will panic if: -/// - we cannot set global subscriber -/// - we cannot init the log tracer -fn tracing_setup() { - let format = tracing_subscriber::fmt::layer() - .with_level(true) - .with_target(true) - .with_thread_ids(true) - .with_line_number(true) - .with_file(true) - .with_ansi(true) - .compact(); - - let (filter, _reload_handle) = - tracing_subscriber::reload::Layer::new(EnvFilter::from_default_env()); - - tracing_subscriber::registry() - .with(filter) - .with(format) - .init(); -} diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index 8ca1e12..2e994c5 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -44,7 +44,8 @@ clap = { workspace = true, features = [ "cargo", ] } cloudproof_findex = { workspace = true, features = ["redis-interface"] } -cosmian_logger = { path = "../logger" } +cosmian_findex_structs = { path = "../structs" } +cosmian_logger = { workspace = true } dotenvy = "0.15" futures = "0.3" openssl = { workspace = true, default-features = false } diff --git a/crate/server/src/config/command_line/http_config.rs b/crate/server/src/config/command_line/http_config.rs index f9f6935..0fbb2b4 100644 --- a/crate/server/src/config/command_line/http_config.rs +++ b/crate/server/src/config/command_line/http_config.rs @@ -3,7 +3,7 @@ use std::{fmt::Display, path::PathBuf}; use clap::Args; use serde::{Deserialize, Serialize}; -const DEFAULT_PORT: u16 = 6666; +const DEFAULT_PORT: u16 = 6668; const DEFAULT_HOSTNAME: &str = "0.0.0.0"; #[derive(Args, Clone, Deserialize, Serialize)] diff --git a/crate/server/src/core/implementation.rs b/crate/server/src/core/implementation.rs index 324dac2..8c95e42 100644 --- a/crate/server/src/core/implementation.rs +++ b/crate/server/src/core/implementation.rs @@ -1,7 +1,7 @@ use actix_web::{HttpMessage, HttpRequest}; +use cosmian_findex_structs::Permission; use tracing::{debug, instrument, trace}; -use super::Permission; use crate::{ config::{DbParams, ServerParams}, database::{Database, Redis}, diff --git a/crate/server/src/core/mod.rs b/crate/server/src/core/mod.rs index 3960a1e..f4f5bf6 100644 --- a/crate/server/src/core/mod.rs +++ b/crate/server/src/core/mod.rs @@ -1,5 +1,3 @@ pub(crate) mod implementation; -pub(crate) mod permissions; pub(crate) use implementation::FindexServer; -pub(crate) use permissions::{Permission, Permissions}; diff --git a/crate/server/src/database/database_trait.rs b/crate/server/src/database/database_trait.rs index 55aec66..5031367 100644 --- a/crate/server/src/database/database_trait.rs +++ b/crate/server/src/database/database_trait.rs @@ -5,60 +5,67 @@ use cloudproof_findex::{ TokenToEncryptedValueMap, TokenWithEncryptedValueList, Tokens, ENTRY_LENGTH, LINK_LENGTH, }, }; +use cosmian_findex_structs::{EncryptedEntries, Permission, Permissions, Uuids}; use uuid::Uuid; -use crate::{ - core::{Permission, Permissions}, - error::result::FResult, -}; +use crate::error::result::FResult; #[async_trait] pub(crate) trait Database: Sync + Send { - async fn fetch_entries( + // + // Findex v6 + // + async fn findex_fetch_entries( &self, index_id: &Uuid, tokens: Tokens, ) -> FResult>; - - async fn fetch_chains( + async fn findex_fetch_chains( &self, index_id: &Uuid, tokens: Tokens, ) -> FResult>; - - async fn upsert_entries( + async fn findex_upsert_entries( &self, index_id: &Uuid, upsert_data: UpsertData, ) -> FResult>; - - async fn insert_chains( + async fn findex_insert_chains( &self, index_id: &Uuid, items: TokenToEncryptedValueMap, ) -> FResult<()>; - - async fn delete( + async fn findex_delete( &self, index_id: &Uuid, findex_table: FindexTable, tokens: Tokens, ) -> FResult<()>; + async fn findex_dump_tokens(&self, index_id: &Uuid) -> FResult; - async fn dump_tokens(&self, index_id: &Uuid) -> FResult; - + // + // Permissions + // async fn create_index_id(&self, user_id: &str) -> FResult; - async fn get_permissions(&self, user_id: &str) -> FResult; - async fn get_permission(&self, user_id: &str, index_id: &Uuid) -> FResult; - async fn grant_permission( &self, user_id: &str, permission: Permission, index_id: &Uuid, ) -> FResult<()>; - async fn revoke_permission(&self, user_id: &str, index_id: &Uuid) -> FResult<()>; + + // + // Dataset management + // + async fn dataset_add_entries(&self, index_id: &Uuid, entries: &EncryptedEntries) + -> FResult<()>; + async fn dataset_delete_entries(&self, index_id: &Uuid, uuids: &Uuids) -> FResult<()>; + async fn dataset_get_entries( + &self, + index_id: &Uuid, + uuids: &Uuids, + ) -> FResult; } diff --git a/crate/server/src/database/redis.rs b/crate/server/src/database/redis.rs index cec902b..f06a980 100644 --- a/crate/server/src/database/redis.rs +++ b/crate/server/src/database/redis.rs @@ -1,3 +1,5 @@ +#![allow(clippy::blocks_in_conditions)] //todo(manu): fix it + use std::{ collections::{HashMap, HashSet}, convert::TryFrom, @@ -14,15 +16,13 @@ use cloudproof_findex::{ Tokens, ENTRY_LENGTH, LINK_LENGTH, }, }; +use cosmian_findex_structs::{EncryptedEntries, Permission, Permissions, Uuids}; use redis::{aio::ConnectionManager, pipe, AsyncCommands, Script}; use tracing::{info, instrument, trace}; use uuid::Uuid; use super::Database; -use crate::{ - core::{Permission, Permissions}, - error::{result::FResult, server::FindexServerError}, -}; +use crate::error::{result::FResult, server::FindexServerError}; /// The conditional upsert script used to only update a table if the /// indexed value matches ARGV[2]. When the value does not match, the @@ -41,12 +41,17 @@ const CONDITIONAL_UPSERT_SCRIPT: &str = r" fn build_key(index_id: &Uuid, table: FindexTable, uid: &[u8]) -> Vec { [index_id.as_bytes().as_ref(), &[0x00, u8::from(table)], uid].concat() } +/// Generate a key for the dataset table +fn build_dataset_key(index_id: &Uuid, uid: &Uuid) -> Vec { + [index_id.as_bytes().as_ref(), uid.as_bytes().as_ref()].concat() +} pub(crate) struct Redis { mgr: ConnectionManager, upsert_script: Script, } +//todo(manu): move all test_data in root folder impl Redis { pub(crate) async fn instantiate(redis_url: &str, clear_database: bool) -> FResult { let client = redis::Client::open(redis_url)?; @@ -74,7 +79,7 @@ impl Redis { impl Database for Redis { // todo(manu): merge the 2 fetch #[instrument(ret(Display), err, skip_all)] - async fn fetch_entries( + async fn findex_fetch_entries( &self, index_id: &Uuid, tokens: Tokens, @@ -104,13 +109,11 @@ impl Database for Redis { .collect::, CoreError>>()?; trace!("fetch_entries: non empty tuples len: {}", res.len()); - let result: TokenWithEncryptedValueList = res.into(); - - Ok(result) + Ok(res.into()) } #[instrument(ret(Display), err, skip_all)] - async fn fetch_chains( + async fn findex_fetch_chains( &self, index_id: &Uuid, tokens: Tokens, @@ -146,7 +149,7 @@ impl Database for Redis { } #[instrument(ret(Display), err, skip_all)] - async fn upsert_entries( + async fn findex_upsert_entries( &self, index_id: &Uuid, upsert_data: UpsertData, @@ -197,7 +200,7 @@ impl Database for Redis { } #[instrument(ret, err, skip_all)] - async fn insert_chains( + async fn findex_insert_chains( &self, index_id: &Uuid, items: TokenToEncryptedValueMap, @@ -213,7 +216,7 @@ impl Database for Redis { } #[instrument(ret, err, skip_all)] - async fn delete( + async fn findex_delete( &self, index_id: &Uuid, findex_table: FindexTable, @@ -232,7 +235,7 @@ impl Database for Redis { #[instrument(ret(Display), err, skip_all)] #[allow(clippy::indexing_slicing)] - async fn dump_tokens(&self, index_id: &Uuid) -> FResult { + async fn findex_dump_tokens(&self, index_id: &Uuid) -> FResult { let keys: Vec> = self .mgr .clone() @@ -282,7 +285,7 @@ impl Database for Redis { "No permission for {user_id} since unwrapping serialized value failed" )) })?; - Permissions::deserialize(&serialized_value) + Permissions::deserialize(&serialized_value).map_err(FindexServerError::from) } #[instrument(ret(Display), err, skip(self))] @@ -345,4 +348,61 @@ impl Database for Redis { Ok(()) } + + // + // Dataset management + // + #[instrument(ret, err, skip(self))] + async fn dataset_add_entries( + &self, + index_id: &Uuid, + entries: &EncryptedEntries, + ) -> FResult<()> { + let mut pipe = pipe(); + for (entry_id, data) in entries.iter() { + let key = build_dataset_key(index_id, entry_id); + pipe.set(key, data); + } + pipe.atomic() + .query_async(&mut self.mgr.clone()) + .await + .map_err(FindexServerError::from) + } + + #[instrument(ret, err, skip(self))] + async fn dataset_delete_entries(&self, index_id: &Uuid, uuids: &Uuids) -> FResult<()> { + let mut pipe = pipe(); + for entry_id in uuids.iter() { + let key = build_dataset_key(index_id, entry_id); + pipe.del(key); + } + pipe.atomic() + .query_async(&mut self.mgr.clone()) + .await + .map_err(FindexServerError::from) + } + + #[instrument(ret(Display), err, skip(self))] + async fn dataset_get_entries( + &self, + index_id: &Uuid, + uuids: &Uuids, + ) -> FResult { + let redis_keys = uuids + .iter() + .map(|uid| build_dataset_key(index_id, uid)) + .collect::>(); + trace!("dataset_get_entries: redis_keys len: {}", redis_keys.len()); + + let values: Vec> = self.mgr.clone().mget(redis_keys).await?; + + // Zip and filter empty values out. + let entries = uuids + .iter() + .zip(values) + .filter_map(|(k, v)| if v.is_empty() { None } else { Some((k, v)) }) + .collect::>(); + + Ok(entries.into()) + } } diff --git a/crate/server/src/error/server.rs b/crate/server/src/error/server.rs index a772f13..728441a 100644 --- a/crate/server/src/error/server.rs +++ b/crate/server/src/error/server.rs @@ -5,6 +5,7 @@ use cloudproof_findex::{ db_interfaces::DbInterfaceError, reexport::cosmian_findex::CoreError, ser_de::SerializationError, }; +use cosmian_findex_structs::StructsError; use redis::ErrorKind; use thiserror::Error; use x509_parser::prelude::{PEMError, X509Error}; @@ -50,11 +51,14 @@ pub enum FindexServerError { #[error("Findex Error: {0}")] Findex(String), - #[error("Invalid URL: {0}")] - UrlError(String), + #[error(transparent)] + StructsError(#[from] StructsError), - #[error("Serialization: {0}")] - Deserialization(String), + #[error(transparent)] + SendError(#[from] SendError), + + #[error(transparent)] + UrlParseError(#[from] url::ParseError), } impl From> for FindexServerError { @@ -137,12 +141,6 @@ impl From for FindexServerError { } } -impl From> for FindexServerError { - fn from(e: SendError) -> Self { - Self::ServerError(format!("Failed to send the server handle: {e}")) - } -} - impl From for FindexServerError { fn from(err: redis::RedisError) -> Self { Self::Redis(err.to_string()) @@ -159,12 +157,6 @@ impl From for redis::RedisError { } } -impl From for FindexServerError { - fn from(e: url::ParseError) -> Self { - Self::UrlError(e.to_string()) - } -} - impl From for FindexServerError { fn from(e: SerializationError) -> Self { Self::Findex(e.to_string()) @@ -195,6 +187,12 @@ impl From for FindexServerError { } } +impl From for FindexServerError { + fn from(e: uuid::Error) -> Self { + Self::ConversionError(e.to_string()) + } +} + /// Return early with an error if a condition is not satisfied. /// /// This macro is equivalent to `if !$cond { return Err(From::from($err)); }`. diff --git a/crate/server/src/findex_server.rs b/crate/server/src/findex_server.rs index 371e893..29a3f2e 100644 --- a/crate/server/src/findex_server.rs +++ b/crate/server/src/findex_server.rs @@ -21,8 +21,10 @@ use crate::{ findex_server_bail, middlewares::{extract_peer_certificate, AuthTransformer, JwksManager, JwtConfig, SslAuth}, routes::{ - create_index_id, delete_chains, delete_entries, dump_tokens, fetch_chains, fetch_entries, - get_version, grant_permission, insert_chains, revoke_permission, upsert_entries, + create_index_id, datasets_add_entries, datasets_del_entries, datasets_get_entries, + findex_delete_chains, findex_delete_entries, findex_dump_tokens, findex_fetch_chains, + findex_fetch_entries, findex_insert_chains, findex_upsert_entries, get_version, + grant_permission, revoke_permission, }, }; @@ -248,17 +250,21 @@ pub(crate) async fn prepare_findex_server( // preflight (OPTION) requests. .wrap(Cors::permissive()) // Findex endpoints - .service(fetch_entries) - .service(fetch_chains) - .service(upsert_entries) - .service(insert_chains) - .service(delete_entries) - .service(delete_chains) - .service(dump_tokens) + .service(findex_fetch_entries) + .service(findex_fetch_chains) + .service(findex_upsert_entries) + .service(findex_insert_chains) + .service(findex_delete_entries) + .service(findex_delete_chains) + .service(findex_dump_tokens) // Permissions management endpoints .service(create_index_id) .service(grant_permission) .service(revoke_permission) + // Dataset management + .service(datasets_add_entries) + .service(datasets_del_entries) + .service(datasets_get_entries) // Version endpoint .service(get_version); diff --git a/crate/server/src/main.rs b/crate/server/src/main.rs index e2960b6..1c09632 100644 --- a/crate/server/src/main.rs +++ b/crate/server/src/main.rs @@ -7,7 +7,7 @@ use cosmian_findex_server::{ findex_server::start_findex_server, findex_server_bail, }; -use cosmian_logger::log_utils::log_init; +use cosmian_logger::log_init; use dotenvy::dotenv; use tracing::{debug, info}; diff --git a/crate/server/src/routes/datasets.rs b/crate/server/src/routes/datasets.rs new file mode 100644 index 0000000..7350dc3 --- /dev/null +++ b/crate/server/src/routes/datasets.rs @@ -0,0 +1,106 @@ +use std::sync::Arc; + +use actix_web::{ + post, + web::{self, Bytes, Data, Json}, + HttpRequest, HttpResponse, +}; +use cosmian_findex_structs::{EncryptedEntries, Permission, Uuids}; +use tracing::{info, trace}; + +use crate::{ + core::FindexServer, + error::result::FResult, + routes::{ + check_permission, + error::{ResponseBytes, SuccessResponse}, + get_index_id, + }, +}; + +//todo(manu): routes must be revisited: +// - /{index_id}/datasets/add_entries +// - /{index_id}/findex/add_entries +// - /{index_id}/permission/grant +// - /{index_id}/create +#[post("/datasets/{index_id}/add_entries")] +pub(crate) async fn datasets_add_entries( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> FResult> { + let user = findex_server.get_user(&req); + info!("user {user}: POST /datasets/{index_id}/add_entries"); + check_permission(&user, &index_id, Permission::Read, &findex_server).await?; + + let encrypted_entries = EncryptedEntries::deserialize(&bytes.into_iter().collect::>())?; + trace!( + "add_entries: number of encrypted entries: {}:", + encrypted_entries.len() + ); + + // Collect into a vector to fix the order. + findex_server + .db + .dataset_add_entries(&get_index_id(index_id.as_str())?, &encrypted_entries) + .await?; + + Ok(Json(SuccessResponse { + success: format!( + "{} entries successfully added to index {index_id}", + encrypted_entries.len() + ), + })) +} + +#[post("/datasets/{index_id}/delete_entries")] +pub(crate) async fn datasets_del_entries( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> FResult> { + let user = findex_server.get_user(&req); + info!("user {user}: POST /datasets/{index_id}/delete_entries"); + check_permission(&user, &index_id, Permission::Read, &findex_server).await?; + + let uuids = Uuids::deserialize(&bytes.into_iter().collect::>())?; + trace!("delete_entries: number of uuids: {}:", uuids.len()); + + findex_server + .db + .dataset_delete_entries(&get_index_id(index_id.as_str())?, &uuids) + .await?; + + Ok(Json(SuccessResponse { + success: format!( + "Encrypted entries successfully deleted from index {index_id}. Uuids were {uuids}" + ), + })) +} + +#[post("/datasets/{index_id}/get_entries")] +pub(crate) async fn datasets_get_entries( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> ResponseBytes { + let user = findex_server.get_user(&req); + info!("user {user}: POST /datasets/{index_id}/get_entries",); + check_permission(&user, &index_id, Permission::Write, &findex_server).await?; + + let uuids = Uuids::deserialize(&bytes.into_iter().collect::>())?; + trace!("get_entries: number of uuids: {}:", uuids.len()); + + let encrypted_entries = findex_server + .db + .dataset_get_entries(&get_index_id(index_id.as_str())?, &uuids) + .await?; + + let bytes = encrypted_entries.serialize()?; + Ok(HttpResponse::Ok() + .content_type("application/octet-stream") + .body(bytes)) +} diff --git a/crate/server/src/routes/error.rs b/crate/server/src/routes/error.rs index 3ee12a8..1ff68ab 100644 --- a/crate/server/src/routes/error.rs +++ b/crate/server/src/routes/error.rs @@ -3,6 +3,7 @@ use actix_web::{ web::Json, HttpResponse, HttpResponseBuilder, }; +use serde::{Deserialize, Serialize}; use tracing::{error, warn}; use crate::error::server::FindexServerError; @@ -20,11 +21,12 @@ impl actix_web::error::ResponseError for FindexServerError { | Self::CryptographicError(_) | Self::Redis(_) | Self::Findex(_) + | Self::SendError(_) | Self::Certificate(_) - | Self::Deserialization(_) + | Self::StructsError(_) | Self::ServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::InvalidRequest(_) | Self::ClientConnectionError(_) | Self::UrlError(_) => { + Self::InvalidRequest(_) | Self::ClientConnectionError(_) | Self::UrlParseError(_) => { StatusCode::UNPROCESSABLE_ENTITY } } @@ -45,3 +47,8 @@ impl actix_web::error::ResponseError for FindexServerError { .body(message) } } + +#[derive(Deserialize, Serialize, Debug)] // Debug is required by ok_json() +pub(crate) struct SuccessResponse { + pub success: String, +} diff --git a/crate/server/src/routes/findex.rs b/crate/server/src/routes/findex.rs index 6d91db9..7ea78b1 100644 --- a/crate/server/src/routes/findex.rs +++ b/crate/server/src/routes/findex.rs @@ -12,35 +12,20 @@ use cloudproof_findex::{ }, ser_de::ffi_ser_de::deserialize_token_set, }; -use tracing::{debug, info, trace}; +use cosmian_findex_structs::Permission; +use tracing::{info, trace}; use crate::{ - core::{FindexServer, Permission}, - error::{result::FResult, server::FindexServerError}, + core::FindexServer, routes::{ + check_permission, error::{Response, ResponseBytes}, get_index_id, }, }; -async fn check_permission( - user: &str, - index_id: &str, - expected_permission: Permission, - findex_server: &FindexServer, -) -> FResult<()> { - let permission = findex_server.get_permission(user, index_id).await?; - debug!("check_permission: user {user} has permission {permission} on index {index_id}"); - if permission < expected_permission { - return Err(FindexServerError::Unauthorized(format!( - "User {user} with permission {permission} is not allowed to write on index {index_id}", - ))); - } - Ok(()) -} - #[post("/indexes/{index_id}/fetch_entries")] -pub(crate) async fn fetch_entries( +pub(crate) async fn findex_fetch_entries( req: HttpRequest, index_id: web::Path, bytes: Bytes, @@ -57,7 +42,7 @@ pub(crate) async fn fetch_entries( // Collect into a vector to fix the order. let uids_and_values = findex_server .db - .fetch_entries(&get_index_id(index_id.as_str())?, tokens) + .findex_fetch_entries(&get_index_id(index_id.as_str())?, tokens) .await?; trace!( "fetch_entries: number of uids_and_values: {}:", @@ -73,7 +58,7 @@ pub(crate) async fn fetch_entries( } #[post("/indexes/{index_id}/fetch_chains")] -pub(crate) async fn fetch_chains( +pub(crate) async fn findex_fetch_chains( req: HttpRequest, index_id: web::Path, bytes: Bytes, @@ -89,7 +74,7 @@ pub(crate) async fn fetch_chains( let uids_and_values = findex_server .db - .fetch_chains(&get_index_id(index_id.as_str())?, tokens) + .findex_fetch_chains(&get_index_id(index_id.as_str())?, tokens) .await?; trace!( "fetch_chains: number of uids_and_values: {}:", @@ -104,7 +89,7 @@ pub(crate) async fn fetch_chains( } #[post("/indexes/{index_id}/upsert_entries")] -pub(crate) async fn upsert_entries( +pub(crate) async fn findex_upsert_entries( req: HttpRequest, index_id: web::Path, bytes: Bytes, @@ -121,7 +106,7 @@ pub(crate) async fn upsert_entries( let rejected = findex_server .db - .upsert_entries(&get_index_id(index_id.as_str())?, upsert_data) + .findex_upsert_entries(&get_index_id(index_id.as_str())?, upsert_data) .await?; let bytes = rejected.serialize()?.to_vec(); @@ -131,7 +116,7 @@ pub(crate) async fn upsert_entries( } #[post("/indexes/{index_id}/insert_chains")] -pub(crate) async fn insert_chains( +pub(crate) async fn findex_insert_chains( req: HttpRequest, index_id: web::Path, bytes: Bytes, @@ -147,7 +132,7 @@ pub(crate) async fn insert_chains( findex_server .db - .insert_chains( + .findex_insert_chains( &get_index_id(index_id.as_str())?, token_to_value_encrypted_value_map, ) @@ -157,7 +142,7 @@ pub(crate) async fn insert_chains( } #[post("/indexes/{index_id}/delete_entries")] -pub(crate) async fn delete_entries( +pub(crate) async fn findex_delete_entries( req: HttpRequest, index_id: web::Path, bytes: Bytes, @@ -173,7 +158,7 @@ pub(crate) async fn delete_entries( findex_server .db - .delete( + .findex_delete( &get_index_id(index_id.as_str())?, FindexTable::Entry, tokens, @@ -184,7 +169,7 @@ pub(crate) async fn delete_entries( } #[post("/indexes/{index_id}/delete_chains")] -pub(crate) async fn delete_chains( +pub(crate) async fn findex_delete_chains( req: HttpRequest, index_id: web::Path, bytes: Bytes, @@ -200,7 +185,7 @@ pub(crate) async fn delete_chains( findex_server .db - .delete( + .findex_delete( &get_index_id(index_id.as_str())?, FindexTable::Chain, tokens, @@ -211,7 +196,7 @@ pub(crate) async fn delete_chains( } #[post("/indexes/{index_id}/dump_tokens")] -pub(crate) async fn dump_tokens( +pub(crate) async fn findex_dump_tokens( req: HttpRequest, index_id: web::Path, findex_server: Data>, @@ -223,7 +208,7 @@ pub(crate) async fn dump_tokens( let tokens = findex_server .db - .dump_tokens(&get_index_id(index_id.as_str())?) + .findex_dump_tokens(&get_index_id(index_id.as_str())?) .await?; trace!("dump_tokens: number of tokens: {}:", tokens.len()); diff --git a/crate/server/src/routes/mod.rs b/crate/server/src/routes/mod.rs index 7a648e6..21e679c 100644 --- a/crate/server/src/routes/mod.rs +++ b/crate/server/src/routes/mod.rs @@ -1,13 +1,17 @@ +mod datasets; mod error; mod findex; mod permissions; mod utils; mod version; +pub(crate) use datasets::{datasets_add_entries, datasets_del_entries, datasets_get_entries}; pub(crate) use findex::{ - delete_chains, delete_entries, dump_tokens, fetch_chains, fetch_entries, insert_chains, - upsert_entries, + findex_delete_chains, findex_delete_entries, findex_dump_tokens, findex_fetch_chains, + findex_fetch_entries, findex_insert_chains, findex_upsert_entries, +}; +pub(crate) use permissions::{ + check_permission, create_index_id, grant_permission, revoke_permission, }; -pub(crate) use permissions::{create_index_id, grant_permission, revoke_permission}; pub(crate) use utils::get_index_id; pub(crate) use version::get_version; diff --git a/crate/server/src/routes/permissions.rs b/crate/server/src/routes/permissions.rs index afe983a..0ed0b8b 100644 --- a/crate/server/src/routes/permissions.rs +++ b/crate/server/src/routes/permissions.rs @@ -5,18 +5,29 @@ use actix_web::{ web::{self, Data, Json}, HttpRequest, }; -use serde::{Deserialize, Serialize}; -use tracing::info; +use cosmian_findex_structs::Permission; +use tracing::{debug, info}; use crate::{ - core::{FindexServer, Permission}, + core::FindexServer, error::{result::FResult, server::FindexServerError}, - routes::get_index_id, + routes::{error::SuccessResponse, get_index_id}, }; -#[derive(Deserialize, Serialize, Debug)] // Debug is required by ok_json() -struct SuccessResponse { - pub success: String, +pub(crate) async fn check_permission( + user: &str, + index_id: &str, + expected_permission: Permission, + findex_server: &FindexServer, +) -> FResult<()> { + let permission = findex_server.get_permission(user, index_id).await?; + debug!("check_permission: user {user} has permission {permission} on index {index_id}"); + if permission < expected_permission { + return Err(FindexServerError::Unauthorized(format!( + "User {user} with permission {permission} is not allowed to write on index {index_id}", + ))); + } + Ok(()) } #[post("/create/index")] diff --git a/crate/server/src/routes/utils.rs b/crate/server/src/routes/utils.rs index 1067c35..b1292b9 100644 --- a/crate/server/src/routes/utils.rs +++ b/crate/server/src/routes/utils.rs @@ -1,9 +1,7 @@ use uuid::Uuid; -use crate::error::{result::FResult, server::FindexServerError}; +use crate::error::result::FResult; pub(crate) fn get_index_id(index_id: &str) -> FResult { - Uuid::parse_str(index_id).map_err(|e| { - FindexServerError::Deserialization(format!("Invalid index_id: {index_id}. Error: {e}")) - }) + Ok(Uuid::parse_str(index_id)?) } diff --git a/crate/structs/Cargo.toml b/crate/structs/Cargo.toml new file mode 100644 index 0000000..b859719 --- /dev/null +++ b/crate/structs/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cosmian_findex_structs" # todo(manu): remove prefix `cosmian_findex_` on all crates? +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lib] +# doc test linking as a separate binary is extremely slow +# and is not needed for internal lib +doctest = false + +[features] + +[dependencies] +cloudproof_findex = { workspace = true } +base64 = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } +uuid = { workspace = true } diff --git a/crate/structs/README.md b/crate/structs/README.md new file mode 100644 index 0000000..c4e3941 --- /dev/null +++ b/crate/structs/README.md @@ -0,0 +1,3 @@ +# Cosmian Findex CLI configuration + +The `cosmian_findex_config` contains the configuration of the Cosmian Findex CLI. It is a JSON file that contains the HTTP configuration. diff --git a/crate/structs/src/encrypted_entries.rs b/crate/structs/src/encrypted_entries.rs new file mode 100644 index 0000000..fddc0e1 --- /dev/null +++ b/crate/structs/src/encrypted_entries.rs @@ -0,0 +1,130 @@ +use std::{ + collections::HashMap, + fmt::Display, + ops::{Deref, DerefMut}, +}; + +use base64::{engine::general_purpose, Engine}; +use cloudproof_findex::reexport::cosmian_crypto_core::bytes_ser_de::{Deserializer, Serializer}; +use uuid::Uuid; + +use crate::error::result::StructsResult; + +pub(crate) const UUID_LENGTH: usize = 16; + +#[derive(Debug, PartialEq, Eq)] +pub struct EncryptedEntries { + pub entries: HashMap>, +} + +impl Deref for EncryptedEntries { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.entries + } +} + +impl DerefMut for EncryptedEntries { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.entries + } +} + +impl FromIterator<(Uuid, Vec)> for EncryptedEntries { + fn from_iter)>>(iter: T) -> Self { + Self { + entries: iter.into_iter().collect(), + } + } +} + +impl From>> for EncryptedEntries { + fn from(entries: HashMap<&Uuid, Vec>) -> Self { + Self { + entries: entries.into_iter().map(|(k, v)| (*k, v)).collect(), + } + } +} +impl From> for EncryptedEntries { + fn from(entries: HashMap) -> Self { + Self { + entries: entries.into_iter().map(|(k, v)| (k, v.to_vec())).collect(), + } + } +} +impl From>> for EncryptedEntries { + fn from(entries: HashMap>) -> Self { + Self { + entries: entries.into_iter().collect(), + } + } +} + +impl Display for EncryptedEntries { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (index_id, entry) in &self.entries { + let entry_b64 = general_purpose::STANDARD.encode(entry); + writeln!(f, "Index ID: {index_id}, Entry: {entry_b64}")?; + } + Ok(()) + } +} + +impl Default for EncryptedEntries { + fn default() -> Self { + Self::new() + } +} + +impl EncryptedEntries { + pub fn new() -> Self { + Self { + entries: HashMap::new(), + } + } + + pub fn serialize(&self) -> StructsResult> { + let mut ser = Serializer::with_capacity(self.len()); + ser.write_leb128_u64(self.len() as u64)?; + for (uid, value) in self.iter() { + ser.write_array(uid.as_bytes())?; + ser.write_vec(value)?; + } + Ok(ser.finalize().to_vec()) + } + + pub fn deserialize(bytes: &[u8]) -> StructsResult { + let mut de = Deserializer::new(bytes); + let length = ::try_from(de.read_leb128_u64()?)?; + let mut items = HashMap::with_capacity(length); + for _ in 0..length { + let key = Uuid::from_bytes(de.read_array()?); + let value = de.read_vec()?; + items.insert(key, value); + } + Ok(Self::from(items)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use uuid::Uuid; + + use super::EncryptedEntries; + + #[test] + fn test_encrypted_entries() { + let mut entries = HashMap::new(); + entries.insert(Uuid::new_v4(), vec![1_u8, 2, 3]); + entries.insert(Uuid::new_v4(), vec![4, 5, 6, 7]); + let encrypted_entries = EncryptedEntries::from(entries); + + let serialized = encrypted_entries.serialize().unwrap(); + let deserialized = EncryptedEntries::deserialize(&serialized).unwrap(); + + assert_eq!(encrypted_entries, deserialized); + } +} diff --git a/crate/structs/src/error/mod.rs b/crate/structs/src/error/mod.rs new file mode 100644 index 0000000..a6da948 --- /dev/null +++ b/crate/structs/src/error/mod.rs @@ -0,0 +1,62 @@ +use cloudproof_findex::reexport::cosmian_crypto_core::CryptoCoreError; +use thiserror::Error; + +pub(crate) mod result; + +#[derive(Error, Clone, Debug)] +pub enum StructsError { + #[error(transparent)] + Base64DecodeError(#[from] base64::DecodeError), + + #[error(transparent)] + Uuid(#[from] uuid::Error), + + #[error("{0}")] + Default(String), + + #[error("Indexing slicing: {0}")] + IndexingSlicing(String), + + #[error(transparent)] + UrlError(#[from] url::ParseError), + + #[error(transparent)] + TryFromIntError(#[from] std::num::TryFromIntError), + + #[error("Crypto core error: {0}")] + Crypto(String), +} + +impl From for StructsError { + fn from(err: CryptoCoreError) -> Self { + Self::Crypto(err.to_string()) + } +} + +/// Construct a server error from a string. +#[macro_export] +macro_rules! structs_error { + ($msg:literal) => { + $crate::error::StructsError::Default(::core::format_args!($msg).to_string()) + }; + ($err:expr $(,)?) => ({ + $crate::error::StructsError::Default($err.to_string()) + }); + ($fmt:expr, $($arg:tt)*) => { + $crate::error::StructsError::Default(::core::format_args!($fmt, $($arg)*).to_string()) + }; +} + +/// Return early with an error if a condition is not satisfied. +#[macro_export] +macro_rules! structs_bail { + ($msg:literal) => { + return ::core::result::Result::Err($crate::structs_error!($msg)) + }; + ($err:expr $(,)?) => { + return ::core::result::Result::Err($err) + }; + ($fmt:expr, $($arg:tt)*) => { + return ::core::result::Result::Err($crate::structs_error!($fmt, $($arg)*)) + }; +} diff --git a/crate/structs/src/error/result.rs b/crate/structs/src/error/result.rs new file mode 100644 index 0000000..9ee7a8d --- /dev/null +++ b/crate/structs/src/error/result.rs @@ -0,0 +1,45 @@ +use std::fmt::Display; + +use super::StructsError; + +pub(crate) type StructsResult = Result; + +#[allow(dead_code)] +pub(crate) trait StructsResultHelper { + fn context(self, context: &str) -> StructsResult; + fn with_context(self, op: O) -> StructsResult + where + D: Display + Send + Sync + 'static, + O: FnOnce() -> D; +} + +impl StructsResultHelper for Result +where + E: std::error::Error, +{ + fn context(self, context: &str) -> StructsResult { + self.map_err(|e| StructsError::Default(format!("{context}: {e}"))) + } + + fn with_context(self, op: O) -> StructsResult + where + D: Display + Send + Sync + 'static, + O: FnOnce() -> D, + { + self.map_err(|e| StructsError::Default(format!("{}: {e}", op()))) + } +} + +impl StructsResultHelper for Option { + fn context(self, context: &str) -> StructsResult { + self.ok_or_else(|| StructsError::Default(context.to_string())) + } + + fn with_context(self, op: O) -> StructsResult + where + D: Display + Send + Sync + 'static, + O: FnOnce() -> D, + { + self.ok_or_else(|| StructsError::Default(format!("{}", op()))) + } +} diff --git a/crate/structs/src/lib.rs b/crate/structs/src/lib.rs new file mode 100644 index 0000000..4a811c8 --- /dev/null +++ b/crate/structs/src/lib.rs @@ -0,0 +1,9 @@ +mod encrypted_entries; +mod error; +mod permissions; +mod uuids; + +pub use encrypted_entries::EncryptedEntries; +pub use error::StructsError; +pub use permissions::{Permission, Permissions}; +pub use uuids::Uuids; diff --git a/crate/server/src/core/permissions.rs b/crate/structs/src/permissions.rs similarity index 65% rename from crate/server/src/core/permissions.rs rename to crate/structs/src/permissions.rs index 2097771..9e9b6fb 100644 --- a/crate/server/src/core/permissions.rs +++ b/crate/structs/src/permissions.rs @@ -3,13 +3,31 @@ use std::{collections::HashMap, fmt::Display, str::FromStr}; use uuid::Uuid; use crate::{ - error::{result::FResult, server::FindexServerError}, - findex_server_bail, + error::{result::StructsResult, StructsError}, + structs_bail, }; +// #[derive(Clone, Debug, ValueEnum)] +// pub enum Permission { +// Read = 0, +// Write = 1, +// Admin = 2, +// } + +// impl Display for Permission { +// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +// let s = match self { +// Self::Read => "read", +// Self::Write => "write", +// Self::Admin => "admin", +// }; +// write!(f, "{s}") +// } +// } + #[repr(u8)] #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -pub(crate) enum Permission { +pub enum Permission { Read = 0, Write = 1, Admin = 2, @@ -23,27 +41,27 @@ impl From for u8 { } impl TryFrom for Permission { - type Error = FindexServerError; + type Error = StructsError; - fn try_from(value: u8) -> FResult { + fn try_from(value: u8) -> StructsResult { match value { 0 => Ok(Self::Read), 1 => Ok(Self::Write), 2 => Ok(Self::Admin), - _ => findex_server_bail!("Invalid permission: {}", value), + _ => structs_bail!("Invalid permission: {}", value), } } } impl FromStr for Permission { - type Err = FindexServerError; + type Err = StructsError; - fn from_str(s: &str) -> FResult { + fn from_str(s: &str) -> StructsResult { match s { "read" => Ok(Self::Read), "write" => Ok(Self::Write), "admin" => Ok(Self::Admin), - _ => findex_server_bail!("Invalid permission: {}", s), + _ => structs_bail!("Invalid permission: {}", s), } } } @@ -62,7 +80,7 @@ impl Display for Permission { const PERMISSION_LENGTH: usize = 1; const INDEX_ID_LENGTH: usize = 16; -pub(crate) struct Permissions { +pub struct Permissions { pub permissions: HashMap, } @@ -76,21 +94,21 @@ impl Display for Permissions { } impl Permissions { - pub(crate) fn new(index_id: Uuid, permission: Permission) -> Self { + pub fn new(index_id: Uuid, permission: Permission) -> Self { let mut permissions = HashMap::new(); permissions.insert(index_id, permission); Self { permissions } } - pub(crate) fn grant_permission(&mut self, index_id: Uuid, permission: Permission) { + pub fn grant_permission(&mut self, index_id: Uuid, permission: Permission) { self.permissions.insert(index_id, permission); } - pub(crate) fn revoke_permission(&mut self, index_id: &Uuid) { + pub fn revoke_permission(&mut self, index_id: &Uuid) { self.permissions.remove(index_id); } - pub(crate) fn serialize(&self) -> Vec { + pub fn serialize(&self) -> Vec { let mut bytes = Vec::with_capacity(self.permissions.len() * (PERMISSION_LENGTH + INDEX_ID_LENGTH)); for (index_id, permission) in &self.permissions { @@ -100,24 +118,22 @@ impl Permissions { bytes } - pub(crate) fn deserialize(bytes: &[u8]) -> FResult { + pub fn deserialize(bytes: &[u8]) -> StructsResult { let mut permissions = HashMap::new(); let mut i = 0; while i < bytes.len() { let permission_u8 = bytes.get(i).ok_or_else(|| { - FindexServerError::Deserialization("Failed to deserialize Permission".to_owned()) + StructsError::IndexingSlicing("Failed to deserialize Permission".to_owned()) })?; let permission = Permission::try_from(*permission_u8)?; i += PERMISSION_LENGTH; let uuid_slice = bytes.get(i..i + INDEX_ID_LENGTH).ok_or_else(|| { - FindexServerError::Deserialization( + StructsError::IndexingSlicing( "Failed to extract {INDEX_ID_LENGTH} bytes from Uuid".to_owned(), ) })?; let index_id = Uuid::from_slice(uuid_slice).map_err(|e| { - FindexServerError::Deserialization(format!( - "Failed to deserialize Uuid. Error: {e}" - )) + StructsError::IndexingSlicing(format!("Failed to deserialize Uuid. Error: {e}")) })?; i += INDEX_ID_LENGTH; permissions.insert(index_id, permission); @@ -125,7 +141,7 @@ impl Permissions { Ok(Self { permissions }) } - pub(crate) fn get_permission(&self, index_id: &Uuid) -> Option { + pub fn get_permission(&self, index_id: &Uuid) -> Option { self.permissions.get(index_id).cloned() } } diff --git a/crate/structs/src/uuids.rs b/crate/structs/src/uuids.rs new file mode 100644 index 0000000..dbd984f --- /dev/null +++ b/crate/structs/src/uuids.rs @@ -0,0 +1,79 @@ +use std::{fmt::Display, ops::Deref}; + +use uuid::Uuid; + +use crate::{encrypted_entries::UUID_LENGTH, error::result::StructsResult, StructsError}; + +#[derive(Debug)] +pub struct Uuids { + pub uuids: Vec, +} + +impl Deref for Uuids { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.uuids + } +} + +impl Display for Uuids { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for uuid in &self.uuids { + writeln!(f, "UUID: {uuid}")?; + } + Ok(()) + } +} + +impl From> for Uuids { + fn from(uuids: Vec) -> Self { + Self { uuids } + } +} + +impl Uuids { + pub fn serialize(&self) -> Vec { + let mut bytes = Vec::new(); + for uuid in &self.uuids { + bytes.extend_from_slice(uuid.as_bytes().as_ref()); + } + bytes + } + + pub fn deserialize(bytes: &[u8]) -> StructsResult { + let mut uuids = Vec::new(); + let mut i = 0; + while i < bytes.len() { + let uuid_slice = bytes.get(i..i + UUID_LENGTH).ok_or_else(|| { + StructsError::IndexingSlicing("UUID indexing slicing failed".to_string()) + })?; + let uuid = Uuid::from_slice(uuid_slice)?; + i += UUID_LENGTH; + uuids.push(uuid); + } + Ok(Self { uuids }) + } +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use super::Uuids; + + #[test] + fn test_uuids() { + let uuids = Uuids { + uuids: vec![ + Uuid::new_v4(), + Uuid::new_v4(), + Uuid::new_v4(), + Uuid::new_v4(), + ], + }; + let bytes = uuids.serialize(); + let deserialized_uuids = Uuids::deserialize(&bytes).unwrap(); + assert_eq!(uuids.uuids, deserialized_uuids.uuids); + } +} diff --git a/crate/test_server/Cargo.toml b/crate/test_server/Cargo.toml index 0a9124c..56c9306 100644 --- a/crate/test_server/Cargo.toml +++ b/crate/test_server/Cargo.toml @@ -17,10 +17,12 @@ actix-server = { workspace = true } cosmian_findex_server = { path = "../server", features = [ "insecure", ], default-features = false } -cosmian_logger = { path = "../logger" } -cosmian_rest_client = { path = "../client" } +cosmian_logger = { workspace = true } +cosmian_findex_client = { path = "../client" } tokio = { workspace = true, features = ["rt-multi-thread"] } tracing = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } [dev-dependencies] criterion = { version = "0.5", features = [ diff --git a/crate/test_server/certificates/README.md b/crate/test_server/certificates/README.md index e6f3e53..49c8799 100644 --- a/crate/test_server/certificates/README.md +++ b/crate/test_server/certificates/README.md @@ -17,5 +17,5 @@ RUST_LOG="cosmian=debug" cargo run --bin cosmian_findex_server -- \ The following command will test a client connection with client cert authentication: ```sh -curl -k --cert ./crate/cli/test_data/certificates/owner.client.cosmian.com.crt --key ./crate/cli/test_data/certificates/owner.client.cosmian.com.key https://localhost:6666/version +curl -k --cert ./crate/cli/test_data/certificates/owner.client.cosmian.com.crt --key ./crate/cli/test_data/certificates/owner.client.cosmian.com.key https://localhost:6668/version ``` diff --git a/crate/test_server/src/test_server.rs b/crate/test_server/src/test_server.rs index d6a7803..2b4bb22 100644 --- a/crate/test_server/src/test_server.rs +++ b/crate/test_server/src/test_server.rs @@ -1,21 +1,24 @@ use std::{ - env, - path::PathBuf, + env, fs, + path::{Path, PathBuf}, sync::mpsc, thread::{self, JoinHandle}, time::Duration, }; use actix_server::ServerHandle; +use cosmian_findex_client::{ + findex_client_bail, findex_client_error, + reexport::{cosmian_findex_config::FindexClientConfig, cosmian_http_client::HttpClientConfig}, + FindexClient, FindexClientError, +}; use cosmian_findex_server::{ config::{ ClapConfig, DBConfig, DatabaseType, HttpConfig, HttpParams, JwtAuthConfig, ServerParams, }, findex_server::start_findex_server, }; -use cosmian_rest_client::{ - client_bail, client_error, write_json_object_to_file, ClientConf, ClientError, RestClient, -}; +use serde::Serialize; use tokio::sync::OnceCell; use tracing::{info, trace}; @@ -29,6 +32,28 @@ use crate::test_jwt::{get_auth0_jwt_config, AUTH0_TOKEN}; pub(crate) static ONCE: OnceCell = OnceCell::const_new(); pub(crate) static ONCE_SERVER_WITH_AUTH: OnceCell = OnceCell::const_new(); +/// Write all bytes to a file +pub(crate) fn write_bytes_to_file( + bytes: &[u8], + file: &impl AsRef, +) -> Result<(), FindexClientError> { + fs::write(file, bytes) + .map_err(|e| findex_client_error!(format!("failed writing the bytes to the file: {e}"))) +} + +/// Write a JSON object to a file +pub(crate) fn write_json_object_to_file( + json_object: &T, + file: &impl AsRef, +) -> Result<(), FindexClientError> +where + T: Serialize, +{ + let bytes = serde_json::to_vec::(json_object) + .map_err(|e| findex_client_error!("failed serializing the JSON object to bytes: {e}"))?; + write_bytes_to_file(&bytes, file) +} + fn redis_db_config() -> DBConfig { let url = if let Ok(var_env) = env::var("REDIS_HOST") { format!("redis://{var_env}:6379") @@ -58,7 +83,7 @@ pub async fn start_default_test_findex_server() -> &'static TestsContext { ONCE.get_or_try_init(|| { start_test_server_with_options( get_db_config(), - 6666, + 6668, AuthenticationOptions { use_jwt_token: false, use_https: false, @@ -76,7 +101,7 @@ pub async fn start_default_test_findex_server_with_cert_auth() -> &'static Tests .get_or_try_init(|| { start_test_server_with_options( get_db_config(), - 6668, + 6660, AuthenticationOptions { use_jwt_token: false, use_https: true, @@ -91,17 +116,17 @@ pub async fn start_default_test_findex_server_with_cert_auth() -> &'static Tests pub struct TestsContext { pub owner_client_conf_path: String, pub user_client_conf_path: String, - pub owner_client_conf: ClientConf, + pub owner_client_conf: FindexClientConfig, pub server_handle: ServerHandle, - pub thread_handle: JoinHandle>, + pub thread_handle: JoinHandle>, } impl TestsContext { - pub async fn stop_server(self) -> Result<(), ClientError> { + pub async fn stop_server(self) -> Result<(), FindexClientError> { self.server_handle.stop(false).await; self.thread_handle .join() - .map_err(|_e| client_error!("failed joining the stop thread"))? + .map_err(|_e| findex_client_error!("failed joining the stop thread"))? } } @@ -116,17 +141,17 @@ pub async fn start_test_server_with_options( db_config: DBConfig, port: u16, authentication_options: AuthenticationOptions, -) -> Result { - cosmian_logger::log_utils::log_init(None); +) -> Result { + cosmian_logger::log_init(None); let server_params = generate_server_params(db_config.clone(), port, &authentication_options)?; // Create a (object owner) conf let (owner_client_conf_path, owner_client_conf) = generate_owner_conf(&server_params)?; - let findex_client = owner_client_conf.initialize_findex_client(None, None)?; + let findex_client = FindexClient::new(owner_client_conf.clone())?; info!( "Starting Findex test server at URL: {} with server params {:?}", - owner_client_conf.findex_server_url, &server_params + owner_client_conf.http_config.server_url, &server_params ); let (server_handle, thread_handle) = start_test_findex_server(server_params); @@ -152,7 +177,7 @@ pub async fn start_test_server_with_options( /// Start a test Findex server with the given config in a separate thread fn start_test_findex_server( server_params: ServerParams, -) -> (ServerHandle, JoinHandle>) { +) -> (ServerHandle, JoinHandle>) { let (tx, rx) = mpsc::channel::(); let thread_handle = thread::spawn(move || { @@ -161,7 +186,7 @@ fn start_test_findex_server( .enable_all() .build()? .block_on(start_findex_server(server_params, Some(tx))) - .map_err(|e| ClientError::UnexpectedError(e.to_string())) + .map_err(|e| FindexClientError::UnexpectedError(e.to_string())) }); trace!("Waiting for test Findex server to start..."); let server_handle = rx @@ -172,7 +197,7 @@ fn start_test_findex_server( } /// Wait for the server to start by reading the version -async fn wait_for_server_to_start(findex_client: &RestClient) -> Result<(), ClientError> { +async fn wait_for_server_to_start(findex_client: &FindexClient) -> Result<(), FindexClientError> { // Depending on the running environment, the server could take a bit of time to // start We try to query it with a dummy request until be sure it is // started. @@ -191,7 +216,7 @@ async fn wait_for_server_to_start(findex_client: &RestClient) -> Result<(), Clie waiting *= 2; } else { info!("The server is still not up, stop trying"); - client_bail!("Can't start the Findex server to run tests"); + findex_client_bail!("Can't start the Findex server to run tests"); } } else { info!("UP!"); @@ -238,7 +263,7 @@ fn generate_server_params( db_config: DBConfig, port: u16, authentication_options: &AuthenticationOptions, -) -> Result { +) -> Result { // Configure the server let clap_config = ClapConfig { auth: if authentication_options.use_jwt_token { @@ -254,8 +279,9 @@ fn generate_server_params( ), ..ClapConfig::default() }; - ServerParams::try_from(clap_config) - .map_err(|e| ClientError::Default(format!("failed initializing the server config: {e}"))) + ServerParams::try_from(clap_config).map_err(|e| { + FindexClientError::Default(format!("failed initializing the server config: {e}")) + }) } fn set_access_token(server_params: &ServerParams) -> Option { @@ -267,43 +293,50 @@ fn set_access_token(server_params: &ServerParams) -> Option { } } -fn generate_owner_conf(server_params: &ServerParams) -> Result<(String, ClientConf), ClientError> { +fn generate_owner_conf( + server_params: &ServerParams, +) -> Result<(String, FindexClientConfig), FindexClientError> { // This create root dir let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); // Create a conf let owner_client_conf_path = format!("/tmp/owner_findex_{}.json", server_params.port); - let owner_client_conf = ClientConf { - findex_server_url: if matches!(server_params.http_params, HttpParams::Https(_)) { - format!("https://0.0.0.0:{}", server_params.port) - } else { - format!("http://0.0.0.0:{}", server_params.port) - }, - accept_invalid_certs: true, - findex_access_token: set_access_token(server_params), - ssl_client_pkcs12_path: if server_params.authority_cert_file.is_some() { - #[cfg(not(target_os = "macos"))] - let p = root_dir.join("certificates/owner/owner.client.acme.com.p12"); - #[cfg(target_os = "macos")] - let p = root_dir.join("certificates/owner/owner.client.acme.com.old.format.p12"); - Some( - p.to_str() - .ok_or_else(|| ClientError::Default("Can't convert path to string".to_owned()))? - .to_string(), - ) - } else { - None - }, - ssl_client_pkcs12_password: if server_params.authority_cert_file.is_some() { - Some("password".to_owned()) - } else { - None + let owner_client_conf = FindexClientConfig { + http_config: HttpClientConfig { + server_url: if matches!(server_params.http_params, HttpParams::Https(_)) { + format!("https://0.0.0.0:{}", server_params.port) + } else { + format!("http://0.0.0.0:{}", server_params.port) + }, + accept_invalid_certs: true, + access_token: set_access_token(server_params), + ssl_client_pkcs12_path: if server_params.authority_cert_file.is_some() { + #[cfg(not(target_os = "macos"))] + let p = root_dir.join("certificates/owner/owner.client.acme.com.p12"); + #[cfg(target_os = "macos")] + let p = root_dir.join("certificates/owner/owner.client.acme.com.old.format.p12"); + Some( + p.to_str() + .ok_or_else(|| { + FindexClientError::Default("Can't convert path to string".to_owned()) + })? + .to_string(), + ) + } else { + None + }, + ssl_client_pkcs12_password: if server_params.authority_cert_file.is_some() { + Some("password".to_owned()) + } else { + None + }, + ..Default::default() }, // We use the private key since the private key is the public key with additional // information. - ..ClientConf::default() + ..FindexClientConfig::default() }; // write the conf to a file write_json_object_to_file(&owner_client_conf, &owner_client_conf_path) @@ -314,23 +347,28 @@ fn generate_owner_conf(server_params: &ServerParams) -> Result<(String, ClientCo /// Generate a user configuration for user.client@acme.com and return the file /// path -fn generate_user_conf(port: u16, owner_client_conf: &ClientConf) -> Result { +fn generate_user_conf( + port: u16, + owner_client_conf: &FindexClientConfig, +) -> Result { // This create root dir let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let mut user_conf = owner_client_conf.clone(); - user_conf.ssl_client_pkcs12_path = { + user_conf.http_config.ssl_client_pkcs12_path = { #[cfg(not(target_os = "macos"))] let p = root_dir.join("certificates/user/user.client.acme.com.p12"); #[cfg(target_os = "macos")] let p = root_dir.join("certificates/user/user.client.acme.com.old.format.p12"); Some( p.to_str() - .ok_or_else(|| ClientError::Default("Can't convert path to string".to_owned()))? + .ok_or_else(|| { + FindexClientError::Default("Can't convert path to string".to_owned()) + })? .to_string(), ) }; - user_conf.ssl_client_pkcs12_password = Some("password".to_owned()); + user_conf.http_config.ssl_client_pkcs12_password = Some("password".to_owned()); // write the user conf let user_conf_path = format!("/tmp/user_findex_{port}.json"); @@ -342,7 +380,7 @@ fn generate_user_conf(port: u16, owner_client_conf: &ClientConf) -> Result Result<(), ClientError> { + async fn test_server_auth_matrix() -> Result<(), FindexClientError> { let test_cases = vec![ (false, false, false, "all_disabled"), (true, false, false, "https_no_auth"), diff --git a/rust-toolchain b/rust-toolchain index a872b04..f2cf302 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -stable-2024-10-17 +nightly-2024-06-09