diff --git a/.github/scripts/cargo_build.sh b/.github/scripts/cargo_build.sh index da21b06..9ad2dfa 100644 --- a/.github/scripts/cargo_build.sh +++ b/.github/scripts/cargo_build.sh @@ -24,7 +24,9 @@ fi rustup target add "$TARGET" # shellcheck disable=SC2086 -cargo build --target $TARGET $RELEASE $FEATURES +cargo build --target $TARGET $RELEASE + +export RUST_LOG="cosmian_findex_cli=trace,cosmian_findex_server=trace,test_findex_server=trace" # shellcheck disable=SC2086 cargo test --target $TARGET $RELEASE --workspace -- --nocapture $SKIP_SERVICES_TESTS diff --git a/.github/scripts/loop.sh b/.github/scripts/loop.sh new file mode 100644 index 0000000..98bd89e --- /dev/null +++ b/.github/scripts/loop.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -ex + +cargo build + +# export RUST_LOG="cosmian_findex_cli=trace,cosmian_findex_server=trace,test_findex_server=trace" + +echo "Running tests in an infinite loop" +while true; do + reset + # cargo test --workspace -- --nocapture + cargo nextest run --workspace --nocapture + sleep 1 +done diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index f749326..b93f04f 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -39,7 +39,7 @@ jobs: archive-name: ${{ matrix.archive-name }} target: ${{ matrix.target }} debug_or_release: ${{ inputs.debug_or_release }} - skip_services_tests: --skip test_redis + skip_services_tests: --skip test_findex --skip test_all_authentications --skip test_server_auth_matrix generic-macos: strategy: @@ -58,7 +58,7 @@ jobs: archive-name: ${{ matrix.archive-name }} target: ${{ matrix.target }} debug_or_release: ${{ inputs.debug_or_release }} - skip_services_tests: --skip test_redis + skip_services_tests: --skip test_findex --skip test_all_authentications --skip test_server_auth_matrix cleanup: needs: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69ca600..3797039 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: nightly-2024-06-09 + toolchain: stable-2024-10-17 build_tests: uses: ./.github/workflows/build_all.yml secrets: inherit with: - toolchain: nightly-2024-06-09 + toolchain: stable-2024-10-17 debug_or_release: debug diff --git a/.github/workflows/main_release.yml b/.github/workflows/main_release.yml index 30a1c58..41818bf 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: nightly-2024-06-09 + toolchain: stable-2024-10-17 build: uses: ./.github/workflows/build_all.yml secrets: inherit with: - toolchain: nightly-2024-06-09 + toolchain: stable-2024-10-17 debug_or_release: release diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9563295..c571df5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,24 @@ # pre-commit install # pre-commit install --install-hooks -t commit-msg # pre-commit autoupdate - -# See https://pre-commit.com for more information -# See https://pre-commit.com/hooks.html for more hooks -exclude: tests_data +# +# (optional) Creating a virtual environment +# ``` +# sudo pip3 install virtualenv +# virtualenv venv +# source venv/bin/activate +# ``` +# Known issues: +# - If markdownlint-cli fails to install, try installing node js LTS : +# first, install nvm according to the docs : https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating +# then, run the following commands: +# ``` +# nvm install --lts +# nvm use --lts +# ``` +# - If docker-compose-up fails to install, try installing docker-compose and installing the latest docker version. +# If you get an error related to unavailable ports, you might have an instance of redis-server running on the same port. If so, stop the redis-server with the following command: `sudo systemctl stop redis-server.service` and relaunch the pre-commit hooks. +exclude: crate/client/datasets/users.json repos: - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.4.0 @@ -107,14 +121,16 @@ repos: args: [--skip-string-normalization] - repo: https://github.com/Cosmian/git-hooks.git - rev: v1.0.29 + rev: v1.0.31 hooks: + # - id: nightly-cargo-format - id: cargo-format # - id: dprint-toml-fix # - id: cargo-upgrade # - id: cargo-update - id: cargo-machete - id: docker-compose-up + - id: cargo-build-kms - id: cargo-test - id: clippy-autofix-unreachable-pub - id: clippy-autofix-all-targets-all-features diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..7f4d443 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,58 @@ +# Specifies which edition is used by the parser. +# Default value: "2015" +edition = "2021" + +# How imports should be grouped into use statements. Imports will be merged or split to the configured level of granularity. +# Default value: Preserve +imports_granularity = "Crate" + +# Format the metavariable matching patterns in macros. +# Default value: false +format_macro_matchers = true + +# Format string literals where necessary +# Default value: false +format_strings = true + +# Break comments to fit on the line +# Default value: false +# Possible values: true, false +wrap_comments = true + +# Convert /* */ comments to // comments where possible +# Default value: false +# Possible values: true, false +# normalize_comments = true + +# Reorder impl items. type and const are put first, then macros and methods. +# Default value: false +reorder_impl_items = true + +# Controls the strategy for how imports are grouped together. +# Default value: Preserve +group_imports = "StdExternalCrate" + +# Add trailing semicolon after break, continue and return +# Default value: true +trailing_semicolon = false + +# Enable unstable features on the unstable channel. +# Default value: false +unstable_features = true + +# Use field initialize shorthand if possible. +# Default value: false +use_field_init_shorthand = true + +# Which version of the formatting rules to use. Version::One is backwards-compatible with Rustfmt 1.0. Other versions are only backwards compatible within a major version number. +# Default value: "One" +version = "Two" + +# # Controls the edition of the Rust Style Guide to use for formatting (RFC 3338) +# Default value: "2015" +# style_edition = "2021" + +# The following rust files listing have been made in october 2021. +# This listing allows us first to ignore all rust files formmatting. +# Then we can remove progressively from this list the files we want to format +ignore = [] diff --git a/Cargo.lock b/Cargo.lock index 721e7a7..f7fbf28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,33 +386,12 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "argon2" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" -dependencies = [ - "base64ct", - "blake2", - "cpufeatures", - "password-hash", -] - [[package]] name = "asn1-rs" version = "0.6.2" @@ -509,12 +488,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - [[package]] name = "base64" version = "0.13.1" @@ -560,15 +533,6 @@ dependencies = [ "serde", ] -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -578,15 +542,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-padding" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" -dependencies = [ - "generic-array", -] - [[package]] name = "bstr" version = "1.10.0" @@ -631,15 +586,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher", -] - [[package]] name = "cc" version = "1.1.30" @@ -655,30 +601,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.38" @@ -729,7 +651,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", - "zeroize", ] [[package]] @@ -770,112 +691,22 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" -[[package]] -name = "cloudproof" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e4a805ae87855dc2676adb766401a0cb45d1e88eb7298ea6ffedc9dc8f58e7" -dependencies = [ - "cloudproof_aesgcm", - "cloudproof_anonymization", - "cloudproof_cover_crypt", - "cloudproof_ecies", - "cloudproof_findex 6.0.2", - "cloudproof_fpe", - "cosmian_crypto_core", -] - -[[package]] -name = "cloudproof_aesgcm" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f2755b9f0f157a2a8816cb88663af7741c72df12fba115ba1f98e6e7077b15" -dependencies = [ - "cosmian_crypto_core", -] - -[[package]] -name = "cloudproof_anonymization" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4c483a4f8ce9ebd178dfc8578b327d195ff7d44cbdab5bb171a8c8e8c7290f" -dependencies = [ - "argon2", - "base64 0.21.7", - "chrono", - "cosmian_crypto_core", - "hex", - "rand", - "rand_distr", - "regex", - "sha2", - "tiny-keccak", -] - -[[package]] -name = "cloudproof_cover_crypt" -version = "14.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b957408fa6185ed0a2140da5df66cd37326da478a11a6c1a9ac07650d58121" -dependencies = [ - "cosmian_cover_crypt", - "cosmian_crypto_core", - "serde_json", -] - -[[package]] -name = "cloudproof_ecies" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d90e7789327a566109776551d69980264647d3aab290116b8b90f3bae8c53a7" -dependencies = [ - "cosmian_crypto_core", -] - -[[package]] -name = "cloudproof_findex" -version = "5.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25dad7ff37557df4cb27a20d80727d75dfa2ddcbed14f02761e8bfb1f595741" -dependencies = [ - "async-trait", - "cosmian_crypto_core", - "cosmian_findex 5.0.3", - "redis", - "serde", - "serde_json", - "thiserror", - "tokio", - "tracing", -] - [[package]] name = "cloudproof_findex" version = "6.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648998ca94264039683dd60818940cb461790cd0f62b7823caaa814869d2c7f1" +source = "git+https://www.github.com/Cosmian/cloudproof_rust?branch=feat/add_basic_findex_rest_client#cd1649c39d0e0c89e4815aa25f867d11721c4ce5" dependencies = [ "async-trait", + "base64 0.21.7", "cosmian_crypto_core", - "cosmian_findex 6.0.0", + "cosmian_findex", + "redis", + "reqwest", "serde", "tracing", "tracing-subscriber", ] -[[package]] -name = "cloudproof_fpe" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f1457fe34566c626b37275061dcc88dd71ebf06ebd3d7ee62c6b0b37f2c426" -dependencies = [ - "aes", - "cosmian_fpe", - "itertools", - "num-bigint", - "num-traits", -] - [[package]] name = "combine" version = "4.6.7" @@ -904,9 +735,6 @@ name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -dependencies = [ - "arbitrary", -] [[package]] name = "convert_case" @@ -948,20 +776,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cosmian_cover_crypt" -version = "14.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2162c569b1b72a9b18929bb6b6effdc59ef12101276559bbb7a01c3f08056b2e" -dependencies = [ - "cosmian_crypto_core", - "pqc_kyber", - "serde", - "serde_json", - "tiny-keccak", - "zeroize", -] - [[package]] name = "cosmian_crypto_core" version = "9.5.0" @@ -970,54 +784,18 @@ checksum = "1fec20750102fb599eea419a4167ff42f0b6770e6a187abdbbd4d45d01e576e1" dependencies = [ "aead", "aes-gcm", - "blake2", - "chacha20", - "chacha20poly1305", - "crypto_box", - "curve25519-dalek", - "digest", - "ed25519-dalek", - "elliptic-curve", "getrandom", "leb128", - "p192", - "p224", - "p256", - "p384", - "pkcs8", "rand_chacha", "rand_core", - "rsa", - "sha1", - "sha2", - "sha3", - "signature", - "tiny-keccak", - "uuid", - "x509-cert", - "zeroize", -] - -[[package]] -name = "cosmian_findex" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cb51a6ded995165d6343c3b74d6f25191a02cbd6a2148a1403c0583a04e9e94" -dependencies = [ - "async-trait", - "base64 0.21.7", - "cosmian_crypto_core", - "never", - "rand", "tiny-keccak", "zeroize", ] [[package]] name = "cosmian_findex" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac33fe1817637e96a87727995829d536c7d4554421d3ff2809cc39997bc1f91" +version = "6.0.1" +source = "git+https://www.github.com/Cosmian/findex?branch=fix/missing_some_structs_serialization#b1d8d021aa19a6bbd3726513e1755432bc1742dd" dependencies = [ "async-trait", "base64 0.21.7", @@ -1038,10 +816,11 @@ dependencies = [ "assert_cmd", "base64 0.21.7", "clap", - "cloudproof", + "cloudproof_findex", "const-oid", - "cosmian_findex_client", "cosmian_logger", + "cosmian_rest_client", + "csv", "der", "hex", "oauth2", @@ -1058,29 +837,10 @@ dependencies = [ "tokio", "tracing", "url", + "uuid", "x509-parser", ] -[[package]] -name = "cosmian_findex_client" -version = "0.1.0" -dependencies = [ - "base64 0.21.7", - "cloudproof", - "der", - "log", - "pem", - "reqwest", - "rustls", - "serde", - "serde_json", - "thiserror", - "tracing", - "url", - "webpki-roots 0.22.6", - "x509-cert", -] - [[package]] name = "cosmian_findex_server" version = "0.1.0" @@ -1091,58 +851,54 @@ dependencies = [ "actix-tls", "actix-web", "alcoholic_jwt", - "argon2", "async-trait", "base64 0.21.7", "chrono", "clap", - "cloudproof", - "cloudproof_findex 5.0.4", + "cloudproof_findex", "cosmian_logger", "dotenvy", "futures", - "hex", - "lazy_static", - "num-bigint-dig", - "num_cpus", "openssl", - "rawsql", "redis", "reqwest", "serde", "serde_json", "sqlx", "thiserror", - "time", "tokio", "toml", "tracing", "url", + "uuid", "x509-parser", - "zeroize", ] [[package]] -name = "cosmian_fpe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8146536a868bcda32bab8d40da8696668beae1e8075f773478cd5663e856baf1" +name = "cosmian_logger" +version = "0.1.0" dependencies = [ - "cbc", - "cipher", - "libm", - "num-bigint", - "num-integer", - "num-traits", - "static_assertions", + "tracing", + "tracing-subscriber", ] [[package]] -name = "cosmian_logger" +name = "cosmian_rest_client" version = "0.1.0" dependencies = [ + "base64 0.21.7", + "clap", + "der", + "faker_rand", + "pem", + "rand", + "reqwest", + "serde", + "serde_json", + "thiserror", "tracing", - "tracing-subscriber", + "url", + "uuid", ] [[package]] @@ -1226,18 +982,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -1250,33 +994,24 @@ dependencies = [ ] [[package]] -name = "crypto_box" -version = "0.9.1" +name = "csv" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" dependencies = [ - "aead", - "blake2", - "crypto_secretbox", - "curve25519-dalek", - "salsa20", - "subtle", - "zeroize", + "csv-core", + "itoa", + "ryu", + "serde", ] [[package]] -name = "crypto_secretbox" -version = "0.1.1" +name = "csv-core" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" dependencies = [ - "aead", - "cipher", - "generic-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", + "memchr", ] [[package]] @@ -1288,33 +1023,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "data-encoding" version = "2.6.0" @@ -1327,10 +1035,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ - "arbitrary", "const-oid", - "der_derive", - "flagset", "pem-rfc7468", "zeroize", ] @@ -1349,17 +1054,6 @@ 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" @@ -1369,17 +1063,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "derive_more" version = "0.99.18" @@ -1393,6 +1076,12 @@ 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" @@ -1412,65 +1101,27 @@ dependencies = [ ] [[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "pkcs8", - "signature", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "ed25519-dalek" -version = "2.1.1" +name = "doc-comment" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" @@ -1481,27 +1132,6 @@ dependencies = [ "serde", ] -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - [[package]] name = "encoding_rs" version = "0.8.34" @@ -1550,32 +1180,21 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" - -[[package]] -name = "ff" -version = "0.13.0" +name = "faker_rand" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "300d2ddbf2245b5b5e723995e0961033121b4fc2be9045fb661af82bd739ffb6" dependencies = [ - "rand_core", - "subtle", + "deunicode", + "lazy_static", + "rand", ] [[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "flagset" -version = "0.4.6" +name = "fastrand" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "float-cmp" @@ -1588,9 +1207,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -1735,7 +1354,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -1767,17 +1385,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - [[package]] name = "h2" version = "0.3.26" @@ -1919,9 +1526,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -2003,9 +1610,9 @@ dependencies = [ [[package]] name = "impl-more" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" +checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" [[package]] name = "indexmap" @@ -2023,7 +1630,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "block-padding", "generic-array", ] @@ -2068,15 +1674,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -2100,15 +1697,15 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libsqlite3-sys" @@ -2291,7 +1888,6 @@ dependencies = [ "num-iter", "num-traits", "rand", - "serde", "smallvec", "zeroize", ] @@ -2332,25 +1928,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "oauth2" version = "4.4.2" @@ -2409,9 +1986,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -2441,9 +2018,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -2457,54 +2034,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "p192" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b0533bc6c238f2669aab8db75ae52879dc74e88d6bd3685bd4022a00fa85cd2" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sec1", -] - -[[package]] -name = "p224" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c06436d66652bc2f01ade021592c80a2aad401570a18aa18b82e440d2b9aa1" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - [[package]] name = "parking" version = "2.2.1" @@ -2534,33 +2063,12 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - [[package]] name = "pem" version = "3.0.4" @@ -2629,21 +2137,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "aes", - "cbc", - "der", - "pbkdf2", - "scrypt", - "sha2", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -2651,8 +2144,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", - "pkcs5", - "rand_core", "spki", ] @@ -2662,17 +2153,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - [[package]] name = "polyval" version = "0.6.2" @@ -2700,15 +2180,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "pqc_kyber" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b79004a05337e54e8ffc0ec7470e40fa26eca6fe182968ec2b803247f2283c" -dependencies = [ - "rand_core", -] - [[package]] name = "predicates" version = "3.1.2" @@ -2739,20 +2210,11 @@ dependencies = [ "termtree", ] -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -2796,22 +2258,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand", -] - -[[package]] -name = "rawsql" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f70b36c8415361d34d90adf494f45bc18e568f909b5946d27c79559ddd812fa8" - [[package]] name = "redis" version = "0.23.3" @@ -2936,20 +2382,10 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.4", + "webpki-roots", "winreg", ] -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - [[package]] name = "ring" version = "0.17.8" @@ -3059,15 +2495,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -3092,17 +2519,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "pbkdf2", - "salsa20", - "sha2", -] - [[package]] name = "sct" version = "0.7.1" @@ -3113,20 +2529,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -3158,18 +2560,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ "proc-macro2", "quote", @@ -3178,11 +2580,10 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ - "indexmap", "itoa", "memchr", "ryu", @@ -3248,16 +2649,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -3345,7 +2736,6 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "arbitrary", "base64ct", "der", ] @@ -3555,12 +2945,6 @@ dependencies = [ "url", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stringprep" version = "0.1.5" @@ -3580,9 +2964,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -3651,11 +3035,10 @@ name = "test_findex_server" version = "0.1.0" dependencies = [ "actix-server", - "cosmian_findex_client", "cosmian_findex_server", "cosmian_logger", + "cosmian_rest_client", "criterion", - "tempfile", "tokio", "tracing", "zeroize", @@ -3663,18 +3046,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", @@ -3699,9 +3082,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", @@ -3758,32 +3139,11 @@ 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.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -4051,9 +3411,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", ] @@ -4193,25 +3553,6 @@ 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" @@ -4435,21 +3776,6 @@ 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 = [ - "arbitrary", - "const-oid", - "der", - "sha1", - "signature", - "spki", - "tls_codec", -] - [[package]] name = "x509-parser" version = "0.16.0" diff --git a/Cargo.toml b/Cargo.toml index a56729b..2293817 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,11 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" -rust-version = "1.71.0" -authors = ["Hatem M'naouer "] +rust-version = "1.82.0" +authors = [ + "Emmanuel Coste", + "Hatem M'naouer ", +] license = "BUSL-1.1" license-file = "LICENSE" repository = "https://github.com/Cosmian/cosmian_findex_server" @@ -38,37 +41,22 @@ incremental = false opt-level = 0 [workspace.dependencies] -actix-rt = "2.10" actix-server = { version = "2.5", default-features = false } actix-web = { version = "4.9.0", default-features = false } base64 = "0.21" -chrono = "0.4" clap = { version = "4.5", default-features = false } -cloudproof = "3.0" +# 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 } -env_logger = "0.11" -hex = { version = "0.4", default-features = false } -leb128 = "0.2.5" -log = { version = "0.4", default-features = false } -native-tls = "0.2" -num_cpus = "1.13" -num-bigint-dig = { version = "0.8", default-features = false } openssl = { version = "0.10", default-features = false } pem = "3.0" -pyo3 = { version = "0.20", default-features = false } reqwest = { version = "0.11", default-features = false } -rustls = "0.21" -serde = "1.0" -serde_json = "1.0" -sha3 = { version = "0.10", default-features = false } -strum = { version = "0.25", default-features = false } -thiserror = "1.0" -time = "0.3" -tokio = { version = "1.39", default-features = false } -tracing-subscriber = { version = "0.3", default-features = false } +serde = "1.0.213" +serde_json = "1.0.132" +thiserror = "1.0.65" +tokio = { version = "1.41", default-features = false } tracing = "0.1" url = "2.5" -uuid = "1.10" -x509-cert = { version = "0.2.5", default-features = false } x509-parser = "0.16" zeroize = { version = "1.8", default-features = false } +uuid = { version = "1.10", features = ["v4"] } diff --git a/LICENSE b/LICENSE index 71030ae..6e2b18e 100644 --- a/LICENSE +++ b/LICENSE @@ -4,7 +4,7 @@ License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. Parameters Licensor: Cosmian Tech SAS. -Licensed Work: Findex server version 0.2.0 or later. +Licensed Work: Findex server version 0.1.0 or later. The Licensed Work is (c) 2024 Cosmian Tech SAS. Additional Use Grant: You may use the Licensed Work in production, provided your total use of does not exceed a total of 4 vCPUS on virtual diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml index 19daa27..ab318ca 100644 --- a/crate/cli/Cargo.toml +++ b/crate/cli/Cargo.toml @@ -9,7 +9,7 @@ rust-version.workspace = true description = "CLI used to manage the Cosmian Findex." [[bin]] -name = "findex" +name = "cosmian_findex_cli" path = "src/main.rs" test = false @@ -32,11 +32,12 @@ clap = { workspace = true, features = [ "derive", "cargo", ] } -cloudproof = { workspace = true } -cosmian_findex_client = { path = "../client" } +cloudproof_findex = { workspace = true, features = ["rest-interface"] } cosmian_logger = { path = "../logger" } +cosmian_rest_client = { path = "../client" } +csv = "1.3.0" der = { workspace = true, features = ["pem"] } -hex = { workspace = true } +hex = "0.4" oauth2 = { version = "4.4", features = ["reqwest"] } pem = { workspace = true } reqwest = { workspace = true } @@ -46,15 +47,16 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } url = { workspace = true } +uuid = { workspace = true, features = ["v4"] } [dev-dependencies] -actix-rt = { workspace = true } +actix-rt = "2.10" actix-server = { workspace = true } assert_cmd = "2.0" const-oid = { version = "0.9", features = ["db"] } -test_findex_server = { path = "../test_server" } openssl = { workspace = true } predicates = "3.1" -regex = { version = "1.10", default-features = false } -tempfile = "3.11" +regex = { version = "1.11", default-features = false } +tempfile = "3.13" +test_findex_server = { path = "../test_server" } x509-parser = { workspace = true, features = ["verify"] } diff --git a/crate/cli/src/actions/console.rs b/crate/cli/src/actions/console.rs index 1cdb03a..14e225d 100644 --- a/crate/cli/src/actions/console.rs +++ b/crate/cli/src/actions/console.rs @@ -2,7 +2,7 @@ use serde::Serialize; use crate::error::result::CliResult; -pub const KMS_CLI_FORMAT: &str = "KMS_CLI_FORMAT"; +pub const FINDEX_CLI_FORMAT: &str = "FINDEX_CLI_FORMAT"; pub const CLI_DEFAULT_FORMAT: &str = "text"; pub const CLI_JSON_FORMAT: &str = "json"; @@ -15,7 +15,7 @@ impl Stdout { #[must_use] pub fn new(stdout: &str) -> Self { Self { - stdout: stdout.to_string(), + stdout: stdout.to_owned(), } } @@ -27,8 +27,8 @@ impl Stdout { #[allow(clippy::print_stdout)] pub fn write(&self) -> CliResult<()> { // Check if the output format should be JSON - let json_format_from_env = std::env::var(KMS_CLI_FORMAT) - .unwrap_or_else(|_| CLI_DEFAULT_FORMAT.to_string()) + let json_format_from_env = std::env::var(FINDEX_CLI_FORMAT) + .unwrap_or_else(|_| CLI_DEFAULT_FORMAT.to_owned()) .to_lowercase() == CLI_JSON_FORMAT; diff --git a/crate/cli/src/actions/findex/add_or_delete.rs b/crate/cli/src/actions/findex/add_or_delete.rs new file mode 100644 index 0000000..2bc6649 --- /dev/null +++ b/crate/cli/src/actions/findex/add_or_delete.rs @@ -0,0 +1,118 @@ +use std::{ + collections::{HashMap, HashSet}, + fs::File, + path::PathBuf, +}; + +use clap::Parser; +use cloudproof_findex::reexport::cosmian_findex::{ + Data, IndexedValue, IndexedValueToKeywordsMap, Keyword, +}; +use cosmian_rest_client::RestClient; +use tracing::{instrument, trace}; + +use super::FindexParameters; +use crate::{ + actions::{console, findex::instantiate_findex}, + error::result::CliResult, +}; + +#[derive(Parser, Debug)] +#[clap(verbatim_doc_comment)] +pub struct AddOrDeleteAction { + #[clap(flatten)] + pub(crate) findex_parameters: FindexParameters, + + /// The path to the CSV file containing the data to index + #[clap(long)] + pub(crate) csv: PathBuf, +} + +impl AddOrDeleteAction { + /// Converts a CSV file to a hashmap where the keys are indexed values and + /// the values are sets of keywords. + /// + /// # Errors + /// + /// This function will return an error if: + /// - The CSV file cannot be opened. + /// - There is an error reading the CSV records. + /// - There is an error converting the CSV records to the expected data + /// types. + #[instrument(ret(Display), err, skip(self))] + pub(crate) fn csv_to_hashmap(&self) -> CliResult { + // read the database + let mut csv_in_memory = Vec::new(); + let file = File::open(self.csv.clone())?; + let mut rdr = csv::Reader::from_reader(file); + for result in rdr.byte_records() { + // The iterator yields Result, so we check the + // error here. + let record = result?; + let indexed_value: IndexedValue = + IndexedValue::Data(Data::from(record.as_slice())); + let keywords = record.iter().map(Keyword::from).collect::>(); + csv_in_memory.push((indexed_value, keywords)); + trace!("CSV line: {record:?}"); + } + let result: HashMap, HashSet> = + csv_in_memory.iter().cloned().collect(); + Ok(IndexedValueToKeywordsMap::from(result)) + } + + #[allow(clippy::future_not_send)] + /// Adds the data from the CSV file to the Findex index. + /// + /// # Errors + /// + /// This function will return an error if: + /// - There is an error instantiating the Findex client. + /// - There is an error retrieving the user key or label from the Findex + /// parameters. + /// - 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<()> { + let keywords = instantiate_findex(rest_client, &self.findex_parameters.index_id) + .await? + .add( + &self.findex_parameters.user_key()?, + &self.findex_parameters.label(), + self.csv_to_hashmap()?, + ) + .await?; + trace!("indexing done: keywords: {keywords}"); + + console::Stdout::new(&format!("indexing done: keywords: {keywords}")).write()?; + + Ok(()) + } + + #[allow(clippy::future_not_send)] + /// Deletes the data from the CSV file from the Findex index. + /// + /// # Errors + /// + /// This function will return an error if: + /// - There is an error instantiating the Findex client. + /// - There is an error retrieving the user key or label from the Findex + /// parameters. + /// - 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<()> { + let keywords = instantiate_findex(rest_client, &self.findex_parameters.index_id) + .await? + .delete( + &self.findex_parameters.user_key()?, + &self.findex_parameters.label(), + self.csv_to_hashmap()?, + ) + .await?; + trace!("deleting keywords done: {keywords}"); + + console::Stdout::new(&format!("deleting keywords done: {keywords}")).write()?; + + Ok(()) + } +} diff --git a/crate/cli/src/actions/findex/mod.rs b/crate/cli/src/actions/findex/mod.rs new file mode 100644 index 0000000..30e07a9 --- /dev/null +++ b/crate/cli/src/actions/findex/mod.rs @@ -0,0 +1,57 @@ +use clap::Parser; +use cloudproof_findex::{ + reexport::{ + cosmian_crypto_core::FixedSizeCBytes, + cosmian_findex::{Label, UserKey}, + }, + Configuration, InstantiatedFindex, +}; +use cosmian_rest_client::RestClient; +use tracing::debug; +use uuid::Uuid; + +use crate::error::result::CliResult; + +pub mod add_or_delete; +pub mod search; + +#[derive(Parser, Debug)] +#[clap(verbatim_doc_comment)] +pub(crate) 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')] + pub key: String, + /// The Findex label + #[clap(long, short = 'l')] + pub label: String, + /// The index ID + #[clap(long, short = 'i')] + pub index_id: Uuid, +} + +impl FindexParameters { + pub(crate) fn user_key(&self) -> CliResult { + Ok(UserKey::try_from_slice(&hex::decode(self.key.clone())?)?) + } + + pub(crate) fn label(&self) -> Label { + Label::from(self.label.as_str()) + } +} + +#[allow(clippy::future_not_send)] +pub(crate) async fn instantiate_findex( + rest_client: RestClient, + index_id: &Uuid, +) -> CliResult { + let config = Configuration::Rest( + rest_client.client, + rest_client.server_url.clone(), + rest_client.server_url, + index_id.to_string(), + ); + let findex = InstantiatedFindex::new(config).await?; + debug!("Findex instantiated"); + Ok(findex) +} diff --git a/crate/cli/src/actions/findex/search.rs b/crate/cli/src/actions/findex/search.rs new file mode 100644 index 0000000..722f6be --- /dev/null +++ b/crate/cli/src/actions/findex/search.rs @@ -0,0 +1,57 @@ +use clap::Parser; +use cloudproof_findex::reexport::cosmian_findex::{Keyword, Keywords}; +use cosmian_rest_client::RestClient; +use tracing::trace; + +use super::FindexParameters; +use crate::{ + actions::{console, findex::instantiate_findex}, + error::result::CliResult, +}; + +/// Findex: Search keywords. +#[derive(Parser, Debug)] +#[clap(verbatim_doc_comment)] +pub struct SearchAction { + #[clap(flatten)] + pub(crate) findex_parameters: FindexParameters, + + /// The word to search. Can be repeated. + #[clap(long)] + pub(crate) keyword: Vec, +} + +impl SearchAction { + /// Search indexed keywords. + /// + /// # Arguments + /// + /// * `rest_client` - The Findex server client instance used to communicate + /// with the Findex server server. + /// + /// # Errors + /// + /// 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<()> { + let findex = instantiate_findex(rest_client, &self.findex_parameters.index_id).await?; + let results = findex + .search( + &self.findex_parameters.user_key()?, + &self.findex_parameters.label(), + self.keyword + .clone() + .into_iter() + .map(|word| Keyword::from(word.as_bytes())) + .collect::(), + &|_| async move { Ok(false) }, + ) + .await?; + + console::Stdout::new(&results.to_string()).write()?; + trace!("Search results: {results}"); + + Ok(()) + } +} diff --git a/crate/cli/src/actions/login.rs b/crate/cli/src/actions/login.rs index daa294d..e4c0cdb 100644 --- a/crate/cli/src/actions/login.rs +++ b/crate/cli/src/actions/login.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + convert::TryFrom, path::PathBuf, sync::mpsc::{self, Sender}, thread, @@ -11,7 +12,7 @@ use actix_web::{ App, HttpResponse, HttpServer, }; use clap::Parser; -use cosmian_findex_client::ClientConf; +use cosmian_rest_client::ClientConf; use oauth2::{ basic::BasicClient, http::{ @@ -30,17 +31,23 @@ use crate::{ error::{result::CliResult, CliError}, }; -/// Login to the Identity Provider of the Findex server using the `OAuth2` authorization code flow. +/// 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. +/// 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. /// -/// The configuration file must contain an `oauth2_conf` object with the following fields: -/// - `client_id`: The client ID of your application. This is provided by the Identity Provider. -/// - `client_secret`: The client secret of your application. This is provided by the Identity Provider. +/// The configuration file must contain an `oauth2_conf` object with the +/// following fields: +/// - `client_id`: The client ID of your application. This is provided by the +/// Identity Provider. +/// - `client_secret`: The client secret of your application. This is provided +/// by the Identity Provider. /// - `authorize_url`: The authorization URL of the provider. For example, for Google it is `https://accounts.google.com/o/oauth2/v2/auth`. /// - `token_url`: The token URL of the provider. For example, for Google it is `https://oauth2.googleapis.com/token`. -/// - `scopes`: The scopes to request. For example, for Google it is `["openid", "email"]`. +/// - `scopes`: The scopes to request. For example, for Google it is `["openid", +/// "email"]`. /// /// The callback url must be authorized on the Identity Provider with value `http://localhost:17899/token`. #[derive(Parser, Debug)] @@ -49,10 +56,12 @@ pub struct LoginAction; impl LoginAction { /// This function processes the login action. - /// It loads the client configuration from the specified path, retrieves the `OAuth2` configuration, - /// initializes the login state, prompts the user to browse to the authorization URL, - /// finalizes the login process by receiving the authorization code and exchanging it for an access token, - /// updates the configuration with the access token, and saves the configuration to the specified path. + /// It loads the client configuration from the specified path, retrieves the + /// `OAuth2` configuration, initializes the login state, prompts the + /// user to browse to the authorization URL, finalizes the login process + /// by receiving the authorization code and exchanging it for an access + /// token, updates the configuration with the access token, and saves + /// the configuration to the specified path. /// /// # Arguments /// @@ -62,11 +71,15 @@ impl LoginAction { /// /// This function can return a `CliError` in the following cases: /// - /// * The `login` command requires an Identity Provider (`IdP`) that must be configured in the `oauth2_conf` object in the client configuration file. + /// * The `login` command requires an Identity Provider (`IdP`) that must be + /// configured in the `oauth2_conf` object in the client configuration + /// file. /// * The client configuration file cannot be loaded. - /// * The `OAuth2` configuration is missing or invalid in the client configuration file. + /// * The `OAuth2` configuration is missing or invalid in the client + /// configuration file. /// * The authorization URL cannot be parsed. - /// * The authorization code is not received or does not match the CSRF token. + /// * The authorization code is not received or does not match the CSRF + /// token. /// * The access token cannot be requested from the Identity Provider. /// * The token exchange request fails. /// * The token exchange response cannot be parsed. @@ -121,15 +134,16 @@ pub struct Oauth2LoginConfig { } /// 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. +/// 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 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. +/// 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. @@ -146,22 +160,28 @@ pub struct LoginState { /// 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 +/// 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. +/// 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("KMS_CLI_OAUTH2_REDIRECT_URL_PORT") { + 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 KMS_CLI_OAUTH2_REDIRECT_URL_PORT: {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 KMS_CLI_OAUTH2_REDIRECT_URL_PORT: {e:?}")) + CliError::Default(format!( + "Invalid FINDEX_CLI_OAUTH2_REDIRECT_URL_PORT: {e:?}" + )) })?; } @@ -210,15 +230,18 @@ 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. + /// 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 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. @@ -226,12 +249,14 @@ impl LoginState { /// * 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 + // 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`. + // 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()))?; @@ -248,7 +273,8 @@ impl LoginState { // 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 + // 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())) @@ -256,7 +282,8 @@ impl LoginState { // .set_pkce_verifier(login_state.pkce_verifier) // .request_async(async_http_client) // .await - // .map_err(|e| CliError::Default(format!("token exchange failed: {:?}", e)))?; + // .map_err(|e| CliError::Default(format!("token exchange failed: {:?}", + // e)))?; let token_result = request_token( &self.login_config, @@ -273,8 +300,11 @@ impl LoginState { }) } - /// 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. + /// 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 @@ -326,17 +356,19 @@ pub struct OAuthResponse { /// 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. +/// 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. +/// * `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. +/// * `authorization_code` - The authorization code received from the Identity +/// Provider. /// /// # Errors /// diff --git a/crate/cli/src/actions/logout.rs b/crate/cli/src/actions/logout.rs index 2b1a456..bc30173 100644 --- a/crate/cli/src/actions/logout.rs +++ b/crate/cli/src/actions/logout.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::Parser; -use cosmian_findex_client::ClientConf; +use cosmian_rest_client::ClientConf; use crate::error::result::CliResult; @@ -21,15 +21,18 @@ impl LogoutAction { /// /// # Errors /// - /// Returns an error if there is an issue loading or saving the configuration file. - /// + /// 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)?; - println!("\nThe access token was removed from the Findex server configuration file: {conf_path:?}"); + println!( + "\nThe access token was removed from the Findex server configuration file: \ + {conf_path:?}" + ); Ok(()) } diff --git a/crate/cli/src/actions/markdown.rs b/crate/cli/src/actions/markdown.rs index 2357d65..1c04524 100644 --- a/crate/cli/src/actions/markdown.rs +++ b/crate/cli/src/actions/markdown.rs @@ -17,7 +17,8 @@ impl MarkdownAction { /// /// # Errors /// - /// Returns an error if there is an issue creating or writing to the markdown file. + /// Returns an error if there is an issue creating or writing to the + /// markdown file. #[allow(clippy::print_stdout)] pub fn process(&self, cmd: &Command) -> CliResult<()> { let mut output = String::new(); @@ -144,7 +145,7 @@ fn write_subcommands<'a>( format!("{}.{}", parent_index, i + 1) }; let full_command = if parent_command.is_empty() { - sub_command.get_name().to_string() + sub_command.get_name().to_owned() } else { format!("{} {}", parent_command, sub_command.get_name()) }; diff --git a/crate/cli/src/actions/mod.rs b/crate/cli/src/actions/mod.rs index c435e64..802bc76 100644 --- a/crate/cli/src/actions/mod.rs +++ b/crate/cli/src/actions/mod.rs @@ -1,6 +1,7 @@ pub mod console; +pub mod findex; pub mod login; pub mod logout; pub mod markdown; -pub mod new_database; +pub mod permissions; pub mod version; diff --git a/crate/cli/src/actions/new_database.rs b/crate/cli/src/actions/new_database.rs deleted file mode 100644 index 2151de4..0000000 --- a/crate/cli/src/actions/new_database.rs +++ /dev/null @@ -1,52 +0,0 @@ -use clap::Parser; -use cosmian_findex_client::FindexClient; - -use crate::error::result::{CliResult, CliResultHelper}; - -/// Initialize a new user encrypted database and return the secret (`SQLCipher` only). -/// -/// This secret is only displayed once and is not stored anywhere on the server. -/// The secret must be set in the `kms_database_secret` property -/// of the CLI `findex.json` configuration file to use the encrypted database. -/// -/// Passing the correct secret "auto-selects" the correct encrypted database: -/// multiple encrypted databases can be used concurrently on the same Findex server server. -/// -/// Note: this action creates a new database: it will not return the secret -/// of the last created database and will not overwrite it. -#[derive(Parser, Debug)] -#[clap(verbatim_doc_comment)] -pub struct NewDatabaseAction; - -impl NewDatabaseAction { - /// Process the `NewDatabaseAction` by querying the Findex server to get a new database. - /// - /// # Arguments - /// - /// * `findex_rest_client` - The Findex server client used to communicate with the Findex server server. - /// - /// # Errors - /// - /// Returns an error if the query execution on the Findex server server fails. - /// - #[allow(clippy::print_stdout)] - pub async fn process(&self, findex_rest_client: &FindexClient) -> CliResult<()> { - // Query the Findex server to get a new database - let token = findex_rest_client - .new_database() - .await - .with_context(|| "Can't execute the query on the findex server")?; - - println!( - "A new user encrypted database is configured. Use the following token (by adding it \ - to the 'kms_database_secret' entry of your KMS_CLI_CONF):\n\n{token}\n\n" - ); - - println!( - "Do not loose it: there is not other copy!\nIt is impossible to recover the database \ - without the token." - ); - - Ok(()) - } -} diff --git a/crate/cli/src/actions/permissions.rs b/crate/cli/src/actions/permissions.rs new file mode 100644 index 0000000..b2a6420 --- /dev/null +++ b/crate/cli/src/actions/permissions.rs @@ -0,0 +1,153 @@ +use clap::Parser; +use cosmian_rest_client::{Permission, RestClient}; +use uuid::Uuid; + +use crate::{ + actions::console, + error::result::{CliResult, CliResultHelper}, +}; + +/// Manage the users permissions to the indexes +#[derive(Parser, Debug)] +pub enum PermissionsAction { + Create(CreateIndex), + Grant(GrantPermission), + Revoke(RevokePermission), +} + +impl PermissionsAction { + /// Processes the permissions action. + /// + /// # Arguments + /// + /// * `rest_client` - The Findex client used for the action. + /// + /// # Errors + /// + /// Returns an error if there was a problem running the action. + pub async fn process(&self, rest_client: RestClient) -> CliResult<()> { + match self { + Self::Create(action) => action.run(rest_client).await?, + Self::Grant(action) => action.run(rest_client).await?, + Self::Revoke(action) => action.run(rest_client).await?, + }; + + Ok(()) + } +} + +/// Create a new index. It results on an `admin` permission on a new index. +/// +/// Users can have 1 permission on multiple indexes +#[derive(Parser, Debug)] +pub struct CreateIndex; + +impl CreateIndex { + /// Create a new Index with a default `admin` permission. + /// + /// Generates an unique index ID which is returned to the owner. + /// This ID will be shared between several users that will be able to: + /// * index new keywords with their own datasets + /// * or search keywords in the index + /// + /// # Arguments + /// + /// * `rest_client` - A reference to the Findex client used to communicate + /// with the Findex server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the Findex server fails. + pub async fn run(&self, rest_client: RestClient) -> CliResult { + let response = rest_client + .create_index_id() + .await + .with_context(|| "Can't execute the create index id query on the findex server")?; + // should replace the user configuration file + console::Stdout::new(&response.success).write()?; + + Ok(response.success) + } +} + +/// Grant permission on a index. +/// +/// This command can only be called by the owner of the index. It allows to +/// grant: +/// * `read` permission: the user can only read the index +/// * `write` permission: the user can read and write the index +/// * `admin` permission: the user can read, write and grant permission to the +/// index +#[derive(Parser, Debug)] +pub struct GrantPermission { + /// The user identifier to allow + #[clap(long, required = true)] + pub user: String, + + /// The index ID + #[clap(long, required = true)] + pub index_id: Uuid, + + #[clap(long, required = true)] + pub permission: Permission, +} + +impl GrantPermission { + /// Runs the `GrantPermission` action. + /// + /// # Arguments + /// + /// * `rest_client` - A reference to the Findex client used to communicate + /// with the Findex server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the Findex server fails. + pub async fn run(&self, rest_client: RestClient) -> CliResult { + let response = rest_client + .grant_permission(&self.user, &self.permission, &self.index_id) + .await + .with_context(|| "Can't execute the grant permission query on the findex server")?; + + console::Stdout::new(&response.success).write()?; + + Ok(response.success) + } +} + +/// Revoke user permission. +/// +/// This command can only be called by the owner of the index. +#[derive(Parser, Debug)] +pub struct RevokePermission { + /// The user identifier to revoke + #[clap(long, required = true)] + pub user: String, + + /// The index id + #[clap(long, required = true)] + pub index_id: Uuid, +} + +impl RevokePermission { + /// Runs the `RevokePermission` action. + /// + /// # Arguments + /// + /// * `rest_client` - A reference to the Findex client used to communicate + /// with the Findex server. + /// + /// # Errors + /// + /// Returns an error if the query execution on the Findex server fails. + pub async fn run(&self, rest_client: RestClient) -> CliResult { + let response = rest_client + .revoke_permission(&self.user, &self.index_id) + .await + .with_context(|| "Can't execute the revoke permission query on the findex server")?; + + console::Stdout::new(&response.success).write()?; + + Ok(response.success) + } +} diff --git a/crate/cli/src/actions/version.rs b/crate/cli/src/actions/version.rs index 2ecf28b..c5ea090 100644 --- a/crate/cli/src/actions/version.rs +++ b/crate/cli/src/actions/version.rs @@ -1,5 +1,5 @@ use clap::Parser; -use cosmian_findex_client::FindexClient; +use cosmian_rest_client::RestClient; use super::console; use crate::error::result::{CliResult, CliResultHelper}; @@ -14,13 +14,15 @@ impl ServerVersionAction { /// /// # Arguments /// - /// * `findex_rest_client` - The Findex server client instance used to communicate with the Findex server server. + /// * `rest_client` - The Findex server client instance used to communicate + /// with the Findex server server. /// /// # Errors /// - /// Returns an error if the version query fails or if there is an issue writing to the console. - pub async fn process(&self, findex_rest_client: &FindexClient) -> CliResult<()> { - let version = findex_rest_client + /// 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<()> { + let version = rest_client .version() .await .with_context(|| "Can't execute the version query on the findex server")?; diff --git a/crate/cli/src/error/mod.rs b/crate/cli/src/error/mod.rs index 0866e3f..1a63e5c 100644 --- a/crate/cli/src/error/mod.rs +++ b/crate/cli/src/error/mod.rs @@ -2,7 +2,11 @@ use std::{array::TryFromSliceError, num::TryFromIntError, str::Utf8Error}; #[cfg(test)] use assert_cmd::cargo::CargoError; -use cosmian_findex_client::ClientError; +use cloudproof_findex::{ + db_interfaces::DbInterfaceError, + reexport::{cosmian_crypto_core::CryptoCoreError, cosmian_findex}, +}; +use cosmian_rest_client::ClientError; use hex::FromHexError; use pem::PemError; use thiserror::Error; @@ -37,11 +41,11 @@ pub enum CliError { ServerError(String), // Any actions of the user which is not allowed - #[error("Access denied: {0}")] + #[error("Permission denied: {0}")] Unauthorized(String), // A cryptographic error - #[error("Cryptographic error: {0}")] + #[error("CLI Cryptographic error: {0}")] Cryptographic(String), // Conversion errors @@ -75,18 +79,6 @@ impl From for CliError { } } -impl From for CliError { - fn from(e: cloudproof::reexport::crypto_core::reexport::pkcs8::Error) -> Self { - Self::Conversion(e.to_string()) - } -} - -impl From for CliError { - fn from(e: cloudproof::reexport::cover_crypt::Error) -> Self { - Self::InvalidRequest(e.to_string()) - } -} - impl From for CliError { fn from(e: TryFromSliceError) -> Self { Self::Conversion(e.to_string()) @@ -166,6 +158,36 @@ impl From for CliError { } } +impl From for CliError { + fn from(e: CryptoCoreError) -> Self { + Self::Cryptographic(e.to_string()) + } +} + +impl From> for CliError { + fn from(e: cosmian_findex::Error) -> Self { + Self::Cryptographic(e.to_string()) + } +} + +impl From for CliError { + fn from(e: DbInterfaceError) -> Self { + Self::Cryptographic(e.to_string()) + } +} + +impl From for CliError { + fn from(e: csv::Error) -> Self { + Self::Conversion(e.to_string()) + } +} + +impl From for CliError { + fn from(e: uuid::Error) -> Self { + Self::Conversion(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)); }`. @@ -217,6 +239,7 @@ macro_rules! cli_bail { } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use crate::error::result::CliResult; diff --git a/crate/cli/src/error/result.rs b/crate/cli/src/error/result.rs index 01bf489..82779f9 100644 --- a/crate/cli/src/error/result.rs +++ b/crate/cli/src/error/result.rs @@ -43,7 +43,7 @@ where impl CliResultHelper for Option { fn context(self, context: &str) -> CliResult { - self.ok_or_else(|| CliError::Default(context.to_string())) + self.ok_or_else(|| CliError::Default(context.to_owned())) } fn with_context(self, op: O) -> CliResult diff --git a/crate/cli/src/lib.rs b/crate/cli/src/lib.rs index bd2f7b4..cd81fe6 100644 --- a/crate/cli/src/lib.rs +++ b/crate/cli/src/lib.rs @@ -16,9 +16,27 @@ clippy::nursery, // restriction lints - clippy::map_err_ignore, + 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::redundant_clone + clippy::empty_structs_with_brackets, + clippy::unseparated_literal_suffix, + clippy::map_err_ignore, + clippy::redundant_clone, + clippy::todo + )] #![allow( clippy::module_name_repetitions, diff --git a/crate/cli/src/main.rs b/crate/cli/src/main.rs index 9d4459a..878f67f 100644 --- a/crate/cli/src/main.rs +++ b/crate/cli/src/main.rs @@ -3,13 +3,17 @@ use std::{path::PathBuf, process}; use clap::{CommandFactory, Parser, Subcommand}; use cosmian_findex_cli::{ actions::{ - login::LoginAction, logout::LogoutAction, markdown::MarkdownAction, - new_database::NewDatabaseAction, version::ServerVersionAction, + findex::{add_or_delete::AddOrDeleteAction, search::SearchAction}, + login::LoginAction, + logout::LogoutAction, + markdown::MarkdownAction, + permissions::PermissionsAction, + version::ServerVersionAction, }, error::result::CliResult, }; -use cosmian_findex_client::ClientConf; use cosmian_logger::log_utils::log_init; +use cosmian_rest_client::ClientConf; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -19,8 +23,8 @@ struct Cli { /// Configuration file location /// - /// This is an alternative to the env variable `KMS_CLI_CONF`. - /// Takes precedence over `KMS_CLI_CONF` env variable. + /// This is an alternative to the env variable `FINDEX_CLI_CONF`. + /// Takes precedence over `FINDEX_CLI_CONF` env variable. #[arg(short, long)] conf: Option, @@ -30,21 +34,28 @@ struct Cli { /// 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 + /// `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 { - NewDatabase(NewDatabaseAction), + /// 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` + /// Run `cargo run --bin findex -- markdown + /// documentation/docs/cli/main_commands.md` #[clap(hide = true)] Markdown(MarkdownAction), } @@ -76,12 +87,15 @@ async fn main_() -> CliResult<()> { command => { let conf = ClientConf::load(&conf_path)?; - let findex_rest_client = + let rest_client = conf.initialize_findex_client(opts.url.as_deref(), opts.accept_invalid_certs)?; match command { - CliCommands::NewDatabase(action) => action.process(&findex_rest_client).await?, - CliCommands::ServerVersion(action) => action.process(&findex_rest_client).await?, + 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"); } diff --git a/crate/cli/src/tests/auth_tests.rs b/crate/cli/src/tests/auth_tests.rs index 7d859c7..23f8301 100644 --- a/crate/cli/src/tests/auth_tests.rs +++ b/crate/cli/src/tests/auth_tests.rs @@ -1,32 +1,38 @@ #![allow(unused)] -use std::{path::PathBuf, process::Command}; +use std::{env, path::PathBuf, process::Command}; use assert_cmd::prelude::*; use base64::Engine; -use cosmian_findex_client::FINDEX_CLI_CONF_ENV; use cosmian_logger::log_utils::log_init; +use cosmian_rest_client::FINDEX_CLI_CONF_ENV; use tempfile::TempDir; use test_findex_server::{ - start_test_server_with_options, AuthenticationOptions, DBConfig, TestsContext, + start_test_server_with_options, AuthenticationOptions, DBConfig, DatabaseType, TestsContext, }; use tracing::{info, trace}; use crate::{error::result::CliResult, tests::PROG_NAME}; // let us not make other test cases fail -const PORT: u16 = 9999; +const PORT: u16 = 6667; #[tokio::test] #[allow(clippy::needless_return)] +#[ignore] pub(crate) async fn test_all_authentications() -> CliResult<()> { log_init(option_env!("RUST_LOG")); + let url = env::var("REDIS_HOST").map_or_else( + |_| "redis://localhost:6379".to_owned(), + |var_env| format!("redis://{var_env}:6379"), + ); + trace!("TESTS: using redis on {url}"); // plaintext no auth info!("Testing server with no auth"); let ctx = start_test_server_with_options( DBConfig { - database_type: Some("sqlite".to_owned()), - sqlite_path: PathBuf::from("./sqlite-data-auth-tests"), - clear_database: true, + database_type: Some(DatabaseType::Redis), + clear_database: false, + database_url: Some(url.clone()), ..DBConfig::default() }, PORT, @@ -40,9 +46,9 @@ pub(crate) async fn test_all_authentications() -> CliResult<()> { ctx.stop_server().await?; let default_db_config = DBConfig { - database_type: Some("sqlite".to_owned()), - sqlite_path: PathBuf::from("./sqlite-data-auth-tests"), + database_type: Some(DatabaseType::Redis), clear_database: false, + database_url: Some(url), ..DBConfig::default() }; @@ -74,37 +80,9 @@ pub(crate) async fn test_all_authentications() -> CliResult<()> { .await?; ctx.stop_server().await?; - // Bad API token auth but JWT auth used at first - info!("Testing server with bad API token auth but JWT auth used at first"); - let ctx = start_test_server_with_options( - default_db_config.clone(), - PORT, - AuthenticationOptions { - use_jwt_token: true, - use_https: true, - use_client_cert: false, - }, - ) - .await?; - ctx.stop_server().await?; - - // API token auth - info!("Testing server with API token auth"); - let ctx = start_test_server_with_options( - default_db_config.clone(), - PORT, - AuthenticationOptions { - use_jwt_token: false, - use_https: false, - use_client_cert: false, - }, - ) - .await?; - ctx.stop_server().await?; - - // On recent versions of macOS, the root Certificate for the client is searched on the keychains - // and not found, since it is a local self-signed certificate. - // This is likely a bug in reqwest + // On recent versions of macOS, the root Certificate for the client is searched + // on the keychains and not found, since it is a local self-signed + // certificate. This is likely a bug in reqwest #[cfg(not(target_os = "macos"))] { // tls client cert auth @@ -121,21 +99,7 @@ pub(crate) async fn test_all_authentications() -> CliResult<()> { .await?; ctx.stop_server().await?; - // Bad API token auth but cert auth used at first - info!("Testing server with bad API token auth but cert auth used at first"); - let ctx = start_test_server_with_options( - default_db_config.clone(), - PORT, - AuthenticationOptions { - use_jwt_token: false, - use_https: true, - use_client_cert: true, - }, - ) - .await?; - ctx.stop_server().await?; - - // Bad API token and good JWT token auth but still cert auth used at first + // Good JWT token auth but still cert auth used at first info!( "Testing server with bad API token and good JWT token auth but still cert auth used \ at first" diff --git a/crate/cli/src/tests/datasets/smallpop.csv b/crate/cli/src/tests/datasets/smallpop.csv new file mode 100644 index 0000000..b071c25 --- /dev/null +++ b/crate/cli/src/tests/datasets/smallpop.csv @@ -0,0 +1,11 @@ +city,region,country,population +Southborough,MA,United States,9686 +Northbridge,MA,United States,14061 +Westborough,MA,United States,29313 +Marlborough,MA,United States,38334 +Springfield,MA,United States,152227 +Springfield,MO,United States,150443 +Springfield,NJ,United States,14976 +Springfield,OH,United States,64325 +Springfield,OR,United States,56032 +Concord,NH,United States,42605 diff --git a/crate/cli/src/tests/findex/add_or_delete.rs b/crate/cli/src/tests/findex/add_or_delete.rs new file mode 100644 index 0000000..a7097bd --- /dev/null +++ b/crate/cli/src/tests/findex/add_or_delete.rs @@ -0,0 +1,42 @@ +use std::process::Command; + +use assert_cmd::prelude::*; +use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use tracing::debug; + +use crate::{ + actions::findex::add_or_delete::AddOrDeleteAction, + error::{result::CliResult, CliError}, + tests::{utils::recover_cmd_logs, PROG_NAME}, +}; + +#[allow(clippy::unwrap_used)] +pub(crate) fn add_or_delete_cmd( + cli_conf_path: &str, + command: &str, + action: AddOrDeleteAction, +) -> CliResult { + let mut cmd = Command::cargo_bin(PROG_NAME)?; + let args = vec![ + "--key".to_owned(), + action.findex_parameters.key.clone(), + "--label".to_owned(), + action.findex_parameters.label, + "--index-id".to_owned(), + action.findex_parameters.index_id.to_string(), + "--csv".to_owned(), + action.csv.to_str().unwrap().to_owned(), + ]; + cmd.env(FINDEX_CLI_CONF_ENV, cli_conf_path); + + cmd.arg(command).args(args); + debug!("cmd: {:?}", cmd); + let output = recover_cmd_logs(&mut cmd); + if output.status.success() { + let findex_output = std::str::from_utf8(&output.stdout)?; + return Ok(findex_output.to_owned()); + } + Err(CliError::Default( + std::str::from_utf8(&output.stderr)?.to_owned(), + )) +} diff --git a/crate/cli/src/tests/findex/mod.rs b/crate/cli/src/tests/findex/mod.rs new file mode 100644 index 0000000..acd19c5 --- /dev/null +++ b/crate/cli/src/tests/findex/mod.rs @@ -0,0 +1,187 @@ +use add_or_delete::add_or_delete_cmd; +use cosmian_logger::log_utils::log_init; +use cosmian_rest_client::Permission; +use search::search_cmd; +use test_findex_server::{ + start_default_test_findex_server, start_default_test_findex_server_with_cert_auth, +}; +use tracing::trace; +use uuid::Uuid; + +use crate::{ + actions::{ + findex::{add_or_delete::AddOrDeleteAction, search::SearchAction, FindexParameters}, + permissions::{GrantPermission, RevokePermission}, + }, + error::result::CliResult, + tests::permissions::{create_index_id_cmd, grant_permission_cmd, revoke_permission_cmd}, +}; + +pub(crate) mod add_or_delete; +pub(crate) mod search; + +fn add(cli_conf_path: &str, index_id: &Uuid) -> CliResult<()> { + add_or_delete_cmd( + cli_conf_path, + "add", + AddOrDeleteAction { + findex_parameters: FindexParameters { + key: "11223344556677889900AABBCCDDEEFF".to_owned(), + label: "My Findex label".to_owned(), + index_id: index_id.to_owned(), + }, + csv: "src/tests/datasets/smallpop.csv".into(), + }, + )?; + Ok(()) +} + +fn delete(cli_conf_path: &str, index_id: &Uuid) -> CliResult<()> { + add_or_delete_cmd( + cli_conf_path, + "delete", + AddOrDeleteAction { + findex_parameters: FindexParameters { + key: "11223344556677889900AABBCCDDEEFF".to_owned(), + label: "My Findex label".to_owned(), + index_id: index_id.to_owned(), + }, + csv: "src/tests/datasets/smallpop.csv".into(), + }, + )?; + Ok(()) +} + +fn search(cli_conf_path: &str, index_id: &Uuid) -> CliResult { + search_cmd( + cli_conf_path, + SearchAction { + findex_parameters: FindexParameters { + key: "11223344556677889900AABBCCDDEEFF".to_owned(), + label: "My Findex label".to_owned(), + index_id: index_id.to_owned(), + }, + keyword: vec!["Southborough".to_owned(), "Northbridge".to_owned()], + }, + ) +} + +#[allow(clippy::panic_in_result_fn)] +fn add_search_delete(cli_conf_path: &str, index_id: &Uuid) -> CliResult<()> { + add(cli_conf_path, index_id)?; + + // make sure searching returns the expected results + let search_results = search(cli_conf_path, index_id)?; + assert!(search_results.contains("States9686")); // for Southborough + assert!(search_results.contains("States14061")); // for Northbridge + + delete(cli_conf_path, index_id)?; + + // make sure no results are returned after deletion + let search_results = search(cli_conf_path, index_id)?; + assert!(!search_results.contains("States9686")); // for Southborough + assert!(!search_results.contains("States14061")); // for Northbridge + + Ok(()) +} + +#[tokio::test] +pub(crate) async fn test_findex_no_auth() -> CliResult<()> { + log_init(None); + let ctx = start_default_test_findex_server().await; + add_search_delete(&ctx.owner_client_conf_path, &Uuid::new_v4())?; + Ok(()) +} + +#[tokio::test] +pub(crate) async fn test_findex_cert_auth() -> CliResult<()> { + log_init(None); + let ctx = start_default_test_findex_server_with_cert_auth().await; + + let index_id = create_index_id_cmd(&ctx.owner_client_conf_path)?; + trace!("index_id: {index_id}"); + + add_search_delete(&ctx.owner_client_conf_path, &index_id)?; + Ok(()) +} + +#[allow(clippy::panic_in_result_fn, clippy::unwrap_used)] +#[tokio::test] +pub(crate) async fn test_findex_grant_and_revoke_permission() -> CliResult<()> { + log_init(None); + let ctx = start_default_test_findex_server_with_cert_auth().await; + + let index_id = create_index_id_cmd(&ctx.owner_client_conf_path)?; + trace!("index_id: {index_id}"); + + add(&ctx.owner_client_conf_path, &index_id)?; + + // Grant read permission to the client + grant_permission_cmd( + &ctx.owner_client_conf_path, + &GrantPermission { + user: "user.client@acme.com".to_owned(), + index_id, + permission: Permission::Read, + }, + )?; + + // User can read... + let search_results = search(&ctx.user_client_conf_path, &index_id)?; + assert!(search_results.contains("States9686")); // for Southborough + assert!(search_results.contains("States14061")); // for Northbridge + + // ... but not write + assert!(add(&ctx.user_client_conf_path, &index_id).is_err()); + + // Grant write permission + grant_permission_cmd( + &ctx.owner_client_conf_path, + &GrantPermission { + user: "user.client@acme.com".to_owned(), + index_id, + permission: Permission::Write, + }, + )?; + + // User can read... + let search_results = search(&ctx.user_client_conf_path, &index_id)?; + assert!(search_results.contains("States9686")); // for Southborough + assert!(search_results.contains("States14061")); // for Northbridge + + // ... and write + add(&ctx.user_client_conf_path, &index_id)?; + + // Try to escalade privileges from `read` to `admin` + grant_permission_cmd( + &ctx.user_client_conf_path, + &GrantPermission { + user: "user.client@acme.com".to_owned(), + index_id, + permission: Permission::Admin, + }, + ) + .unwrap_err(); + + revoke_permission_cmd( + &ctx.owner_client_conf_path, + &RevokePermission { + user: "user.client@acme.com".to_owned(), + index_id, + }, + )?; + + search(&ctx.user_client_conf_path, &index_id).unwrap_err(); + + Ok(()) +} + +#[allow(clippy::panic_in_result_fn)] +#[tokio::test] +pub(crate) async fn test_findex_no_permission() -> CliResult<()> { + log_init(None); + let ctx = start_default_test_findex_server_with_cert_auth().await; + + assert!(add_search_delete(&ctx.user_client_conf_path, &Uuid::new_v4()).is_err()); + Ok(()) +} diff --git a/crate/cli/src/tests/findex/search.rs b/crate/cli/src/tests/findex/search.rs new file mode 100644 index 0000000..a093819 --- /dev/null +++ b/crate/cli/src/tests/findex/search.rs @@ -0,0 +1,40 @@ +use std::process::Command; + +use assert_cmd::prelude::*; +use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use tracing::debug; + +use crate::{ + actions::findex::search::SearchAction, + error::{result::CliResult, CliError}, + tests::{utils::recover_cmd_logs, PROG_NAME}, +}; + +pub(crate) fn search_cmd(cli_conf_path: &str, action: SearchAction) -> CliResult { + let mut args = vec![ + "--key".to_owned(), + action.findex_parameters.key.clone(), + "--label".to_owned(), + action.findex_parameters.label, + "--index-id".to_owned(), + action.findex_parameters.index_id.to_string(), + ]; + + for word in action.keyword { + args.push("--keyword".to_owned()); + args.push(word); + } + let mut cmd = Command::cargo_bin(PROG_NAME)?; + cmd.env(FINDEX_CLI_CONF_ENV, cli_conf_path); + + cmd.arg("search").args(args); + debug!("cmd: {:?}", cmd); + let output = recover_cmd_logs(&mut cmd); + if output.status.success() { + let findex_output = std::str::from_utf8(&output.stdout)?; + return Ok(findex_output.to_owned()); + } + Err(CliError::Default( + std::str::from_utf8(&output.stderr)?.to_owned(), + )) +} diff --git a/crate/cli/src/tests/mod.rs b/crate/cli/src/tests/mod.rs index 4a18d73..cc31530 100644 --- a/crate/cli/src/tests/mod.rs +++ b/crate/cli/src/tests/mod.rs @@ -1,5 +1,6 @@ mod auth_tests; -mod new_database; +mod findex; +mod permissions; mod utils; -const PROG_NAME: &str = "findex"; +const PROG_NAME: &str = "cosmian_findex_cli"; diff --git a/crate/cli/src/tests/new_database.rs b/crate/cli/src/tests/new_database.rs deleted file mode 100644 index 9808c2f..0000000 --- a/crate/cli/src/tests/new_database.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::process::Command; - -use assert_cmd::prelude::*; -use cosmian_findex_client::FINDEX_CLI_CONF_ENV; -use cosmian_logger::log_utils::log_init; -use predicates::prelude::*; -use test_findex_server::start_default_test_findex_server; -use tracing::info; - -use crate::{ - error::result::CliResult, - tests::{utils::recover_cmd_logs, PROG_NAME}, -}; - -#[tokio::test] -#[allow(clippy::needless_return)] -pub(crate) async fn test_new_database() -> CliResult<()> { - log_init(option_env!("RUST_LOG")); - let ctx = start_default_test_findex_server().await; - - if ctx.owner_client_conf.findex_database_secret.is_none() { - info!("Skipping test_new_database as backend not sqlite-enc"); - return Ok(()); - } - - let mut cmd = Command::cargo_bin(PROG_NAME)?; - cmd.env(FINDEX_CLI_CONF_ENV, &ctx.owner_client_conf_path); - - cmd.arg("new-database"); - recover_cmd_logs(&mut cmd); - cmd.assert().success().stdout(predicate::str::contains( - "A new user encrypted database is configured", - )); - - Ok(()) -} - -#[tokio::test] -#[allow(clippy::needless_return)] -pub(crate) async fn test_conf_does_not_exist() -> CliResult<()> { - log_init(option_env!("RUST_LOG")); - let ctx = start_default_test_findex_server().await; - - if ctx.owner_client_conf.findex_database_secret.is_none() { - info!("Skipping test_conf_does_not_exist as backend not sqlite-enc"); - return Ok(()); - } - - let mut cmd = Command::cargo_bin(PROG_NAME)?; - cmd.env( - FINDEX_CLI_CONF_ENV, - "test_data/configs/kms_bad_group_id.bad", - ); - - cmd.arg("ec").args(vec!["keys", "create"]); - let output = recover_cmd_logs(&mut cmd); - assert!(!output.status.success()); - Ok(()) -} diff --git a/crate/cli/src/tests/permissions.rs b/crate/cli/src/tests/permissions.rs new file mode 100644 index 0000000..55e5a17 --- /dev/null +++ b/crate/cli/src/tests/permissions.rs @@ -0,0 +1,104 @@ +use std::process::Command; + +use assert_cmd::prelude::*; +use cosmian_rest_client::FINDEX_CLI_CONF_ENV; +use regex::{Regex, RegexBuilder}; +use tracing::{debug, trace}; +use uuid::Uuid; + +use crate::{ + actions::permissions::{GrantPermission, RevokePermission}, + error::{result::CliResult, CliError}, + tests::{utils::recover_cmd_logs, PROG_NAME}, +}; + +/// Extract the `key_uid` (prefixed by a pattern) from a text +#[allow(clippy::unwrap_used)] +pub(crate) fn extract_uid<'a>(text: &'a str, pattern: &'a str) -> Option<&'a str> { + let formatted = format!(r"\[\S+\] {pattern}: (?P[0-9a-fA-F-]+)"); + let uid_regex: Regex = RegexBuilder::new(formatted.as_str()) + .multi_line(true) + .build() + .unwrap(); + uid_regex + .captures(text) + .and_then(|cap| cap.name("uid").map(|uid| uid.as_str())) +} + +pub(crate) fn create_index_id_cmd(cli_conf_path: &str) -> CliResult { + let mut cmd = Command::cargo_bin(PROG_NAME)?; + let args = vec!["create".to_owned()]; + cmd.env(FINDEX_CLI_CONF_ENV, cli_conf_path); + + cmd.arg("permissions").args(args); + debug!("cmd: {:?}", cmd); + let output = recover_cmd_logs(&mut cmd); + if output.status.success() { + let findex_output = std::str::from_utf8(&output.stdout)?; + trace!("findex_output: {}", findex_output); + let unique_identifier = extract_uid( + findex_output, + "New admin permission successfully created on index", + ) + .ok_or_else(|| CliError::Default("failed extracting the unique identifier".to_owned()))?; + let uuid = Uuid::parse_str(unique_identifier)?; + return Ok(uuid); + } + Err(CliError::Default( + std::str::from_utf8(&output.stderr)?.to_owned(), + )) +} + +pub(crate) fn grant_permission_cmd( + cli_conf_path: &str, + action: &GrantPermission, +) -> CliResult { + let mut cmd = Command::cargo_bin(PROG_NAME)?; + let args = vec![ + "grant".to_owned(), + "--user".to_owned(), + action.user.clone(), + "--index-id".to_owned(), + action.index_id.to_string(), + "--permission".to_owned(), + action.permission.to_string(), + ]; + cmd.env(FINDEX_CLI_CONF_ENV, cli_conf_path); + + cmd.arg("permissions").args(args); + debug!("cmd: {:?}", cmd); + let output = recover_cmd_logs(&mut cmd); + if output.status.success() { + let findex_output = std::str::from_utf8(&output.stdout)?; + return Ok(findex_output.to_owned()); + } + Err(CliError::Default( + std::str::from_utf8(&output.stderr)?.to_owned(), + )) +} + +pub(crate) fn revoke_permission_cmd( + cli_conf_path: &str, + action: &RevokePermission, +) -> CliResult { + let mut cmd = Command::cargo_bin(PROG_NAME)?; + let args = vec![ + "revoke".to_owned(), + "--user".to_owned(), + action.user.clone(), + "--index-id".to_owned(), + action.index_id.to_string(), + ]; + cmd.env(FINDEX_CLI_CONF_ENV, cli_conf_path); + + cmd.arg("permissions").args(args); + debug!("cmd: {:?}", cmd); + let output = recover_cmd_logs(&mut cmd); + if output.status.success() { + let findex_output = std::str::from_utf8(&output.stdout)?; + return Ok(findex_output.to_owned()); + } + Err(CliError::Default( + std::str::from_utf8(&output.stderr)?.to_owned(), + )) +} diff --git a/crate/cli/src/tests/utils/cmd_logs.rs b/crate/cli/src/tests/utils/cmd_logs.rs index 6dff72e..cf4ea70 100644 --- a/crate/cli/src/tests/utils/cmd_logs.rs +++ b/crate/cli/src/tests/utils/cmd_logs.rs @@ -4,6 +4,7 @@ use std::{ }; /// Recover output logs from a command call `cmd` and re-inject it into stdio +#[allow(clippy::unwrap_used, dead_code)] pub(crate) fn recover_cmd_logs(cmd: &mut Command) -> Output { let output = cmd .stdout(Stdio::piped()) diff --git a/crate/cli/src/tests/utils/mod.rs b/crate/cli/src/tests/utils/mod.rs index 26349d4..7bd5ac4 100644 --- a/crate/cli/src/tests/utils/mod.rs +++ b/crate/cli/src/tests/utils/mod.rs @@ -1,3 +1,4 @@ +#![allow(unused_imports)] pub(crate) use cmd_logs::recover_cmd_logs; mod cmd_logs; diff --git a/crate/client/Cargo.toml b/crate/client/Cargo.toml index b1ef914..9e66772 100644 --- a/crate/client/Cargo.toml +++ b/crate/client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "cosmian_findex_client" +name = "cosmian_rest_client" version.workspace = true authors.workspace = true edition.workspace = true @@ -16,16 +16,17 @@ doctest = false [dependencies] base64 = { workspace = true } -cloudproof = { workspace = true } +clap = { workspace = true } der = { workspace = true } -log = { workspace = true } pem = { workspace = true } reqwest = { workspace = true, features = ["default", "json", "native-tls"] } -rustls = { workspace = true, features = ["dangerous_configuration"] } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } -webpki-roots = "0.22" -x509-cert = { workspace = true } +uuid = { workspace = true, features = ["v4"] } + +[dev-dependencies] +faker_rand = "0.1" +rand = "0.8" diff --git a/crate/client/README.md b/crate/client/README.md index d4d992c..e1628a6 100644 --- a/crate/client/README.md +++ b/crate/client/README.md @@ -1,3 +1,3 @@ # Cosmian Findex Client -The `cosmian_findex_client` forges and sends post query to the `cosmian_findex_server`. +The `cosmian_rest_client` forges and sends post query to the `cosmian_findex_server`. diff --git a/crate/client/src/certificate_verifier.rs b/crate/client/src/certificate_verifier.rs deleted file mode 100644 index ad01e9e..0000000 --- a/crate/client/src/certificate_verifier.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::{sync::Arc, time::SystemTime}; - -use rustls::{ - client::{ServerCertVerified, ServerCertVerifier}, - Certificate, Error as RustTLSError, ServerName, -}; - -/// A TLS verifier adding the ability to match the leaf certificate with a trusted one. -pub(crate) struct LeafCertificateVerifier { - // The certificate we expect to see in the TLS connection - expected_cert: Certificate, - // A default verifier to run anyway - default_verifier: Arc, -} - -impl LeafCertificateVerifier { - pub(crate) fn new( - expected_cert: Certificate, - default_verifier: Arc, - ) -> Self { - Self { - expected_cert, - default_verifier, - } - } -} - -impl ServerCertVerifier for LeafCertificateVerifier { - fn verify_server_cert( - &self, - end_entity: &Certificate, // end_entity - intermediates: &[Certificate], // intermediates - server_name: &ServerName, // server_name - scts: &mut dyn Iterator, // scts - ocsp_response: &[u8], // ocsp_response - now: SystemTime, // now - ) -> Result { - // Verify the leaf certificate - if !end_entity.eq(&self.expected_cert) { - return Err(RustTLSError::General( - "Leaf certificate doesn't match the expected one".to_owned(), - )); - } - - // Now proceed with typical verifications - self.default_verifier.verify_server_cert( - end_entity, - intermediates, - server_name, - scts, - ocsp_response, - now, - ) - } -} - -/// Remove all verifications -pub(crate) struct NoVerifier; - -impl ServerCertVerifier for NoVerifier { - fn verify_server_cert( - &self, - _: &Certificate, // end_entity - _: &[Certificate], // intermediates - _: &ServerName, // server_name - _: &mut dyn Iterator, // scts - _: &[u8], // ocsp_response - _: SystemTime, // now - ) -> Result { - Ok(ServerCertVerified::assertion()) - } -} diff --git a/crate/client/src/config.rs b/crate/client/src/config.rs index dd14315..424cc99 100644 --- a/crate/client/src/config.rs +++ b/crate/client/src/config.rs @@ -5,27 +5,27 @@ use std::{ path::PathBuf, }; -use der::{DecodePem, Encode}; -#[cfg(target_os = "linux")] -use log::info; -use rustls::Certificate; use serde::{Deserialize, Serialize}; -use x509_cert::Certificate as X509Certificate; +use tracing::info; #[cfg(target_os = "linux")] use crate::client_bail; use crate::{ - error::{result::RestClientResultHelper, ClientError}, - FindexClient, + error::{ + result::{ClientResult, RestClientResultHelper}, + ClientError, + }, + RestClient, }; /// Returns the path to the current user's home folder. /// -/// On Linux and macOS, the home folder is typically located at `/home/` -/// or `/Users/`, respectively. On Windows, the home folder is typically -/// located at `C:\Users\`. However, the location of the home folder can -/// be changed by the user or by system administrators, so it's important to check -/// for the existence of the appropriate environment variables. +/// On Linux and macOS, the home folder is typically located at +/// `/home/` or `/Users/`, respectively. On Windows, the +/// home folder is typically located at `C:\Users\`. However, the +/// location of the home folder can be changed by the user or by system +/// administrators, so it's important to check for the existence of the +/// appropriate environment variables. /// /// Returns `None` if the home folder cannot be determined. fn get_home_folder() -> Option { @@ -42,7 +42,8 @@ fn get_home_folder() -> Option { { return Some(PathBuf::from(hdrive).join(hpath)); } - // If none of the above environment variables exist, the home folder cannot be determined + // If none of the above environment variables exist, the home folder cannot be + // determined None } @@ -54,7 +55,8 @@ fn get_default_conf_path() -> Result { .map(|home| home.join(".cosmian/findex_client.json")) } -/// used for serialization +/// Required for `serde` serialization +#[allow(clippy::trivially_copy_pass_by_ref)] const fn not(b: &bool) -> bool { !*b } @@ -80,23 +82,6 @@ pub struct Oauth2Conf { pub scopes: Vec, } -/// The configuration that is used by the google command -/// to perform actions over Gmail API. -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] -pub struct GmailApiConf { - pub account_type: String, - pub project_id: String, - pub private_key_id: String, - pub private_key: String, - pub client_email: String, - pub client_id: String, - pub auth_uri: String, - pub token_uri: String, - pub auth_provider_x509_cert_url: String, - pub client_x509_cert_url: String, - pub universe_domain: String, -} - #[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 @@ -114,8 +99,6 @@ pub struct ClientConf { #[serde(skip_serializing_if = "Option::is_none")] pub ssl_client_pkcs12_password: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub findex_database_secret: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub oauth2_conf: Option, } @@ -123,10 +106,9 @@ impl Default for ClientConf { fn default() -> Self { Self { accept_invalid_certs: false, - findex_server_url: "http://0.0.0.0:9998".to_owned(), + findex_server_url: "http://0.0.0.0:6666".to_owned(), verified_cert: None, findex_access_token: None, - findex_database_secret: None, ssl_client_pkcs12_path: None, ssl_client_pkcs12_password: None, oauth2_conf: None, @@ -134,31 +116,36 @@ impl Default for ClientConf { } } -/// This method is used to configure the Findex CLI by reading a JSON configuration file. +/// 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 /// { /// "accept_invalid_certs": false, -/// "findex_server_url": "http://127.0.0.1:9998", +/// "findex_server_url": "http://127.0.0.1:6666", /// "findex_access_token": "AA...AAA", -/// "findex_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 `FINDEX_CLI_CONF` environment variable. -/// If the environment variable is not set, a default path is used. -/// If the configuration file does not exist at the path, a new file is created with default values. +/// The path to the configuration file is specified through the +/// `FINDEX_CLI_CONF` environment variable. If the environment variable is not +/// 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 specified in the configuration file. +/// 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 { - pub fn location(conf: Option) -> Result { + /// Returns the path to the configuration file. + /// # Errors + /// Returns an error if the configuration file does not exist. + pub fn location(conf: Option) -> ClientResult { // Obtain the configuration file path from: // - the `--conf` arg // - the environment variable corresponding to `FINDEX_CLI_CONF_ENV` @@ -174,8 +161,8 @@ impl ClientConf { // Error if the specified file does not exist if !conf_path.exists() { return Err(ClientError::NotSupported(format!( - "Configuration file {conf_path:?} specified in {FINDEX_CLI_CONF_ENV} environment \ - variable does not exist" + "Configuration file {conf_path:?} specified in {FINDEX_CLI_CONF_ENV} \ + environment variable does not exist" ))); } return Ok(conf_path); @@ -193,17 +180,19 @@ impl ClientConf { let default_system_path = PathBuf::from(FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH); if default_system_path.exists() { info!( - "No active user, using configuration at {FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH}" + "No active user, using configuration at \ + {FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH}" ); return Ok(default_system_path); } client_bail!( - "no configuration found at {FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH}, and no current \ - user, bailing out" + "no configuration found at {FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH}, and no \ + current user, bailing out" ); } Ok(user_conf) => { - // the user home exists, if there is no conf file, check /etc/cosmian/findex.json + // the user home exists, if there is no conf file, check + // /etc/cosmian/findex.json if !user_conf.exists() { let default_system_path = PathBuf::from(FINDEX_CLI_CONF_DEFAULT_SYSTEM_PATH); if default_system_path.exists() { @@ -223,7 +212,11 @@ impl ClientConf { } } - pub fn save(&self, conf_path: &PathBuf) -> Result<(), ClientError> { + /// 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<()> { fs::write( conf_path, serde_json::to_string_pretty(&self) @@ -236,8 +229,14 @@ impl ClientConf { Ok(()) } - pub fn load(conf_path: &PathBuf) -> Result { - // Deserialize the configuration from the file, or create a default configuration if none exists + /// 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 { + // Deserialize the configuration from the file, or create a default + // configuration if none exists let conf = if conf_path.exists() { // Configuration file exists, read and deserialize it let file = File::open(conf_path) @@ -245,7 +244,8 @@ impl ClientConf { serde_json::from_reader(BufReader::new(file)) .with_context(|| format!("Error while parsing configuration file {conf_path:?}"))? } else { - // Configuration file doesn't exist, create it with default values and serialize it + // Configuration file doesn't exist, create it with default values and serialize + // it let parent = conf_path .parent() .with_context(|| format!("Unable to get parent directory of {conf_path:?}"))?; @@ -263,40 +263,40 @@ impl ClientConf { /// 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. + /// 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 { + ) -> 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 findex_rest_client = FindexClient::instantiate( + 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(), - self.findex_database_secret.as_deref(), accept_invalid_certs, - if let Some(certificate) = &self.verified_cert { - Some(Certificate( - X509Certificate::from_pem(certificate.as_bytes())?.to_der()?, - )) - } else { - None - }, ) .with_context(|| { format!("Unable to instantiate a Findex REST client to server at {findex_server_url}") })?; - Ok(findex_rest_client) + Ok(rest_client) } } +#[allow(unsafe_code, clippy::unwrap_used)] #[cfg(test)] mod tests { use std::{env, fs, path::PathBuf}; @@ -310,22 +310,17 @@ mod tests { env::set_var(FINDEX_CLI_CONF_ENV, "test_data/configs/findex.json"); } let conf_path = ClientConf::location(None).unwrap(); - assert!(ClientConf::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 = ClientConf::location(None).unwrap(); - assert!(ClientConf::load(&conf_path).is_ok()); + ClientConf::load(&conf_path).unwrap(); // Default conf file unsafe { env::remove_var(FINDEX_CLI_CONF_ENV); } - let _ = fs::remove_file(get_default_conf_path().unwrap()); + if get_default_conf_path().unwrap().exists() { + fs::remove_file(get_default_conf_path().unwrap()).unwrap(); + } let conf_path = ClientConf::location(None).unwrap(); - assert!(ClientConf::load(&conf_path).is_ok()); + ClientConf::load(&conf_path).unwrap(); assert!(get_default_conf_path().unwrap().exists()); // invalid conf @@ -342,6 +337,6 @@ mod tests { } let conf_path = ClientConf::location(Some(PathBuf::from("test_data/configs/findex.json"))).unwrap(); - assert!(ClientConf::load(&conf_path).is_ok()); + ClientConf::load(&conf_path).unwrap(); } } diff --git a/crate/client/src/error/mod.rs b/crate/client/src/error/mod.rs index 652b74b..4b53777 100644 --- a/crate/client/src/error/mod.rs +++ b/crate/client/src/error/mod.rs @@ -64,12 +64,6 @@ impl From for ClientError { } } -impl From for ClientError { - fn from(e: cloudproof::reexport::crypto_core::CryptoCoreError) -> Self { - Self::UnexpectedError(e.to_string()) - } -} - /// Construct a server error from a string. #[macro_export] macro_rules! client_error { diff --git a/crate/client/src/error/result.rs b/crate/client/src/error/result.rs index ed31370..ea3a844 100644 --- a/crate/client/src/error/result.rs +++ b/crate/client/src/error/result.rs @@ -32,7 +32,7 @@ where impl RestClientResultHelper for Option { fn context(self, context: &str) -> ClientResult { - self.ok_or_else(|| ClientError::Default(context.to_string())) + self.ok_or_else(|| ClientError::Default(context.to_owned())) } fn with_context(self, op: O) -> ClientResult diff --git a/crate/client/src/file_utils.rs b/crate/client/src/file_utils.rs index a69bf84..1545af9 100644 --- a/crate/client/src/file_utils.rs +++ b/crate/client/src/file_utils.rs @@ -1,16 +1,20 @@ use std::{ fs::{self, File}, io::Read, - path::{Path, PathBuf}, + path::Path, }; -use cloudproof::reexport::crypto_core::bytes_ser_de::{Deserializer, Serializer}; use serde::{de::DeserializeOwned, Serialize}; -use crate::{error::ClientError, ClientResultHelper}; +use crate::{ + error::{result::ClientResult, ClientError}, + ClientResultHelper, +}; /// Read all bytes from a file -pub fn read_bytes_from_file(file: &impl AsRef) -> Result, ClientError> { +/// # 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()))? @@ -21,6 +25,8 @@ pub fn read_bytes_from_file(file: &impl AsRef) -> Result, ClientEr } /// 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, @@ -31,6 +37,8 @@ where } /// 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!( @@ -42,6 +50,8 @@ pub fn write_bytes_to_file(bytes: &[u8], file: &impl AsRef) -> Result<(), } /// 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, @@ -53,175 +63,3 @@ where .with_context(|| "failed parsing the object from the json file")?; write_bytes_to_file(&bytes, file) } - -/// Write the decrypted data to a file -/// -/// If no `output_file` is provided, then -/// it reuses the `input_file` name with the extension `plain`. -pub fn write_single_decrypted_data( - plaintext: &[u8], - input_file: &Path, - output_file: Option<&PathBuf>, -) -> Result<(), ClientError> { - let output_file = output_file.map_or_else( - || input_file.with_extension("plain"), - std::clone::Clone::clone, - ); - - write_bytes_to_file(plaintext, &output_file) - .with_context(|| "failed to write the decrypted file")?; - - tracing::info!("The decrypted file is available at {output_file:?}"); - Ok(()) -} - -/// Write the encrypted data to a file -/// -/// If no `output_file` is provided, then -/// it reuses the `input_file` name with the extension `enc`. -pub fn write_single_encrypted_data( - encrypted_data: &[u8], - input_file: &Path, - output_file: Option<&PathBuf>, -) -> Result<(), ClientError> { - // Write the encrypted file - let output_file = output_file.map_or_else( - || input_file.with_extension("enc"), - std::clone::Clone::clone, - ); - - write_bytes_to_file(encrypted_data, &output_file) - .with_context(|| "failed to write the encrypted file")?; - - tracing::info!("The encrypted file is available at {output_file:?}"); - Ok(()) -} - -/// Read all bytes from multiple files and serialize them -/// into a unique vector using LEB128 serialization (bulk mode) -pub fn read_bytes_from_files_to_bulk(input_files: &[PathBuf]) -> Result, ClientError> { - let mut ser = Serializer::new(); - - // number of files to decrypt - let nb_input_files = u64::try_from(input_files.len()).map_err(|_| { - ClientError::Conversion(format!( - "number of input files is too big for architecture: {} bytes", - input_files.len() - )) - })?; - ser.write_leb128_u64(nb_input_files)?; - - input_files.iter().try_for_each(|input_file| { - let content = read_bytes_from_file(input_file)?; - ser.write_vec(&content)?; - Ok::<_, ClientError>(()) - })?; - - Ok(ser.finalize().to_vec()) -} - -/// Write bulk decrypted data -/// -/// Bulk data is compound of multiple chunks of data. -/// Sizes are written using LEB-128 serialization. -/// -/// Each chunk of plaintext data is written to its own file. -pub fn write_bulk_decrypted_data( - plaintext: &[u8], - input_files: &[PathBuf], - output_file: Option<&PathBuf>, -) -> Result<(), ClientError> { - let mut de = Deserializer::new(plaintext); - - // number of decrypted chunks - let nb_chunks = { - let len = de.read_leb128_u64()?; - usize::try_from(len).map_err(|_| { - ClientError::Conversion(format!( - "size of vector is too big for architecture: {len} bytes", - )) - })? - }; - - (0..nb_chunks).try_for_each(|idx| { - // get chunk of data from slice - let chunk_data = de.read_vec_as_ref()?; - - // Write plaintext data to its own file - // Reuse input file names if there are multiple inputs (and ignore `output_file`) - let input_file = &input_files[idx]; - let output_file = match output_file { - Some(output_file) if nb_chunks > 1 => { - let file_name = input_file.file_name().ok_or_else(|| { - ClientError::Conversion(format!( - "cannot get file name from input file {input_file:?}", - )) - })?; - output_file.join(PathBuf::from(file_name).with_extension("plain")) - } - _ => output_file.map_or_else( - || input_file.with_extension("plain"), - std::clone::Clone::clone, - ), - }; - - write_bytes_to_file(chunk_data, &output_file)?; - - tracing::info!("The decrypted file is available at {output_file:?}"); - Ok(()) - }) -} - -/// Write bulk encrypted data -/// -/// Bulk data is compound of multiple chunks of data. -/// Sizes are written using LEB-128 serialization. -/// -/// Each chunk of data: -/// - is compound of encrypted header + encrypted data -/// - is written to its own file. -pub fn write_bulk_encrypted_data( - plaintext: &[u8], - input_files: &[PathBuf], - output_file: Option<&PathBuf>, -) -> Result<(), ClientError> { - let mut de = Deserializer::new(plaintext); - - // number of encrypted chunks - let nb_chunks = { - let len = de.read_leb128_u64()?; - usize::try_from(len).map_err(|_| { - ClientError::Conversion(format!( - "size of vector is too big for architecture: {len} bytes", - )) - })? - }; - - (0..nb_chunks).try_for_each(|idx| { - // get chunk of data from slice - let chunk_data = de.read_vec_as_ref()?; - - // Write encrypted data to its own file - // Reuse input file names if there are multiple inputs (and ignore `output_file`) - let input_file = &input_files[idx]; - let output_file = match output_file { - Some(output_file) if nb_chunks > 1 => { - let file_name = input_file.file_name().ok_or_else(|| { - ClientError::Conversion(format!( - "cannot get file name from input file {input_file:?}", - )) - })?; - output_file.join(PathBuf::from(file_name).with_extension("enc")) - } - _ => output_file.map_or_else( - || input_file.with_extension("enc"), - std::clone::Clone::clone, - ), - }; - - write_bytes_to_file(chunk_data, &output_file)?; - - tracing::info!("The encrypted file is available at {output_file:?}"); - Ok(()) - }) -} diff --git a/crate/client/src/lib.rs b/crate/client/src/lib.rs index c3bb863..56ebea0 100644 --- a/crate/client/src/lib.rs +++ b/crate/client/src/lib.rs @@ -1,20 +1,66 @@ -#![allow(clippy::upper_case_acronyms)] -//required to detect generic type in Serializer -#![feature(min_specialization)] +#![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 config::{ClientConf, GmailApiConf, FINDEX_CLI_CONF_ENV}; + // 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_bytes_from_files_to_bulk, read_from_json_file, - write_bulk_decrypted_data, write_bulk_encrypted_data, write_bytes_to_file, - write_json_object_to_file, write_single_decrypted_data, write_single_encrypted_data, + read_bytes_from_file, read_from_json_file, write_bytes_to_file, write_json_object_to_file, }; -pub use findex_rest_client::FindexClient; +pub use permission::Permission; +pub use rest_client::RestClient; pub use result::{ClientResultHelper, RestClientResult}; -mod certificate_verifier; mod config; mod error; mod file_utils; -mod findex_rest_client; +mod permission; +mod rest_client; mod result; diff --git a/crate/client/src/permission.rs b/crate/client/src/permission.rs new file mode 100644 index 0000000..e1b3f73 --- /dev/null +++ b/crate/client/src/permission.rs @@ -0,0 +1,21 @@ +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/findex_rest_client.rs b/crate/client/src/rest_client.rs similarity index 52% rename from crate/client/src/findex_rest_client.rs rename to crate/client/src/rest_client.rs index da88398..92baf85 100644 --- a/crate/client/src/findex_rest_client.rs +++ b/crate/client/src/rest_client.rs @@ -1,45 +1,43 @@ use std::{ + fmt::Display, fs::File, io::{BufReader, Read}, - sync::Arc, time::Duration, }; -use log::trace; use reqwest::{ header::{HeaderMap, HeaderValue}, Client, ClientBuilder, Identity, Response, StatusCode, }; -use rustls::{client::WebPkiVerifier, Certificate}; +use serde::{Deserialize, Serialize}; +use tracing::{instrument, trace}; +use uuid::Uuid; use crate::{ - certificate_verifier::{LeafCertificateVerifier, NoVerifier}, error::{result::ClientResult, ClientError}, - ClientResultHelper, + ClientResultHelper, Permission, }; #[derive(Clone)] -pub struct FindexClient { +pub struct RestClient { pub server_url: String, - client: Client, + pub client: Client, } -impl FindexClient { +impl RestClient { /// Instantiate a new Findex REST Client - #[allow(clippy::too_many_arguments)] - #[allow(dead_code)] + /// # 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>, - database_secret: Option<&str>, accept_invalid_certs: bool, - allowed_tee_tls_cert: Option, ) -> Result { let server_url = server_url .strip_suffix('/') - .map_or_else(|| server_url.to_string(), std::string::ToString::to_string); + .map_or_else(|| server_url.to_owned(), std::string::ToString::to_string); let mut headers = HeaderMap::new(); if let Some(bearer_token) = bearer_token { @@ -48,27 +46,12 @@ impl FindexClient { HeaderValue::from_str(format!("Bearer {bearer_token}").as_str())?, ); } - if let Some(database_secret) = database_secret { - headers.insert( - "FindexDatabaseSecret", - HeaderValue::from_str(database_secret)?, - ); - } // We deal with 4 scenarios: // 1. HTTP: no TLS - // 2. HTTPS: - // a) self-signed: we want to remove the verifications - // b) signed in a tee context: we want to verify the /quote and then only accept the allowed certificate - // -> For efficiency purpose, this verification is made outside this call (async with the queries) - // Only the verified certificate is used here - // c) signed in a non-tee context: we want classic TLS verification based on the root ca - let builder = if let Some(certificate) = allowed_tee_tls_cert { - build_tls_client_tee(certificate, accept_invalid_certs) - } else { - ClientBuilder::new().danger_accept_invalid_certs(accept_invalid_certs) - }; - + // 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) => { @@ -95,10 +78,13 @@ impl FindexClient { }) } - /// This operation requests the server to create a new database. + /// This operation requests the server to create a new table. /// The returned secrets could be shared between several users. - pub async fn new_database(&self) -> Result { - let endpoint = "/new_database"; + /// # 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(); @@ -111,23 +97,67 @@ impl FindexClient { Err(ClientError::RequestFailed(p)) } - pub async fn version(&self) -> ClientResult { - let endpoint = "/version"; + #[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); - let response = self.client.get(server_url).send().await?; + 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?); + return Ok(response.json::().await?); } // process error - let p = handle_error(endpoint, response).await?; + 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. +/// 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(); @@ -148,38 +178,13 @@ async fn handle_error(endpoint: &str, response: Response) -> Result ClientBuilder { - let mut root_cert_store = rustls::RootCertStore::empty(); - - let trust_anchors = webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|trust_anchor| { - rustls::OwnedTrustAnchor::from_subject_spki_name_constraints( - trust_anchor.subject, - trust_anchor.spki, - trust_anchor.name_constraints, - ) - }); - root_cert_store.add_trust_anchors(trust_anchors); - - let verifier = if accept_invalid_certs { - LeafCertificateVerifier::new(leaf_cert, Arc::new(NoVerifier)) - } else { - LeafCertificateVerifier::new( - leaf_cert, - Arc::new(WebPkiVerifier::new(root_cert_store, None)), - ) - }; - - let config = rustls::ClientConfig::builder() - .with_safe_defaults() - .with_custom_certificate_verifier(Arc::new(verifier)) - .with_no_client_auth(); - - // Create a client builder - Client::builder().use_preconfigured_tls(config) +#[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 index f3b6f2c..82be409 100644 --- a/crate/client/src/result.rs +++ b/crate/client/src/result.rs @@ -5,7 +5,14 @@ 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, @@ -31,7 +38,7 @@ where impl ClientResultHelper for Option { fn context(self, context: &str) -> RestClientResult { - self.ok_or_else(|| ClientError::Default(context.to_string())) + self.ok_or_else(|| ClientError::Default(context.to_owned())) } fn with_context(self, op: O) -> RestClientResult diff --git a/crate/client/test_data/configs/findex.json b/crate/client/test_data/configs/findex.json index 4d4b1a7..6aa923b 100644 --- a/crate/client/test_data/configs/findex.json +++ b/crate/client/test_data/configs/findex.json @@ -1,5 +1,4 @@ { - "findex_server_url": "http://127.0.0.1:666{}", - "findex_access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A", - "findex_database_secret": "eyJncm91cF9pZCI6MTI5Nzc5NzcwNTcwMzM2OTQxOTA4NDM5ODkzODc0OTI0MDQ5MTkyLCJrZXkiOiI1OTMzOGMxM2Y5ZjY5YmY5ZTM1MDJjNzM0ZjlhZjNlMDRiMzE4OWI1YzVjYmZiMGZhNTJjMWIwYjE5ZmNlN2U3In0=" + "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/client/test_data/configs/findex_partial.json b/crate/client/test_data/configs/findex_partial.json deleted file mode 100644 index 59029df..0000000 --- a/crate/client/test_data/configs/findex_partial.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "findex_server_url": "http://127.0.0.1:666{}", - "findex_access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" -} diff --git a/crate/logger/Cargo.toml b/crate/logger/Cargo.toml index 7807f79..0568c86 100644 --- a/crate/logger/Cargo.toml +++ b/crate/logger/Cargo.toml @@ -9,4 +9,4 @@ rust-version.workspace = true [dependencies] tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index ac1c0a3..8ca1e12 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -2,12 +2,12 @@ name = "cosmian_findex_server" version.workspace = true authors.workspace = true +categories.workspace = true edition.workspace = true +keywords.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true -categories.workspace = true -keywords.workspace = true description = "Cosmian Findex server" [[bin]] @@ -31,11 +31,9 @@ actix-service = "2.0" actix-tls = "3.4" actix-web = { workspace = true, features = ["macros", "openssl"] } alcoholic_jwt = "4091" -argon2 = "0.5" -async-trait = "0.1" +async-trait = "0.1.83" base64 = { workspace = true } -chrono = { workspace = true } -cosmian_logger = { path = "../logger" } +chrono = "0.4" clap = { workspace = true, features = [ "help", "env", @@ -45,21 +43,11 @@ clap = { workspace = true, features = [ "derive", "cargo", ] } -cloudproof = { workspace = true } -cloudproof_findex = { version = "5.0", features = ["findex-redis"] } +cloudproof_findex = { workspace = true, features = ["redis-interface"] } +cosmian_logger = { path = "../logger" } dotenvy = "0.15" futures = "0.3" -hex = { workspace = true, default-features = false } -lazy_static = "1.5" -num-bigint-dig = { workspace = true, features = [ - "std", - "rand", - "serde", - "zeroize", -] } -num_cpus = { workspace = true } openssl = { workspace = true, default-features = false } -rawsql = "0.1" redis = { version = "0.23", features = [ "aio", "ahash", @@ -82,10 +70,9 @@ sqlx = { version = "0.8.2", default-features = false, features = [ "sqlite", ] } thiserror = { workspace = true } -time = { workspace = true, features = ["local-offset", "formatting"] } tokio = { workspace = true, features = ["full"] } toml = "0.8" tracing = { workspace = true } url = { workspace = true } +uuid = { workspace = true, features = ["v4"] } x509-parser = { workspace = true } -zeroize = { workspace = true, default-features = false } diff --git a/crate/server/src/config/command_line/clap_config.rs b/crate/server/src/config/command_line/clap_config.rs index 2ff91d9..1ed274e 100644 --- a/crate/server/src/config/command_line/clap_config.rs +++ b/crate/server/src/config/command_line/clap_config.rs @@ -3,7 +3,7 @@ use std::fmt::{self}; use clap::Parser; use serde::{Deserialize, Serialize}; -use super::{DBConfig, HttpConfig, JwtAuthConfig, WorkspaceConfig}; +use super::{DBConfig, HttpConfig, JwtAuthConfig}; const DEFAULT_USERNAME: &str = "admin"; @@ -13,7 +13,6 @@ impl Default for ClapConfig { db: DBConfig::default(), http: HttpConfig::default(), auth: JwtAuthConfig::default(), - workspace: WorkspaceConfig::default(), default_username: DEFAULT_USERNAME.to_owned(), force_default_username: false, } @@ -33,15 +32,13 @@ pub struct ClapConfig { #[clap(flatten)] pub auth: JwtAuthConfig, - #[clap(flatten)] - pub workspace: WorkspaceConfig, - /// The default username to use when no authentication method is provided #[clap(long, env = "FINDEX_SERVER_DEFAULT_USERNAME", default_value = DEFAULT_USERNAME)] pub default_username: String, /// When an authentication method is provided, perform the authentication - /// but always use the default username instead of the one provided by the authentication method + /// but always use the default username instead of the one provided by the + /// authentication method #[clap(long, env = "FINDEX_SERVER_FORCE_DEFAULT_USERNAME")] pub force_default_username: bool, } @@ -56,7 +53,6 @@ impl fmt::Debug for ClapConfig { x }; let x = x.field("Findex server http", &self.http); - let x = x.field("workspace", &self.workspace); let x = x.field("default username", &self.default_username); let x = x.field("force default username", &self.force_default_username); x.finish() diff --git a/crate/server/src/config/command_line/db.rs b/crate/server/src/config/command_line/db.rs index 0f2ff92..5779904 100644 --- a/crate/server/src/config/command_line/db.rs +++ b/crate/server/src/config/command_line/db.rs @@ -1,15 +1,16 @@ use std::{fmt::Display, path::PathBuf}; -use clap::Args; -use cloudproof_findex::Label; +use clap::{Args, ValueEnum}; use serde::{Deserialize, Serialize}; use url::Url; -use super::workspace::WorkspaceConfig; -use crate::{ - config::params::DbParams, database::RedisWithFindex, findex_server_bail, findex_server_error, - result::FResult, -}; +use crate::{config::params::DbParams, error::result::FResult, findex_server_error}; + +#[derive(ValueEnum, Clone, Deserialize, Serialize)] +pub enum DatabaseType { + // Sqlite, + Redis, +} pub const DEFAULT_SQLITE_PATH: &str = "./sqlite-data"; @@ -18,22 +19,20 @@ pub const DEFAULT_SQLITE_PATH: &str = "./sqlite-data"; #[serde(default)] pub struct DBConfig { /// The database type of the Findex server - /// - sqlite: `SQLite`. The data will be stored at the `sqlite_path` directory - /// - redis-findex: a Redis database with encrypted data and encrypted indexes thanks to Findex. - /// The Redis url must be provided, as well as the redis-master-password and the redis-findex-label - #[clap( - long, - env("FINDEX_SERVER_DATABASE_TYPE"), - value_parser(["sqlite", "redis-findex"]), - verbatim_doc_comment - )] - pub database_type: Option, + /// - sqlite: `SQLite`. The data will be stored at the `sqlite_path` + /// directory + /// - redis-findex: a Redis database with encrypted data and encrypted + /// indexes thanks to Findex. The Redis url must be provided, as well as + /// the redis-master-password and the redis-findex-label + #[clap(long, env("FINDEX_SERVER_DATABASE_TYPE"), verbatim_doc_comment)] + pub database_type: Option, /// The url of the database for findex-redis #[clap( long, env = "FINDEX_SERVER_DATABASE_URL", - required_if_eq_any([("database_type", "redis-findex")]) + required_if_eq_any([("database_type", "redis-findex")]), + default_value = "redis://localhost:6379" )] pub database_url: Option, @@ -42,27 +41,10 @@ pub struct DBConfig { long, env = "FINDEX_SERVER_SQLITE_PATH", default_value = DEFAULT_SQLITE_PATH, - required_if_eq_any([("database_type", "sqlite"), ("database_type", "sqlite-enc")]) + required_if_eq_any([("database_type", "sqlite")]) )] pub sqlite_path: PathBuf, - /// redis-findex: a master password used to encrypt the Redis data and indexes - #[clap( - long, - env = "FINDEX_SERVER_REDIS_MASTER_PASSWORD", - required_if_eq("database_type", "redis-findex") - )] - pub redis_master_password: Option, - - /// redis-findex: a public arbitrary label that can be changed to rotate the Findex ciphertexts - /// without changing the key - #[clap( - long, - env = "FINDEX_SERVER_REDIS_FINDEX_LABEL", - required_if_eq("database_type", "redis-findex") - )] - pub redis_findex_label: Option, - /// Clear the database on start. /// WARNING: This will delete ALL the data in the database #[clap(long, env = "FINDEX_SERVER_CLEAR_DATABASE", verbatim_doc_comment)] @@ -74,10 +56,8 @@ impl Default for DBConfig { Self { sqlite_path: PathBuf::from(DEFAULT_SQLITE_PATH), database_type: None, - database_url: None, + database_url: Some("redis://localhost:6379".to_owned()), clear_database: false, - redis_master_password: None, - redis_findex_label: None, } } } @@ -85,22 +65,15 @@ impl Default for DBConfig { impl Display for DBConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(database_type) = &self.database_type { - match database_type.as_str() { - "sqlite" => write!(f, "sqlite: {}", self.sqlite_path.display()), - "redis-findex" => write!( + match database_type { + DatabaseType::Redis => write!( f, - "redis-findex: {}, password: [****], label: 0x{}", + "redis: {}", &self .database_url .as_ref() .map_or("[INVALID LABEL]", |url| url.as_str()), - hex::encode( - self.redis_findex_label - .as_ref() - .map_or("[INVALID LABEL]", |url| url.as_str()), - ) ), - unknown => write!(f, "Unknown database type: {unknown}"), }?; } else { write!(f, "No database configuration provided")?; @@ -119,47 +92,21 @@ impl DBConfig { /// Initialize the DB parameters based on the command-line parameters /// /// # Parameters - /// - `workspace`: The workspace configuration used to determine the public and shared paths + /// - `workspace`: The workspace configuration used to determine the public + /// and shared paths /// /// # Returns /// - The DB parameters - pub(crate) fn init(&self, workspace: &WorkspaceConfig) -> FResult> { + pub(crate) fn init(&self) -> FResult> { Ok(if let Some(database_type) = &self.database_type { - Some(match database_type.as_str() { - "sqlite" => { - let path = workspace.finalize_directory(&self.sqlite_path)?; - DbParams::Sqlite(path) - } - "redis-findex" => { + Some(match database_type { + DatabaseType::Redis => { let url = ensure_url(self.database_url.as_deref(), "FINDEX_SERVER_REDIS_URL")?; - // Check if a Redis master password was provided - let redis_master_password = ensure_value( - self.redis_master_password.as_deref(), - "redis-master-password", - "FINDEX_SERVER_REDIS_MASTER_PASSWORD", - )?; - - // Generate the symmetric key from the master password - let master_key = - RedisWithFindex::master_key_from_password(&redis_master_password)?; - - let redis_findex_label = ensure_value( - self.redis_findex_label.as_deref(), - "redis-findex-label", - "FINDEX_SERVER_REDIS_FINDEX_LABEL", - )?; - DbParams::RedisFindex( - url, - master_key, - Label::from(redis_findex_label.into_bytes()), - ) + DbParams::Redis(url) } - unknown => findex_server_bail!("Unknown database type: {unknown}"), }) } else { - // No database configuration provided; use the default config - let path = workspace.finalize_directory(&self.sqlite_path)?; - Some(DbParams::Sqlite(path)) + return Err(findex_server_error!("No database configuration provided")); }) } } @@ -170,7 +117,8 @@ fn ensure_url(database_url: Option<&str>, alternate_env_variable: &str) -> FResu std::env::var(alternate_env_variable).map_err(|_e| { findex_server_error!( "No database URL supplied either using the 'database-url' option, or the \ - FINDEX_SERVER_DATABASE_URL or the {alternate_env_variable} environment variables", + FINDEX_SERVER_DATABASE_URL or the {alternate_env_variable} environment \ + variables", ) }) }, @@ -179,22 +127,3 @@ fn ensure_url(database_url: Option<&str>, alternate_env_variable: &str) -> FResu let url = Url::parse(&url)?; Ok(url) } - -fn ensure_value( - value: Option<&str>, - option_name: &str, - env_variable_name: &str, -) -> FResult { - value.map_or_else( - || { - std::env::var(env_variable_name).map_err(|_e| { - findex_server_error!( - "No value supplied either using the {} option, or the {} environment variable", - option_name, - env_variable_name - ) - }) - }, - |value| Ok(value.to_owned()), - ) -} diff --git a/crate/server/src/config/command_line/http_config.rs b/crate/server/src/config/command_line/http_config.rs index 6f7d895..f9f6935 100644 --- a/crate/server/src/config/command_line/http_config.rs +++ b/crate/server/src/config/command_line/http_config.rs @@ -17,7 +17,8 @@ pub struct HttpConfig { #[clap(long, env = "FINDEX_SERVER_HOSTNAME", default_value = DEFAULT_HOSTNAME)] pub hostname: String, - /// The Findex server optional PKCS#12 Certificates and Key file. If provided, this will start the server in HTTPS mode. + /// The Findex server optional PKCS#12 Certificates and Key file. If + /// provided, this will start the server in HTTPS mode. #[clap(long, env = "FINDEX_SERVER_HTTPS_P12_FILE")] pub https_p12_file: Option, @@ -25,9 +26,11 @@ pub struct HttpConfig { #[clap(long, env = "FINDEX_SERVER_HTTPS_P12_PASSWORD")] pub https_p12_password: Option, - /// The server optional authority X509 certificate in PEM format used to validate the client certificate presented for authentication. - /// If provided, this will require clients to present a certificate signed by this authority for authentication. - /// The server must run in TLS mode for this to be used. + /// The server optional authority X509 certificate in PEM format used to + /// validate the client certificate presented for authentication. + /// If provided, this will require clients to present a certificate signed + /// by this authority for authentication. The server must run in TLS + /// mode for this to be used. #[clap(long, env = "FINDEX_SERVER_AUTHORITY_CERT_FILE")] pub authority_cert_file: Option, } diff --git a/crate/server/src/config/command_line/jwt_auth_config.rs b/crate/server/src/config/command_line/jwt_auth_config.rs index ef0a086..f3c29d3 100644 --- a/crate/server/src/config/command_line/jwt_auth_config.rs +++ b/crate/server/src/config/command_line/jwt_auth_config.rs @@ -1,7 +1,7 @@ use clap::Args; use serde::{Deserialize, Serialize}; -use crate::{config::IdpConfig, error::FindexServerError, findex_server_ensure}; +use crate::{config::IdpConfig, error::server::FindexServerError, findex_server_ensure}; // Support for JWT token inspired by the doc at : https://cloud.google.com/api-gateway/docs/authenticating-users-jwt // and following pages @@ -11,16 +11,16 @@ use crate::{config::IdpConfig, error::FindexServerError, findex_server_ensure}; pub struct JwtAuthConfig { /// The issuer URI of the JWT token /// - /// To handle multiple identity managers, add different parameters under each argument - /// (jwt-issuer-uri, jwks-uri and optionally jwt-audience), keeping them in - /// the same order : + /// To handle multiple identity managers, add different parameters under + /// each argument (jwt-issuer-uri, jwks-uri and optionally + /// jwt-audience), keeping them in the same order : /// /// --`jwt_issuer_uri` <`JWT_ISSUER_URI_1`> <`JWT_ISSUER_URI_2`> /// --`jwks_uri` <`JWKS_URI_1`> <`JWKS_URI_2`> /// --`jwt_audience` <`JWT_AUDIENCE_1`> <`JWT_AUDIENCE_2`> /// - /// For Auth0, this is the delegated authority domain configured on Auth0, for instance - /// `https://..auth0.com/` + /// For Auth0, this is the delegated authority domain configured on Auth0, + /// for instance `https://..auth0.com/` /// /// For Google, this would be `https://accounts.google.com` #[clap(long, env = "FINDEX_SERVER_JWT_ISSUER_URI", num_args = 1..)] @@ -28,9 +28,9 @@ pub struct JwtAuthConfig { /// The JWKS (Json Web Key Set) URI of the JWT token /// - /// To handle multiple identity managers, add different parameters under each argument - /// (jwt-issuer-uri, jwks-uri and optionally jwt-audience), keeping them in - /// the same order + /// To handle multiple identity managers, add different parameters under + /// each argument (jwt-issuer-uri, jwks-uri and optionally + /// jwt-audience), keeping them in the same order /// /// For Auth0, this would be `https://..auth0.com/.well-known/jwks.json` /// @@ -42,7 +42,8 @@ pub struct JwtAuthConfig { /// The audience of the JWT token /// - /// Optional: the server will validate the JWT `aud` claim against this value if set + /// Optional: the server will validate the JWT `aud` claim against this + /// value if set #[clap(long, env = "FINDEX_SERVER_JST_AUDIENCE", num_args = 1..)] pub jwt_audience: Option>, } @@ -61,9 +62,11 @@ impl JwtAuthConfig { ) } - /// Parse this configuration into one identity provider configuration per JWT issuer URI. + /// Parse this configuration into one identity provider configuration per + /// JWT issuer URI. /// - /// Assert that when provided, JWKS URI and JWT audience are provided once per JWT issuer URI; + /// Assert that when provided, JWKS URI and JWT audience are provided once + /// per JWT issuer URI; pub(crate) fn extract_idp_configs(self) -> Result>, FindexServerError> { self.jwt_issuer_uri .map(|issuer_uris| { diff --git a/crate/server/src/config/command_line/mod.rs b/crate/server/src/config/command_line/mod.rs index 4a46218..7943446 100644 --- a/crate/server/src/config/command_line/mod.rs +++ b/crate/server/src/config/command_line/mod.rs @@ -2,10 +2,8 @@ mod clap_config; mod db; mod http_config; mod jwt_auth_config; -mod workspace; pub use clap_config::ClapConfig; -pub use db::{DBConfig, DEFAULT_SQLITE_PATH}; +pub use db::{DBConfig, DatabaseType, DEFAULT_SQLITE_PATH}; pub use http_config::HttpConfig; pub use jwt_auth_config::JwtAuthConfig; -pub use workspace::WorkspaceConfig; diff --git a/crate/server/src/config/command_line/workspace.rs b/crate/server/src/config/command_line/workspace.rs deleted file mode 100644 index 584bf38..0000000 --- a/crate/server/src/config/command_line/workspace.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; - -use clap::Args; -use serde::{Deserialize, Serialize}; - -use crate::{findex_server_error, result::FResult}; - -const DEFAULT_ROOT_DATA_PATH: &str = "./cosmian-findex-server"; -const DEFAULT_TMP_PATH: &str = "/tmp"; - -#[derive(Debug, Args, Deserialize, Serialize)] -#[serde(default)] -pub struct WorkspaceConfig { - /// The root folder where the Findex server will store its data - /// A relative path is taken relative to the user HOME directory - #[clap(long, env = "FINDEX_SERVER_ROOT_DATA_PATH", default_value = DEFAULT_ROOT_DATA_PATH)] - pub root_data_path: PathBuf, - - /// The folder to store temporary data (non-persistent data readable by no-one but the current instance during the current execution) - #[clap(long, env = "FINDEX_SERVER_TMP_PATH", default_value = DEFAULT_TMP_PATH)] - pub tmp_path: PathBuf, -} - -impl Default for WorkspaceConfig { - fn default() -> Self { - Self { - root_data_path: PathBuf::from(DEFAULT_ROOT_DATA_PATH), - tmp_path: PathBuf::from(DEFAULT_TMP_PATH), - } - } -} - -impl WorkspaceConfig { - pub(crate) fn init(&self) -> FResult { - let root_data_path = Self::finalize_directory_path(&self.root_data_path, None)?; - let tmp_path = Self::finalize_directory_path(&self.tmp_path, Some(&root_data_path))?; - Ok(Self { - root_data_path, - tmp_path, - }) - } - - /// Transform a relative path to `root_data_path` to an absolute path and ensure that the directory exists. - /// An absolute path is left unchanged. - /// - /// # Arguments - /// - /// * `path` - The path to be transformed. - /// * `relative_root` - The root directory that will be used to make `path` absolute if it's relative. - /// - /// # Returns - /// - /// Returns the canonicalized path. - /// - /// # Errors - /// - /// Returns an error if the directory can't be created or if an error occurs while calling `std::fs::canonicalize` - pub(crate) fn finalize_directory(&self, path: &PathBuf) -> FResult { - Self::finalize_directory_path(path, Some(&self.root_data_path)) - } - - /// Transform a relative path to `root_data_path` to an absolute path and ensure that the directory exists. - /// An absolute path is left unchanged. - /// - /// # Arguments - /// - /// * `path` - The path to be transformed. - /// * `relative_root` - The root directory that will be used to make `path` absolute if it's relative. Or current directory is None. - /// - /// # Returns - /// - /// Returns the canonicalized path. - /// - /// # Errors - /// - /// Returns an error if the directory can't be created or if an error occurs while calling `std::fs::canonicalize` - pub(crate) fn finalize_directory_path( - path: &PathBuf, - relative_root: Option<&Path>, - ) -> FResult { - let path = if path.is_relative() { - relative_root.map_or_else(|| path.clone(), |relative_root| relative_root.join(path)) - } else { - path.clone() - }; - - if !path.exists() { - fs::create_dir_all(&path)?; - } - - fs::canonicalize(path).map_err(|e| findex_server_error!(e)) - } - - /// Transform a relative path to `root_data_path` to an absolute path. - /// An absolute path is left unchanged. - /// - /// # Arguments - /// - /// * `path` - The path to be transformed. - /// - /// # Returns - /// - /// Returns the canonicalized path. - /// - /// # Errors - /// - /// Returns if an error occurs while calling `std::fs::canonicalize` - #[allow(dead_code)] - pub(crate) fn finalize_file_path(&self, path: &PathBuf) -> FResult { - let path = if path.is_relative() { - self.root_data_path.join(path) - } else { - path.clone() - }; - fs::canonicalize(path).map_err(|e| findex_server_error!(e)) - } -} diff --git a/crate/server/src/config/params/db_params.rs b/crate/server/src/config/params/db_params.rs index 4ecde7e..c4fad68 100644 --- a/crate/server/src/config/params/db_params.rs +++ b/crate/server/src/config/params/db_params.rs @@ -1,22 +1,9 @@ -use std::{ - fmt::{self, Display}, - path::PathBuf, -}; +use std::fmt::{self, Display}; -use cloudproof::reexport::crypto_core::SymmetricKey; -use cloudproof_findex::Label; use url::Url; -use crate::database::REDIS_WITH_FINDEX_MASTER_KEY_LENGTH; - pub enum DbParams { - /// contains the dir of the sqlite db file (not the db file itself) - Sqlite(PathBuf), - RedisFindex( - Url, - SymmetricKey, - Label, - ), + Redis(Url), } impl DbParams { @@ -24,8 +11,7 @@ impl DbParams { #[must_use] pub const fn db_name(&self) -> &str { match &self { - Self::Sqlite(_) => "Sqlite", - Self::RedisFindex(_, _, _) => "Redis-Findex", + Self::Redis(_) => "Redis", } } } @@ -33,14 +19,8 @@ impl DbParams { impl Display for DbParams { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Sqlite(path) => write!(f, "sqlite: {}", path.display()), - Self::RedisFindex(url, _, label) => { - write!( - f, - "redis-findex: {}, master key: [****], Findex label: 0x{}", - redact_url(url), - hex::encode(label) - ) + Self::Redis(url) => { + write!(f, "redis: {}", redact_url(url),) } } } diff --git a/crate/server/src/config/params/http_params.rs b/crate/server/src/config/params/http_params.rs index e4f5f5c..c31abbd 100644 --- a/crate/server/src/config/params/http_params.rs +++ b/crate/server/src/config/params/http_params.rs @@ -4,7 +4,7 @@ use openssl::pkcs12::{ParsedPkcs12_2, Pkcs12}; use crate::{ config::HttpConfig, - result::{FResult, FResultHelper}, + error::result::{FResult, FResultHelper}, }; /// The HTTP parameters of the API server @@ -19,15 +19,18 @@ impl HttpParams { /// /// # Arguments /// - /// * `config` - The `HttpConfig` object containing the configuration parameters. + /// * `config` - The `HttpConfig` object containing the configuration + /// parameters. /// /// # Returns /// - /// Returns a `FResult` containing the created `HttpParams` instance on success. + /// Returns a `FResult` containing the created `HttpParams` instance on + /// success. /// /// # Errors /// - /// This function can return an error if there is an issue reading the PKCS#12 file or parsing it. + /// This function can return an error if there is an issue reading the + /// PKCS#12 file or parsing it. pub(crate) fn try_from(config: &HttpConfig) -> FResult { // start in HTTPS mode if a PKCS#12 file is provided if let (Some(p12_file), Some(p12_password)) = @@ -51,7 +54,8 @@ impl HttpParams { /// /// # Returns /// - /// Returns `true` if the server is running in HTTPS mode, `false` otherwise. + /// Returns `true` if the server is running in HTTPS mode, `false` + /// otherwise. #[must_use] pub const fn is_running_https(&self) -> bool { matches!(self, Self::Https(_)) diff --git a/crate/server/src/config/params/server_params.rs b/crate/server/src/config/params/server_params.rs index c5c2f99..fcb4f2e 100644 --- a/crate/server/src/config/params/server_params.rs +++ b/crate/server/src/config/params/server_params.rs @@ -5,8 +5,8 @@ use openssl::x509::X509; use super::{DbParams, HttpParams}; use crate::{ config::{ClapConfig, IdpConfig}, + error::result::FResult, findex_server_bail, - result::FResult, }; /// This structure is the context used by the server @@ -20,7 +20,8 @@ pub struct ServerParams { pub default_username: String, /// When an authentication method is provided, perform the authentication - /// but always use the default username instead of the one provided by the authentication method + /// but always use the default username instead of the one provided by the + /// authentication method pub force_default_username: bool, /// The DB parameters may be supplied on the command line @@ -46,15 +47,18 @@ impl ServerParams { /// /// # Arguments /// - /// * `conf` - The `ClapConfig` object containing the configuration parameters. + /// * `conf` - The `ClapConfig` object containing the configuration + /// parameters. /// /// # Returns /// - /// Returns a `FResult` containing the `ServerParams` instance if successful, or an error if the conversion fails. + /// Returns a `FResult` containing the `ServerParams` instance if + /// successful, or an error if the conversion fails. /// /// # Errors /// - /// Returns an error if the conversion from `ClapConfig` to `ServerParams` fails. + /// Returns an error if the conversion from `ClapConfig` to `ServerParams` + /// fails. pub fn try_from(conf: ClapConfig) -> FResult { let http_params = HttpParams::try_from(&conf.http)?; @@ -76,7 +80,7 @@ impl ServerParams { Ok(Self { identity_provider_configurations: conf.auth.extract_idp_configs()?, - db_params: conf.db.init(&conf.workspace.init()?)?, + db_params: conf.db.init()?, clear_db_on_start: conf.db.clear_database, hostname: conf.http.hostname, port: conf.http.port, @@ -95,11 +99,13 @@ impl ServerParams { /// /// # Returns /// - /// Returns a `FResult` containing the loaded `X509` certificate if successful, or an error if the loading fails. + /// Returns a `FResult` containing the loaded `X509` certificate if + /// successful, or an error if the loading fails. /// /// # Errors /// - /// Returns an error if the certificate file cannot be read or if the parsing of the certificate fails. + /// Returns an error if the certificate file cannot be read or if the + /// parsing of the certificate fails. fn load_cert(authority_cert_file: &PathBuf) -> FResult { // Open and read the file into a byte vector let pem_bytes = std::fs::read(authority_cert_file)?; diff --git a/crate/server/src/core/findex_server.rs b/crate/server/src/core/findex_server.rs deleted file mode 100644 index c5cbf64..0000000 --- a/crate/server/src/core/findex_server.rs +++ /dev/null @@ -1,44 +0,0 @@ -use actix_web::{HttpMessage, HttpRequest}; -use tracing::debug; - -use crate::{ - config::ServerParams, - database::Database, - middlewares::{JwtAuthClaim, PeerCommonName}, -}; - -#[allow(dead_code)] -pub(crate) struct FindexServer { - pub(crate) params: ServerParams, - pub(crate) db: Box, -} - -impl FindexServer { - /// Get the user from the request depending on the authentication method - /// The user is encoded in the JWT `Authorization` header - /// If the header is not present, the user is extracted from the client certificate - /// If the client certificate is not present, the user is extracted from the configuration file - pub(crate) fn get_user(&self, req_http: &HttpRequest) -> String { - let default_username = self.params.default_username.clone(); - - if self.params.force_default_username { - debug!( - "Authenticated using forced default user: {}", - default_username - ); - return default_username; - } - // if there is a JWT token, use it in priority - let user = req_http.extensions().get::().map_or_else( - || { - req_http - .extensions() - .get::() - .map_or(default_username, |claim| claim.common_name.clone()) - }, - |claim| claim.email.clone(), - ); - debug!("Authenticated user: {}", user); - user - } -} diff --git a/crate/server/src/core/implementation.rs b/crate/server/src/core/implementation.rs index 43b0578..324dac2 100644 --- a/crate/server/src/core/implementation.rs +++ b/crate/server/src/core/implementation.rs @@ -1,50 +1,86 @@ -use cloudproof::reexport::crypto_core::FixedSizeCBytes; +use actix_web::{HttpMessage, HttpRequest}; +use tracing::{debug, instrument, trace}; +use super::Permission; use crate::{ config::{DbParams, ServerParams}, - database::{Database, RedisWithFindex, SqlitePool, REDIS_WITH_FINDEX_MASTER_KEY_LENGTH}, + database::{Database, Redis}, + error::result::FResult, findex_server_bail, - result::FResult, - secret::Secret, + middlewares::{JwtAuthClaim, PeerCommonName}, + routes::get_index_id, }; -use super::FindexServer; +pub(crate) struct FindexServer { + pub(crate) params: ServerParams, + pub(crate) db: Box, +} impl FindexServer { pub(crate) async fn instantiate(mut shared_config: ServerParams) -> FResult { - let db: Box = if let Some(mut db_params) = - shared_config.db_params.as_mut() - { - match &mut db_params { - DbParams::Sqlite(db_path) => Box::new( - SqlitePool::instantiate( - &db_path.join("findex_server.db"), - shared_config.clear_db_on_start, - ) - .await?, - ), - DbParams::RedisFindex(url, master_key, label) => { - // There is no reason to keep a copy of the key in the shared config - // So we are going to create a "zeroizable" copy which will be passed to Redis with Findex - // and zeroize the one in the shared config - let new_master_key = - Secret::::from_unprotected_bytes( - &mut master_key.to_bytes(), - ); - // `master_key` implements ZeroizeOnDrop so there is no need - // to manually zeroize. - Box::new( - RedisWithFindex::instantiate(url.as_str(), new_master_key, label).await?, - ) + let db: Box = + if let Some(mut db_params) = shared_config.db_params.as_mut() { + match &mut db_params { + DbParams::Redis(url) => Box::new( + Redis::instantiate(url.as_str(), shared_config.clear_db_on_start).await?, + ), } - } - } else { - findex_server_bail!("Fatal: no database configuration provided. Stopping.") - }; + } else { + findex_server_bail!("Fatal: no database configuration provided. Stopping.") + }; Ok(Self { params: shared_config, db, }) } + + /// Get the user from the request depending on the authentication method + /// The user is encoded in the JWT `Authorization` header + /// If the header is not present, the user is extracted from the client + /// certificate. + /// If the client certificate is not present, the user is + /// extracted from the configuration file + pub(crate) fn get_user(&self, req_http: &HttpRequest) -> String { + let default_username = self.params.default_username.clone(); + + if self.params.force_default_username { + debug!( + "Authenticated using forced default user: {}", + default_username + ); + return default_username; + } + // if there is a JWT token, use it in priority + let user = req_http.extensions().get::().map_or_else( + || { + req_http + .extensions() + .get::() + .map_or(default_username, |claim| claim.common_name.clone()) + }, + |claim| claim.email.clone(), + ); + debug!("Authenticated user: {}", user); + user + } + + #[instrument(ret(Display), err, skip(self))] + pub(crate) async fn get_permission( + &self, + user_id: &str, + index_id: &str, + ) -> FResult { + if user_id == self.params.default_username { + trace!("User is the default user and has admin access"); + return Ok(Permission::Admin); + } + + let permission = self + .db + .get_permission(user_id, &get_index_id(index_id)?) + .await?; + trace!("User {user_id} has: {permission}"); + Ok(permission) + } } diff --git a/crate/server/src/core/mod.rs b/crate/server/src/core/mod.rs index fb90738..3960a1e 100644 --- a/crate/server/src/core/mod.rs +++ b/crate/server/src/core/mod.rs @@ -1,4 +1,5 @@ -pub mod findex_server; pub(crate) mod implementation; +pub(crate) mod permissions; -pub(crate) use findex_server::FindexServer; +pub(crate) use implementation::FindexServer; +pub(crate) use permissions::{Permission, Permissions}; diff --git a/crate/server/src/core/permissions.rs b/crate/server/src/core/permissions.rs new file mode 100644 index 0000000..2097771 --- /dev/null +++ b/crate/server/src/core/permissions.rs @@ -0,0 +1,131 @@ +use std::{collections::HashMap, fmt::Display, str::FromStr}; + +use uuid::Uuid; + +use crate::{ + error::{result::FResult, server::FindexServerError}, + findex_server_bail, +}; + +#[repr(u8)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +pub(crate) enum Permission { + Read = 0, + Write = 1, + Admin = 2, +} + +#[allow(clippy::as_conversions)] +impl From for u8 { + fn from(table: Permission) -> Self { + table as Self + } +} + +impl TryFrom for Permission { + type Error = FindexServerError; + + fn try_from(value: u8) -> FResult { + match value { + 0 => Ok(Self::Read), + 1 => Ok(Self::Write), + 2 => Ok(Self::Admin), + _ => findex_server_bail!("Invalid permission: {}", value), + } + } +} + +impl FromStr for Permission { + type Err = FindexServerError; + + fn from_str(s: &str) -> FResult { + match s { + "read" => Ok(Self::Read), + "write" => Ok(Self::Write), + "admin" => Ok(Self::Admin), + _ => findex_server_bail!("Invalid permission: {}", s), + } + } +} + +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}") + } +} + +const PERMISSION_LENGTH: usize = 1; +const INDEX_ID_LENGTH: usize = 16; + +pub(crate) struct Permissions { + pub permissions: HashMap, +} + +impl Display for Permissions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (index_id, permission) in &self.permissions { + writeln!(f, "Index ID: {index_id}, Permission: {permission}")?; + } + Ok(()) + } +} + +impl Permissions { + pub(crate) 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) { + self.permissions.insert(index_id, permission); + } + + pub(crate) fn revoke_permission(&mut self, index_id: &Uuid) { + self.permissions.remove(index_id); + } + + pub(crate) 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 { + bytes.extend_from_slice(&[u8::from(permission.clone())]); + bytes.extend_from_slice(index_id.as_bytes().as_ref()); + } + bytes + } + + pub(crate) fn deserialize(bytes: &[u8]) -> FResult { + 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()) + })?; + 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( + "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}" + )) + })?; + i += INDEX_ID_LENGTH; + permissions.insert(index_id, permission); + } + Ok(Self { permissions }) + } + + pub(crate) fn get_permission(&self, index_id: &Uuid) -> Option { + self.permissions.get(index_id).cloned() + } +} diff --git a/crate/server/src/database/database_trait.rs b/crate/server/src/database/database_trait.rs index a57d989..55aec66 100644 --- a/crate/server/src/database/database_trait.rs +++ b/crate/server/src/database/database_trait.rs @@ -1,13 +1,64 @@ -use crate::result::FResult; use async_trait::async_trait; +use cloudproof_findex::{ + db_interfaces::{redis::FindexTable, rest::UpsertData}, + reexport::cosmian_findex::{ + TokenToEncryptedValueMap, TokenWithEncryptedValueList, Tokens, ENTRY_LENGTH, LINK_LENGTH, + }, +}; +use uuid::Uuid; -#[async_trait(?Send)] -pub(crate) trait Database { - /// Insert the given Object in the database. - /// - /// A new UUID will be created if none is supplier. - /// This method will fail if a `uid` is supplied - /// and an object with the same id already exists - #[allow(dead_code)] - async fn create(&self) -> FResult<()>; +use crate::{ + core::{Permission, Permissions}, + error::result::FResult, +}; + +#[async_trait] +pub(crate) trait Database: Sync + Send { + async fn fetch_entries( + &self, + index_id: &Uuid, + tokens: Tokens, + ) -> FResult>; + + async fn fetch_chains( + &self, + index_id: &Uuid, + tokens: Tokens, + ) -> FResult>; + + async fn upsert_entries( + &self, + index_id: &Uuid, + upsert_data: UpsertData, + ) -> FResult>; + + async fn insert_chains( + &self, + index_id: &Uuid, + items: TokenToEncryptedValueMap, + ) -> FResult<()>; + + async fn delete( + &self, + index_id: &Uuid, + findex_table: FindexTable, + tokens: Tokens, + ) -> FResult<()>; + + async fn dump_tokens(&self, index_id: &Uuid) -> FResult; + + 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<()>; } diff --git a/crate/server/src/database/mod.rs b/crate/server/src/database/mod.rs index 5606f37..fe10ab6 100644 --- a/crate/server/src/database/mod.rs +++ b/crate/server/src/database/mod.rs @@ -1,24 +1,8 @@ -use lazy_static::lazy_static; -use rawsql::Loader; - mod database_trait; mod redis; -mod sqlite; - -pub(crate) type FServer = crate::core::FindexServer; pub(crate) use database_trait::Database; -pub(crate) use redis::RedisWithFindex; -pub(crate) use redis::REDIS_WITH_FINDEX_MASTER_KEY_LENGTH; -pub(crate) use sqlite::SqlitePool; - -const SQLITE_FILE_QUERIES: &str = include_str!("query.sql"); - -lazy_static! { - static ref SQLITE_QUERIES: Loader = #[allow(clippy::expect_used)] - Loader::get_queries_from(SQLITE_FILE_QUERIES) - .expect("Can't parse the SQL file"); -} +pub(crate) use redis::Redis; #[cfg(test)] mod tests; diff --git a/crate/server/src/database/query.sql b/crate/server/src/database/query.sql deleted file mode 100644 index d300e05..0000000 --- a/crate/server/src/database/query.sql +++ /dev/null @@ -1,140 +0,0 @@ --- name: create-table-context -CREATE TABLE IF NOT EXISTS context ( - version VARCHAR(40) PRIMARY KEY, - state VARCHAR(40) -); - --- name: create-table-objects -CREATE TABLE IF NOT EXISTS objects ( - id VARCHAR(40) PRIMARY KEY, - object json NOT NULL, - attributes json NOT NULL, - state VARCHAR(32), - owner VARCHAR(255) -); --- name: add-column-attributes -ALTER TABLE objects ADD COLUMN attributes json; --- name: has-column-attributes -SELECT attributes from objects; - --- name: create-table-read_access -CREATE TABLE IF NOT EXISTS read_access ( - id VARCHAR(40), - userid VARCHAR(255), - permissions json NOT NULL, - UNIQUE (id, userid) -); - --- name: create-table-tags -CREATE TABLE IF NOT EXISTS tags ( - id VARCHAR(40), - tag VARCHAR(255), - UNIQUE (id, tag) -); - --- name: clean-table-context -DELETE FROM context; - --- name: clean-table-objects -DELETE FROM objects; - --- name: clean-table-read_access -DELETE FROM read_access; - --- name: clean-table-tags -DELETE FROM tags; - --- name: select-context -SELECT * FROM context ORDER BY version ASC LIMIT 1; - --- name: insert-context -INSERT INTO context (version, state) VALUES ($1, $2); - --- name: update-context -UPDATE context SET version=$1, state=$2 WHERE state=$3; - --- name: delete-version -DELETE FROM context WHERE version=$1; - --- name: insert-objects -INSERT INTO objects (id, object, attributes, state, owner) VALUES ($1, $2, $3, $4, $5); - --- name: select-object -SELECT objects.id, objects.object, objects.attributes, objects.owner, objects.state, read_access.permissions - FROM objects - LEFT JOIN read_access - ON objects.id = read_access.id AND ( read_access.userid=$2 OR read_access.userid='*' ) - WHERE objects.id=$1; - --- name: update-object-with-object -UPDATE objects SET object=$1, attributes=$2 WHERE id=$3; - --- name: update-object-with-state -UPDATE objects SET state=$1 WHERE id=$2; - --- name: delete-object -DELETE FROM objects WHERE id=$1 AND owner=$2; - --- name: upsert-object -INSERT INTO objects (id, object, attributes, state, owner) VALUES ($1, $2, $3, $4, $5) - ON CONFLICT(id) - DO UPDATE SET object=$2, attributes=$3, state=$4, owner=$5 - WHERE objects.id=$1; - WHERE objects.owner=$5; - --- name: select-user-accesses-for-object -SELECT permissions - FROM read_access - WHERE id=$1 AND userid=$2; - --- name: upsert-row-read_access -INSERT INTO read_access (id, userid, permissions) VALUES ($1, $2, $3) - ON CONFLICT(id, userid) - DO UPDATE SET permissions=$3 - WHERE read_access.id=$1 AND read_access.userid=$2; - --- name: delete-rows-read_access -DELETE FROM read_access WHERE id=$1 AND userid=$2; - --- name: has-row-objects -SELECT 1 FROM objects WHERE id=$1 AND owner=$2; - --- name: update-rows-read_access-with-permission -UPDATE read_access SET permissions=$3 - WHERE id=$1 AND userid=$2; - --- name: select-rows-read_access-with-object-id -SELECT userid, permissions - FROM read_access - WHERE id=$1; - --- name: select-objects-access-obtained -SELECT objects.id, objects.owner, objects.state, read_access.permissions - FROM objects - INNER JOIN read_access - ON objects.id = read_access.id - WHERE read_access.userid=$1; - --- name: insert-tags -INSERT INTO tags (id, tag) VALUES ($1, $2); - --- name: select-tags -SELECT tag FROM tags WHERE id=$1; - --- name: delete-tags -DELETE FROM tags WHERE id=$1; - - --- name: select-from-tags -SELECT objects.id, objects.object, objects.attributes, objects.owner, objects.state, read_access.permissions -FROM objects -INNER JOIN ( - SELECT id - FROM tags - WHERE tag IN (@TAGS) - GROUP BY id - HAVING COUNT(DISTINCT tag) = @LEN -) AS matched_tags -ON objects.id = matched_tags.id -LEFT JOIN read_access -ON objects.id = read_access.id AND (read_access.userid=@USER OR read_access.userid='*' ); diff --git a/crate/server/src/database/redis.rs b/crate/server/src/database/redis.rs new file mode 100644 index 0000000..cec902b --- /dev/null +++ b/crate/server/src/database/redis.rs @@ -0,0 +1,348 @@ +use std::{ + collections::{HashMap, HashSet}, + convert::TryFrom, +}; + +use async_trait::async_trait; +use cloudproof_findex::{ + db_interfaces::{ + redis::{FindexTable, TABLE_PREFIX_LENGTH}, + rest::UpsertData, + }, + reexport::cosmian_findex::{ + CoreError, EncryptedValue, Token, TokenToEncryptedValueMap, TokenWithEncryptedValueList, + Tokens, ENTRY_LENGTH, LINK_LENGTH, + }, +}; +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}, +}; + +/// The conditional upsert script used to only update a table if the +/// indexed value matches ARGV[2]. When the value does not match, the +/// indexed value is returned. +const CONDITIONAL_UPSERT_SCRIPT: &str = r" + local value=redis.call('GET',ARGV[1]) + if ((value==false) or (ARGV[2] == value)) then + redis.call('SET', ARGV[1], ARGV[3]) + return + else + return value + end; + "; + +/// Generate a key for the entry table or chain table +fn build_key(index_id: &Uuid, table: FindexTable, uid: &[u8]) -> Vec { + [index_id.as_bytes().as_ref(), &[0x00, u8::from(table)], uid].concat() +} + +pub(crate) struct Redis { + mgr: ConnectionManager, + upsert_script: Script, +} + +impl Redis { + pub(crate) async fn instantiate(redis_url: &str, clear_database: bool) -> FResult { + let client = redis::Client::open(redis_url)?; + let mgr = ConnectionManager::new(client).await?; + + if clear_database { + info!("Warning: Irreversible operation: clearing the database"); + Self::clear_database(mgr.clone()).await?; + } + Ok(Self { + mgr, + upsert_script: Script::new(CONDITIONAL_UPSERT_SCRIPT), + }) + } + + pub(crate) async fn clear_database(mgr: ConnectionManager) -> FResult<()> { + redis::cmd("FLUSHDB") + .query_async::<_, ()>(&mut mgr.clone()) + .await?; + Ok(()) + } +} + +#[async_trait] +impl Database for Redis { + // todo(manu): merge the 2 fetch + #[instrument(ret(Display), err, skip_all)] + async fn fetch_entries( + &self, + index_id: &Uuid, + tokens: Tokens, + ) -> FResult> { + trace!("fetch_entries: number of tokens: {}", tokens.len()); + let uids = tokens.into_iter().collect::>(); + trace!("fetch_entries: uids len: {}", uids.len()); + + let redis_keys = uids + .iter() + .map(|uid| build_key(index_id, FindexTable::Entry, uid)) + .collect::>(); + trace!("fetch_entries: redis_keys len: {}", redis_keys.len()); + + let values: Vec> = self.mgr.clone().mget(redis_keys).await?; + // Zip and filter empty values out. + let res = uids + .into_iter() + .zip(values) + .filter_map(|(k, v)| { + if v.is_empty() { + None + } else { + Some(EncryptedValue::try_from(v.as_slice()).map(|v| (k, v))) + } + }) + .collect::, CoreError>>()?; + trace!("fetch_entries: non empty tuples len: {}", res.len()); + + let result: TokenWithEncryptedValueList = res.into(); + + Ok(result) + } + + #[instrument(ret(Display), err, skip_all)] + async fn fetch_chains( + &self, + index_id: &Uuid, + tokens: Tokens, + ) -> FResult> { + trace!("fetch_chains: number of tokens: {}", tokens.len()); + let uids = tokens.into_iter().collect::>(); + trace!("fetch_chains: uids len: {}", uids.len()); + + let redis_keys = uids + .iter() + .map(|uid| build_key(index_id, FindexTable::Chain, uid)) + .collect::>(); + trace!("fetch_chains: redis_keys len: {}", redis_keys.len()); + + let values: Vec> = self.mgr.clone().mget(redis_keys).await?; + // Zip and filter empty values out. + let res = uids + .into_iter() + .zip(values) + .filter_map(|(k, v)| { + if v.is_empty() { + None + } else { + Some(EncryptedValue::try_from(v.as_slice()).map(|v| (k, v))) + } + }) + .collect::, CoreError>>()?; + trace!("fetch_entries: non empty tuples len: {}", res.len()); + + let result: TokenWithEncryptedValueList = res.into(); + + Ok(result) + } + + #[instrument(ret(Display), err, skip_all)] + async fn upsert_entries( + &self, + index_id: &Uuid, + upsert_data: UpsertData, + ) -> FResult> { + trace!( + "upsert_entries: number of upsert data: {}", + upsert_data.len() + ); + + let mut old_values = HashMap::with_capacity(upsert_data.len()); + let mut new_values = HashMap::with_capacity(upsert_data.len()); + for (token, (old_value, new_value)) in upsert_data { + if let Some(old_value) = old_value { + old_values.insert(token, old_value); + } + new_values.insert(token, new_value); + } + + trace!( + "upsert_entries: number of old_values {}, number of new_values {}", + old_values.len(), + new_values.len() + ); + + let mut rejected = HashMap::with_capacity(new_values.len()); + for (uid, new_value) in new_values { + let new_value = Vec::from(&new_value); + let old_value = old_values.get(&uid).map(Vec::from).unwrap_or_default(); + let key = build_key(index_id, FindexTable::Entry, &uid); + + let indexed_value: Vec<_> = self + .upsert_script + .arg(key) + .arg(old_value) + .arg(new_value) + .invoke_async(&mut self.mgr.clone()) + .await?; + + if !indexed_value.is_empty() { + let encrypted_value = EncryptedValue::try_from(indexed_value.as_slice())?; + rejected.insert(uid, encrypted_value); + } + } + + trace!("upsert_entries: rejected: {}", rejected.len()); + + Ok(rejected.into()) + } + + #[instrument(ret, err, skip_all)] + async fn insert_chains( + &self, + index_id: &Uuid, + items: TokenToEncryptedValueMap, + ) -> FResult<()> { + let mut pipe = pipe(); + for (k, v) in &*items { + pipe.set(build_key(index_id, FindexTable::Chain, k), Vec::from(v)); + } + pipe.atomic() + .query_async(&mut self.mgr.clone()) + .await + .map_err(FindexServerError::from) + } + + #[instrument(ret, err, skip_all)] + async fn delete( + &self, + index_id: &Uuid, + findex_table: FindexTable, + entry_uids: Tokens, + ) -> FResult<()> { + let mut pipeline = pipe(); + for uid in entry_uids { + pipeline.del(build_key(index_id, findex_table, &uid)); + } + pipeline + .atomic() + .query_async(&mut self.mgr.clone()) + .await + .map_err(FindexServerError::from) + } + + #[instrument(ret(Display), err, skip_all)] + #[allow(clippy::indexing_slicing)] + async fn dump_tokens(&self, index_id: &Uuid) -> FResult { + let keys: Vec> = self + .mgr + .clone() + .keys(build_key(index_id, FindexTable::Entry, b"*")) + .await?; + + trace!("dumping {} keywords (ET+CT)", keys.len()); + + let mut tokens_set = HashSet::new(); + for key in keys { + if key[..TABLE_PREFIX_LENGTH] == [0x00, u8::from(FindexTable::Entry)] { + if let Ok(token) = Token::try_from(&key[TABLE_PREFIX_LENGTH..]) { + tokens_set.insert(token); + } + } + } + Ok(Tokens::from(tokens_set)) + } + + #[instrument(ret(Display), err, skip(self))] + async fn create_index_id(&self, user_id: &str) -> FResult { + let uuid = Uuid::new_v4(); + let key = user_id.as_bytes().to_vec(); + let permissions = (self.get_permissions(user_id).await).map_or_else( + |_error| Permissions::new(uuid, Permission::Admin), + |mut permissions| { + permissions.grant_permission(uuid, Permission::Admin); + permissions + }, + ); + self.mgr + .clone() + .set::<_, _, ()>(key, permissions.serialize()) + .await?; + + Ok(uuid) + } + + #[instrument(ret(Display), err, skip(self))] + async fn get_permissions(&self, user_id: &str) -> FResult { + let key = user_id.as_bytes().to_vec(); + + let value: Option> = self.mgr.clone().get(key).await?; + trace!("get_permissions: value: {:?}", value); + let serialized_value = value.ok_or_else(|| { + FindexServerError::Unauthorized(format!( + "No permission for {user_id} since unwrapping serialized value failed" + )) + })?; + Permissions::deserialize(&serialized_value) + } + + #[instrument(ret(Display), err, skip(self))] + async fn get_permission(&self, user_id: &str, index_id: &Uuid) -> FResult { + let permissions = self.get_permissions(user_id).await?; + let permission = permissions.get_permission(index_id).ok_or_else(|| { + FindexServerError::Unauthorized(format!( + "No permission for {user_id} on index {index_id}" + )) + })?; + + Ok(permission) + } + + #[instrument(ret, err, skip(self))] + async fn grant_permission( + &self, + user_id: &str, + permission: Permission, + index_id: &Uuid, + ) -> FResult<()> { + let key = user_id.as_bytes().to_vec(); + let permissions = match self.get_permissions(user_id).await { + Ok(mut permissions) => { + permissions.grant_permission(*index_id, permission); + permissions + } + Err(_) => Permissions::new(*index_id, permission), + }; + + self.mgr + .clone() + .set::<_, _, ()>(key, permissions.serialize()) + .await + .map_err(FindexServerError::from) + + // let mut pipe = pipe(); + // pipe.set(key, permissions.serialize()); + // pipe.atomic() + // .query_async(&mut self.mgr.clone()) + // .await + // .map_err(FindexServerError::from) + } + + #[instrument(ret, err, skip(self))] + async fn revoke_permission(&self, user_id: &str, index_id: &Uuid) -> FResult<()> { + let key = user_id.as_bytes().to_vec(); + match self.get_permissions(user_id).await { + Ok(mut permissions) => { + permissions.revoke_permission(index_id); + self.mgr + .clone() + .set::<_, _, ()>(key, permissions.serialize()) + .await?; + } + Err(_) => { + trace!("Nothing to revoke since no permission found for index {index_id}"); + } + }; + + Ok(()) + } +} diff --git a/crate/server/src/database/redis/mod.rs b/crate/server/src/database/redis/mod.rs deleted file mode 100644 index c13c17f..0000000 --- a/crate/server/src/database/redis/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -use argon2::Argon2; -use async_trait::async_trait; -use cloudproof::reexport::crypto_core::{reexport::rand_core::SeedableRng, FixedSizeCBytes}; -use std::{ - collections::HashSet, - sync::{Arc, Mutex}, -}; - -use cloudproof::reexport::crypto_core::{kdf256, Aes256Gcm, CsRng, Instantiable, SymmetricKey}; -use cloudproof_findex::{ - implementations::redis::{FindexRedis, FindexRedisError, RemovedLocationsFinder}, - parameters::MASTER_KEY_LENGTH, - Label, Location, -}; -use redis::aio::ConnectionManager; - -use crate::{error::FindexServerError, result::FResult, secret::Secret}; - -use super::Database; - -pub(crate) const REDIS_WITH_FINDEX_MASTER_KEY_LENGTH: usize = 32; -pub(crate) const REDIS_WITH_FINDEX_MASTER_KEY_DERIVATION_SALT: &[u8; 16] = b"rediswithfindex_"; -pub(crate) const REDIS_WITH_FINDEX_MASTER_FINDEX_KEY_DERIVATION_SALT: &[u8; 6] = b"findex"; -pub(crate) const REDIS_WITH_FINDEX_MASTER_DB_KEY_DERIVATION_SALT: &[u8; 2] = b"db"; - -pub(crate) const DB_KEY_LENGTH: usize = 32; - -#[allow(dead_code)] -pub(crate) struct ObjectsDB { - mgr: ConnectionManager, - dem: Aes256Gcm, - rng: Mutex, -} - -impl ObjectsDB { - pub(crate) fn new(mgr: ConnectionManager, db_key: &SymmetricKey) -> Self { - Self { - mgr, - dem: Aes256Gcm::new(db_key), - rng: Mutex::new(CsRng::from_entropy()), - } - } -} -#[async_trait] -impl RemovedLocationsFinder for ObjectsDB { - async fn find_removed_locations( - &self, - _locations: HashSet, - ) -> Result, FindexRedisError> { - // Objects and permissions are never removed from the DB - Ok(HashSet::new()) - } -} - -#[allow(dead_code)] -pub(crate) struct RedisWithFindex { - objects_db: Arc, - findex: Arc, - findex_key: SymmetricKey, - label: Label, -} - -impl RedisWithFindex { - pub(crate) async fn instantiate( - redis_url: &str, - master_key: Secret, - label: &[u8], - ) -> FResult { - // derive a Findex Key - let mut findex_key = SymmetricKey::::default(); - kdf256!( - &mut findex_key, - REDIS_WITH_FINDEX_MASTER_FINDEX_KEY_DERIVATION_SALT, - &*master_key - ); - // derive a DB Key - let mut db_key = SymmetricKey::::default(); - kdf256!( - &mut db_key, - REDIS_WITH_FINDEX_MASTER_DB_KEY_DERIVATION_SALT, - &*master_key - ); - - let client = redis::Client::open(redis_url)?; - let mgr = ConnectionManager::new(client).await?; - let objects_db = Arc::new(ObjectsDB::new(mgr.clone(), &db_key)); - let findex = - Arc::new(FindexRedis::connect_with_manager(mgr.clone(), objects_db.clone()).await?); - Ok(Self { - objects_db, - findex, - findex_key, - label: Label::from(label), - }) - } - - pub(crate) fn master_key_from_password( - master_password: &str, - ) -> FResult> { - let output_key_material = derive_key_from_password::( - REDIS_WITH_FINDEX_MASTER_KEY_DERIVATION_SALT, - master_password.as_bytes(), - )?; - - let master_secret_key: SymmetricKey = - SymmetricKey::try_from_slice(&output_key_material)?; - - Ok(master_secret_key) - } -} - -pub(crate) fn derive_key_from_password( - salt: &[u8; 16], - password: &[u8], -) -> Result, FindexServerError> { - let mut output_key_material = Secret::::new(); - - Argon2::default() - .hash_password_into(password, salt, output_key_material.as_mut()) - .map_err(|e| FindexServerError::CryptographicError(e.to_string()))?; - - Ok(output_key_material) -} - -#[async_trait(?Send)] -impl Database for RedisWithFindex { - async fn create(&self) -> FResult<()> { - Ok(()) - } -} diff --git a/crate/server/src/database/sqlite/mod.rs b/crate/server/src/database/sqlite/mod.rs deleted file mode 100644 index 59afb0b..0000000 --- a/crate/server/src/database/sqlite/mod.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{path::Path, time::Duration}; - -use async_trait::async_trait; -use sqlx::{ - sqlite::{SqliteConnectOptions, SqlitePoolOptions}, - ConnectOptions, Executor, Pool, Sqlite, -}; - -use crate::{ - database::{Database, SQLITE_QUERIES}, - findex_server_error, - result::FResult, -}; - -#[macro_export] -macro_rules! get_sqlite_query { - ($name:literal) => { - SQLITE_QUERIES - .get($name) - .ok_or_else(|| findex_server_error!("{} SQL query can't be found", $name))? - }; - ($name:expr) => { - SQLITE_QUERIES - .get($name) - .ok_or_else(|| findex_server_error!("{} SQL query can't be found", $name))? - }; -} - -#[allow(dead_code)] -pub(crate) struct SqlitePool { - pool: Pool, -} - -impl SqlitePool { - /// Instantiate a new `SQLite` database - /// and create the appropriate table(s) if need be - pub(crate) async fn instantiate(path: &Path, clear_database: bool) -> FResult { - let options = SqliteConnectOptions::new() - .filename(path) - // Sets a timeout value to wait when the database is locked, before returning a busy timeout error. - .busy_timeout(Duration::from_secs(120)) - .create_if_missing(true) - // disable logging of each query - .disable_statement_logging(); - - let pool = SqlitePoolOptions::new() - .max_connections(u32::try_from(num_cpus::get())?) - .connect_with(options) - .await?; - - sqlx::query(get_sqlite_query!("create-table-context")) - .execute(&pool) - .await?; - - sqlx::query(get_sqlite_query!("create-table-objects")) - .execute(&pool) - .await?; - - sqlx::query(get_sqlite_query!("create-table-read_access")) - .execute(&pool) - .await?; - - sqlx::query(get_sqlite_query!("create-table-tags")) - .execute(&pool) - .await?; - - if clear_database { - clear_database_(&pool).await?; - } - - let sqlite_pool = Self { pool }; - Ok(sqlite_pool) - } -} - -#[async_trait(?Send)] -impl Database for SqlitePool { - async fn create(&self) -> FResult<()> { - Ok(()) - } -} - -pub(crate) async fn clear_database_<'e, E>(executor: E) -> FResult<()> -where - E: Executor<'e, Database = Sqlite> + Copy, -{ - // Erase `context` table - sqlx::query(get_sqlite_query!("clean-table-context")) - .execute(executor) - .await?; - // Erase `objects` table - sqlx::query(get_sqlite_query!("clean-table-objects")) - .execute(executor) - .await?; - // Erase `read_access` table - sqlx::query(get_sqlite_query!("clean-table-read_access")) - .execute(executor) - .await?; - // Erase `tags` table - sqlx::query(get_sqlite_query!("clean-table-tags")) - .execute(executor) - .await?; - Ok(()) -} diff --git a/crate/server/src/error/mod.rs b/crate/server/src/error/mod.rs new file mode 100644 index 0000000..d85381a --- /dev/null +++ b/crate/server/src/error/mod.rs @@ -0,0 +1,2 @@ +pub mod result; +pub mod server; diff --git a/crate/server/src/result.rs b/crate/server/src/error/result.rs similarity index 89% rename from crate/server/src/result.rs rename to crate/server/src/error/result.rs index 82afdb4..827ce2b 100644 --- a/crate/server/src/result.rs +++ b/crate/server/src/error/result.rs @@ -1,21 +1,24 @@ -use crate::error::FindexServerError; +use crate::error::server::FindexServerError; pub type FResult = Result; -/// A helper trait for `FResult` that provides additional methods for error handling. +/// A helper trait for `FResult` that provides additional methods for error +/// handling. pub trait FResultHelper { /// Sets the context for the error. /// /// # Errors /// - /// Returns a `FResult` with the specified context if the original result is an error. + /// Returns a `FResult` with the specified context if the original result is + /// an error. fn context(self, context: &str) -> FResult; /// Sets the context for the error using a closure. /// /// # Errors /// - /// Returns a `FResult` with the context returned by the closure if the original result is an error. + /// Returns a `FResult` with the context returned by the closure if the + /// original result is an error. fn with_context(self, op: O) -> FResult where O: FnOnce() -> String; diff --git a/crate/server/src/error.rs b/crate/server/src/error/server.rs similarity index 88% rename from crate/server/src/error.rs rename to crate/server/src/error/server.rs index c32d781..a772f13 100644 --- a/crate/server/src/error.rs +++ b/crate/server/src/error/server.rs @@ -1,8 +1,10 @@ use std::{array::TryFromSliceError, sync::mpsc::SendError}; use actix_web::{dev::ServerHandle, error::QueryPayloadError}; -use cloudproof::reexport::crypto_core::CryptoCoreError; -use cloudproof_findex::implementations::redis::FindexRedisError; +use cloudproof_findex::{ + db_interfaces::DbInterfaceError, reexport::cosmian_findex::CoreError, + ser_de::SerializationError, +}; use redis::ErrorKind; use thiserror::Error; use x509_parser::prelude::{PEMError, X509Error}; @@ -31,7 +33,7 @@ pub enum FindexServerError { ClientConnectionError(String), // Any actions of the user which is not allowed - #[error("Access denied: {0}")] + #[error("Permission denied: {0}")] Unauthorized(String), // A failure originating from one of the cryptographic algorithms @@ -50,6 +52,9 @@ pub enum FindexServerError { #[error("Invalid URL: {0}")] UrlError(String), + + #[error("Serialization: {0}")] + Deserialization(String), } impl From> for FindexServerError { @@ -76,18 +81,6 @@ impl From> for FindexServerError { } } -impl From for FindexServerError { - fn from(e: CryptoCoreError) -> Self { - Self::CryptographicError(e.to_string()) - } -} - -impl From for FindexServerError { - fn from(e: FindexRedisError) -> Self { - Self::Findex(e.to_string()) - } -} - impl From for FindexServerError { fn from(e: std::string::FromUtf8Error) -> Self { Self::ConversionError(e.to_string()) @@ -106,6 +99,8 @@ impl From for FindexServerError { } } +// todo(manu): remove useless convert + impl From for FindexServerError { fn from(e: std::io::Error) -> Self { Self::ServerError(e.to_string()) @@ -124,12 +119,6 @@ impl From for FindexServerError { } } -impl From for FindexServerError { - fn from(e: cloudproof::reexport::cover_crypt::Error) -> Self { - Self::InvalidRequest(e.to_string()) - } -} - impl From for FindexServerError { fn from(e: QueryPayloadError) -> Self { Self::InvalidRequest(e.to_string()) @@ -176,6 +165,12 @@ impl From for FindexServerError { } } +impl From for FindexServerError { + fn from(e: SerializationError) -> Self { + Self::Findex(e.to_string()) + } +} + impl From for FindexServerError { fn from(e: base64::DecodeError) -> Self { Self::ConversionError(e.to_string()) @@ -188,6 +183,18 @@ impl From for FindexServerError { } } +impl From for FindexServerError { + fn from(e: DbInterfaceError) -> Self { + Self::DatabaseError(e.to_string()) + } +} + +impl From for FindexServerError { + fn from(e: CoreError) -> Self { + Self::Findex(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)); }`. @@ -214,13 +221,13 @@ macro_rules! findex_server_ensure { #[macro_export] macro_rules! findex_server_error { ($msg:literal) => { - $crate::error::FindexServerError::ServerError(::core::format_args!($msg).to_string()) + $crate::error::server::FindexServerError::ServerError(::core::format_args!($msg).to_string()) }; ($err:expr $(,)?) => ({ - $crate::error::FindexServerError::ServerError($err.to_string()) + $crate::error::server::FindexServerError::ServerError($err.to_string()) }); ($fmt:expr, $($arg:tt)*) => { - $crate::error::FindexServerError::ServerError(::core::format_args!($fmt, $($arg)*).to_string()) + $crate::error::server::FindexServerError::ServerError(::core::format_args!($fmt, $($arg)*).to_string()) }; } diff --git a/crate/server/src/findex_server.rs b/crate/server/src/findex_server.rs index c082dad..371e893 100644 --- a/crate/server/src/findex_server.rs +++ b/crate/server/src/findex_server.rs @@ -17,11 +17,13 @@ use tracing::info; use crate::{ config::{self, JwtAuthConfig, ServerParams}, core::FindexServer, + error::result::FResult, findex_server_bail, middlewares::{extract_peer_certificate, AuthTransformer, JwksManager, JwtConfig, SslAuth}, - result::FResult, - routes::get_version, - FServer, + routes::{ + create_index_id, delete_chains, delete_entries, dump_tokens, fetch_chains, fetch_entries, + get_version, grant_permission, insert_chains, revoke_permission, upsert_entries, + }, }; /// Starts the Findex server based on the provided configuration. @@ -30,16 +32,20 @@ use crate::{ /// 1. Plain HTTP, /// 2. HTTPS with PKCS#12, /// -/// The method used depends on the server settings specified in the `ServerParams` instance provided. +/// The method used depends on the server settings specified in the +/// `ServerParams` instance provided. /// /// # Arguments /// -/// * `server_params` - An instance of `ServerParams` that contains the settings for the server. -/// * `server_handle_transmitter` - An optional sender channel of type `mpsc::Sender` that can be used to manage server state. +/// * `server_params` - An instance of `ServerParams` that contains the settings +/// for the server. +/// * `server_handle_transmitter` - An optional sender channel of type +/// `mpsc::Sender` that can be used to manage server state. /// /// # Errors /// -/// This function will return an error if any of the server starting methods fails. +/// This function will return an error if any of the server starting methods +/// fails. pub async fn start_findex_server( server_params: ServerParams, findex_server_handle_tx: Option>, @@ -58,12 +64,15 @@ pub async fn start_findex_server( /// Start a plain HTTP Findex server /// -/// This function will instantiate and prepare the Findex server and run it on a plain HTTP connection +/// This function will instantiate and prepare the Findex server and run it on a +/// plain HTTP connection /// /// # Arguments /// -/// * `server_params` - An instance of `ServerParams` that contains the settings for the server. -/// * `server_handle_transmitter` - An optional sender channel of type `mpsc::Sender` that can be used to manage server state. +/// * `server_params` - An instance of `ServerParams` that contains the settings +/// for the server. +/// * `server_handle_transmitter` - An optional sender channel of type +/// `mpsc::Sender` that can be used to manage server state. /// /// # Errors /// @@ -75,7 +84,7 @@ async fn start_plain_http_findex_server( server_handle_transmitter: Option>, ) -> FResult<()> { // Instantiate and prepare the Findex server - let findex_server = Arc::new(FServer::instantiate(server_params).await?); + let findex_server = Arc::new(FindexServer::instantiate(server_params).await?); // Prepare the server let server = prepare_findex_server(findex_server, None).await?; @@ -94,8 +103,10 @@ async fn start_plain_http_findex_server( /// /// # Arguments /// -/// * `server_params` - An instance of `ServerParams` that contains the settings for the server. -/// * `server_handle_transmitter` - An optional sender channel of type `mpsc::Sender` that can be used to manage server state. +/// * `server_params` - An instance of `ServerParams` that contains the settings +/// for the server. +/// * `server_handle_transmitter` - An optional sender channel of type +/// `mpsc::Sender` that can be used to manage server state. /// /// # Errors /// @@ -103,7 +114,8 @@ async fn start_plain_http_findex_server( /// - The path to the PKCS#12 certificate file is not provided in the config /// - The file cannot be opened or read /// - The file is not a valid PKCS#12 format or the password is incorrect -/// - The SSL acceptor cannot be created or configured with the certificate and key +/// - The SSL acceptor cannot be created or configured with the certificate and +/// key /// - The Findex server cannot be instantiated or prepared /// - The server fails to run async fn start_https_findex_server( @@ -137,7 +149,7 @@ async fn start_https_findex_server( } // Instantiate and prepare the Findex server - let findex_server = Arc::new(FServer::instantiate(server_params).await?); + let findex_server = Arc::new(FindexServer::instantiate(server_params).await?); let server = prepare_findex_server(findex_server, Some(builder)).await?; // send the server handle to the caller @@ -152,30 +164,35 @@ async fn start_https_findex_server( } /** - * This function prepares a server for the application. It creates an `HttpServer` instance, - * configures the routes for the application, and sets the request timeout. The server can be - * configured to use OpenSSL for SSL encryption by providing an `SslAcceptorBuilder`. + * This function prepares a server for the application. It creates an + * `HttpServer` instance, configures the routes for the application, and + * sets the request timeout. The server can be configured to use OpenSSL for + * SSL encryption by providing an `SslAcceptorBuilder`. * * # Arguments * - * * `findex_server`: A shared reference to the `Findex server` instance to be used by the application. - * * `builder`: An optional `SslAcceptorBuilder` to configure the SSL encryption for the server. + * * `findex_server`: A shared reference to the `Findex server` instance to + * be used by the application. + * * `builder`: An optional `SslAcceptorBuilder` to configure the SSL + * encryption for the server. * * # Returns * - * Returns a `Result` type that contains a `Server` instance if successful, or an error if - * something went wrong. + * Returns a `Result` type that contains a `Server` instance if successful, + * or an error if something went wrong. * * # Errors * * This function can return the following errors: - * - `FindexServerError::ServerError` - If there is an error in the server configuration or preparation. + * - `FindexServerError::ServerError` - If there is an error in the server + * configuration or preparation. */ pub(crate) async fn prepare_findex_server( findex_server: Arc, builder: Option, ) -> FResult { - // Prepare the JWT configurations and the JWKS manager if the server is using JWT for authentication. + // Prepare the JWT configurations and the JWKS manager if the server is using + // JWT for authentication. let (jwt_configurations, _jwks_manager) = if let Some(identity_provider_configurations) = &findex_server.params.identity_provider_configurations { @@ -230,6 +247,19 @@ pub(crate) async fn prepare_findex_server( // CORS middleware is the last one so that the auth middlewares do not run on // 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) + // Permissions management endpoints + .service(create_index_id) + .service(grant_permission) + .service(revoke_permission) + // Version endpoint .service(get_version); app.service(default_scope) diff --git a/crate/server/src/lib.rs b/crate/server/src/lib.rs index 666073f..209b850 100644 --- a/crate/server/src/lib.rs +++ b/crate/server/src/lib.rs @@ -40,6 +40,7 @@ clippy::unseparated_literal_suffix, clippy::map_err_ignore, clippy::redundant_clone, + clippy::todo )] #![allow( clippy::module_name_repetitions, @@ -57,11 +58,7 @@ pub mod database; pub mod error; pub mod findex_server; pub mod middlewares; -pub mod result; pub mod routes; -pub mod secret; - -pub(crate) use database::FServer; #[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used, unsafe_code)] #[cfg(test)] diff --git a/crate/server/src/main.rs b/crate/server/src/main.rs index 8caa930..e2960b6 100644 --- a/crate/server/src/main.rs +++ b/crate/server/src/main.rs @@ -3,10 +3,9 @@ use std::path::PathBuf; use clap::Parser; use cosmian_findex_server::{ config::{ClapConfig, ServerParams}, - error::FindexServerError, + error::{result::FResult, server::FindexServerError}, findex_server::start_findex_server, findex_server_bail, - result::FResult, }; use cosmian_logger::log_utils::log_init; use dotenvy::dotenv; @@ -16,8 +15,8 @@ const FINDEX_SERVER_CONF: &str = "/etc/cosmian_findex_server/server.toml"; /// The main entrypoint of the program. /// -/// This function sets up the necessary environment variables and logging options, -/// then parses the command line arguments using [`ClapConfig::parse()`](https://docs.rs/clap/latest/clap/struct.ClapConfig.html#method.parse). +/// This function sets up the necessary environment variables and logging +/// options, then parses the command line arguments using [`ClapConfig::parse()`](https://docs.rs/clap/latest/clap/struct.ClapConfig.html#method.parse). #[tokio::main] #[allow(clippy::needless_return)] async fn main() -> FResult<()> { @@ -46,8 +45,8 @@ async fn main() -> FResult<()> { let conf_path = PathBuf::from(conf_path); if !conf_path.exists() { findex_server_bail!(FindexServerError::ServerError(format!( - "Cannot read findex server config at specified path: {conf_path:?} - file does not \ - exist" + "Cannot read findex server config at specified path: {conf_path:?} - file does \ + not exist" ))); } conf_path @@ -77,7 +76,8 @@ async fn main() -> FResult<()> { ClapConfig::parse() }; - // Instantiate a config object using the env variables and the args of the binary + // Instantiate a config object using the env variables and the args of the + // binary debug!("Command line config: {clap_config:#?}"); // Parse the Server Config from the command line arguments diff --git a/crate/server/src/middlewares/jwks.rs b/crate/server/src/middlewares/jwks.rs index d462abe..1b2a9e5 100644 --- a/crate/server/src/middlewares/jwks.rs +++ b/crate/server/src/middlewares/jwks.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::RwLock}; use alcoholic_jwt::{JWK, JWKS}; use chrono::{DateTime, Duration, Utc}; -use crate::{error::FindexServerError, result::FResult}; +use crate::error::{result::FResult, server::FindexServerError}; static REFRESH_INTERVAL: i64 = 60; // in secs diff --git a/crate/server/src/middlewares/jwt.rs b/crate/server/src/middlewares/jwt.rs index 39f076f..7b059e2 100644 --- a/crate/server/src/middlewares/jwt.rs +++ b/crate/server/src/middlewares/jwt.rs @@ -5,7 +5,10 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, trace}; use super::JwksManager; -use crate::{error::FindexServerError, findex_server_ensure, result::FResult}; +use crate::{ + error::{result::FResult, server::FindexServerError}, + findex_server_ensure, +}; #[derive(Debug, Deserialize, Serialize)] pub(crate) struct UserClaim { @@ -37,19 +40,6 @@ pub(crate) struct UserClaim { pub google_email: Option, } -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct JwtTokenHeaders { - typ: Option, - cty: Option, - alg: Option, - kid: Option, - x5t: Option, - x5u: Option, - x5c: Option>, - crit: Option, -} - #[derive(Debug)] pub(crate) struct JwtConfig { pub jwt_issuer_uri: String, diff --git a/crate/server/src/middlewares/jwt_token_auth.rs b/crate/server/src/middlewares/jwt_token_auth.rs index 2c66baa..c287189 100644 --- a/crate/server/src/middlewares/jwt_token_auth.rs +++ b/crate/server/src/middlewares/jwt_token_auth.rs @@ -11,7 +11,10 @@ use actix_web::{ use tracing::{debug, error, trace}; use super::UserClaim; -use crate::{error::FindexServerError, middlewares::jwt::JwtConfig, result::FResult}; +use crate::{ + error::{result::FResult, server::FindexServerError}, + middlewares::jwt::JwtConfig, +}; pub(crate) async fn manage_jwt_request( service: Rc, @@ -73,7 +76,8 @@ pub(crate) async fn manage_jwt( trace!("Checking JWT identity: {identity}"); let mut private_claim = extract_user_claim(&configs, &identity); - // If no configuration could get the claim, try refreshing them and extract user claim again + // If no configuration could get the claim, try refreshing them and extract user + // claim again if private_claim.is_err() { configs .first() diff --git a/crate/server/src/middlewares/ssl_auth.rs b/crate/server/src/middlewares/ssl_auth.rs index ba9847a..6a3fc45 100644 --- a/crate/server/src/middlewares/ssl_auth.rs +++ b/crate/server/src/middlewares/ssl_auth.rs @@ -19,7 +19,7 @@ use futures::{ use openssl::{nid::Nid, x509::X509}; use tracing::{debug, error, trace}; -use crate::{findex_server_bail, result::FResult}; +use crate::{error::result::FResult, findex_server_bail}; // see this https://github.com/actix/actix-web/pull/1754#issuecomment-716192605 // for inspiration @@ -35,8 +35,9 @@ pub(crate) struct PeerCertificate { /// Extract the peer certificate from the TLS stream and pass it to middleware. /// -/// This function extracts the peer certificate from the TLS stream and passes it to the middleware. -/// The middleware can then use the peer certificate to authenticate the client. +/// This function extracts the peer certificate from the TLS stream and passes +/// it to the middleware. The middleware can then use the peer certificate to +/// authenticate the client. pub(crate) fn extract_peer_certificate(cnx: &dyn Any, extensions: &mut Extensions) { // Check if the connection is a TLS connection. if let Some(cnx) = cnx.downcast_ref::>() { @@ -57,10 +58,12 @@ pub(crate) struct PeerCommonName { pub(crate) common_name: String, } -/// The middleware that checks the peer certificate and extracts the common name. +/// The middleware that checks the peer certificate and extracts the common +/// name. /// -/// This middleware checks the peer certificate for a common name and extracts it. -/// The common name is then added to the request context so that it can be used by other middleware or handlers. +/// This middleware checks the peer certificate for a common name and extracts +/// it. The common name is then added to the request context so that it can be +/// used by other middleware or handlers. pub(crate) struct SslAuth; impl Transform for SslAuth @@ -104,9 +107,11 @@ where /// Call the `SslAuthMiddleware`. /// - /// This function calls the underlying service and checks the peer certificate for a common name. - /// If the common name is found, it is added to the request context so that it can be used by other middleware or handlers. - /// If the common name is not found, an unauthorized response is returned. + /// This function calls the underlying service and checks the peer + /// certificate for a common name. If the common name is found, it is + /// added to the request context so that it can be used by other middleware + /// or handlers. If the common name is not found, an unauthorized + /// response is returned. #[allow(clippy::cognitive_complexity)] fn call(&self, req: ServiceRequest) -> Self::Future { // Log that the middleware is being called. diff --git a/crate/server/src/routes/error.rs b/crate/server/src/routes/error.rs new file mode 100644 index 0000000..3ee12a8 --- /dev/null +++ b/crate/server/src/routes/error.rs @@ -0,0 +1,47 @@ +use actix_web::{ + http::{header, StatusCode}, + web::Json, + HttpResponse, HttpResponseBuilder, +}; +use tracing::{error, warn}; + +use crate::error::server::FindexServerError; + +pub(crate) type Response = Result, FindexServerError>; +pub(crate) type ResponseBytes = Result; + +impl actix_web::error::ResponseError for FindexServerError { + fn status_code(&self) -> StatusCode { + match self { + Self::Unauthorized(_) => StatusCode::UNAUTHORIZED, + + Self::DatabaseError(_) + | Self::ConversionError(_) + | Self::CryptographicError(_) + | Self::Redis(_) + | Self::Findex(_) + | Self::Certificate(_) + | Self::Deserialization(_) + | Self::ServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, + + Self::InvalidRequest(_) | Self::ClientConnectionError(_) | Self::UrlError(_) => { + StatusCode::UNPROCESSABLE_ENTITY + } + } + } + + fn error_response(&self) -> HttpResponse { + let status_code = self.status_code(); + let message = self.to_string(); + + if status_code >= StatusCode::INTERNAL_SERVER_ERROR { + error!("{status_code} - {message}"); + } else { + warn!("{status_code} - {message}"); + } + + HttpResponseBuilder::new(status_code) + .insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8")) + .body(message) + } +} diff --git a/crate/server/src/routes/findex.rs b/crate/server/src/routes/findex.rs new file mode 100644 index 0000000..6d91db9 --- /dev/null +++ b/crate/server/src/routes/findex.rs @@ -0,0 +1,236 @@ +use std::sync::Arc; + +use actix_web::{ + post, + web::{self, Bytes, Data, Json}, + HttpRequest, HttpResponse, +}; +use cloudproof_findex::{ + db_interfaces::{redis::FindexTable, rest::UpsertData}, + reexport::{ + cosmian_crypto_core::bytes_ser_de::Serializable, cosmian_findex::TokenToEncryptedValueMap, + }, + ser_de::ffi_ser_de::deserialize_token_set, +}; +use tracing::{debug, info, trace}; + +use crate::{ + core::{FindexServer, Permission}, + error::{result::FResult, server::FindexServerError}, + routes::{ + 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( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> ResponseBytes { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/fetch_entries"); + + check_permission(&user, &index_id, Permission::Read, &findex_server).await?; + + let tokens = deserialize_token_set(&bytes.into_iter().collect::>())?; + trace!("fetch_entries: number of tokens: {}:", tokens.len()); + + // 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) + .await?; + trace!( + "fetch_entries: number of uids_and_values: {}:", + uids_and_values.len() + ); + + let bytes = uids_and_values.serialize()?.to_vec(); + trace!("fetch_entries: number of bytes: {}:", bytes.len()); + + Ok(HttpResponse::Ok() + .content_type("application/octet-stream") + .body(bytes)) +} + +#[post("/indexes/{index_id}/fetch_chains")] +pub(crate) async fn fetch_chains( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> ResponseBytes { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/fetch_chains"); + + check_permission(&user, &index_id, Permission::Read, &findex_server).await?; + + let tokens = deserialize_token_set(&bytes.into_iter().collect::>())?; + trace!("fetch_chains: number of tokens: {}:", tokens.len()); + + let uids_and_values = findex_server + .db + .fetch_chains(&get_index_id(index_id.as_str())?, tokens) + .await?; + trace!( + "fetch_chains: number of uids_and_values: {}:", + uids_and_values.len() + ); + + let bytes = uids_and_values.serialize()?.to_vec(); + + Ok(HttpResponse::Ok() + .content_type("application/octet-stream") + .body(bytes)) +} + +#[post("/indexes/{index_id}/upsert_entries")] +pub(crate) async fn upsert_entries( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> ResponseBytes { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/upsert_entries",); + + check_permission(&user, &index_id, Permission::Write, &findex_server).await?; + + let upsert_data = UpsertData::deserialize(&bytes.into_iter().collect::>())?; + + trace!("upsert_entries: num upsert data: {}", upsert_data.len()); + + let rejected = findex_server + .db + .upsert_entries(&get_index_id(index_id.as_str())?, upsert_data) + .await?; + + let bytes = rejected.serialize()?.to_vec(); + Ok(HttpResponse::Ok() + .content_type("application/octet-stream") + .body(bytes)) +} + +#[post("/indexes/{index_id}/insert_chains")] +pub(crate) async fn insert_chains( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> Response<()> { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/insert_chains",); + + check_permission(&user, &index_id, Permission::Write, &findex_server).await?; + + let token_to_value_encrypted_value_map = + TokenToEncryptedValueMap::deserialize(&bytes.into_iter().collect::>())?; + + findex_server + .db + .insert_chains( + &get_index_id(index_id.as_str())?, + token_to_value_encrypted_value_map, + ) + .await?; + + Ok(Json(())) +} + +#[post("/indexes/{index_id}/delete_entries")] +pub(crate) async fn delete_entries( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> Response<()> { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/delete_entries",); + + check_permission(&user, &index_id, Permission::Write, &findex_server).await?; + + let tokens = deserialize_token_set(&bytes.into_iter().collect::>())?; + trace!("delete_entries: number of tokens: {}:", tokens.len()); + + findex_server + .db + .delete( + &get_index_id(index_id.as_str())?, + FindexTable::Entry, + tokens, + ) + .await?; + + Ok(Json(())) +} + +#[post("/indexes/{index_id}/delete_chains")] +pub(crate) async fn delete_chains( + req: HttpRequest, + index_id: web::Path, + bytes: Bytes, + findex_server: Data>, +) -> Response<()> { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/delete_chains",); + + check_permission(&user, &index_id, Permission::Write, &findex_server).await?; + + let tokens = deserialize_token_set(&bytes.into_iter().collect::>())?; + trace!("delete_chains: number of tokens: {}:", tokens.len()); + + findex_server + .db + .delete( + &get_index_id(index_id.as_str())?, + FindexTable::Chain, + tokens, + ) + .await?; + + Ok(Json(())) +} + +#[post("/indexes/{index_id}/dump_tokens")] +pub(crate) async fn dump_tokens( + req: HttpRequest, + index_id: web::Path, + findex_server: Data>, +) -> ResponseBytes { + let user = findex_server.get_user(&req); + info!("user {user}: POST /indexes/{index_id}/dump_tokens"); + + check_permission(&user, &index_id, Permission::Read, &findex_server).await?; + + let tokens = findex_server + .db + .dump_tokens(&get_index_id(index_id.as_str())?) + .await?; + trace!("dump_tokens: number of tokens: {}:", tokens.len()); + + let bytes = tokens.serialize()?.to_vec(); + + Ok(HttpResponse::Ok() + .content_type("application/octet-stream") + .body(bytes)) +} +// todo(manu): put findex parameters in cli conf diff --git a/crate/server/src/routes/mod.rs b/crate/server/src/routes/mod.rs index 6c432da..7a648e6 100644 --- a/crate/server/src/routes/mod.rs +++ b/crate/server/src/routes/mod.rs @@ -1,62 +1,13 @@ -use std::sync::Arc; - -use actix_web::{ - get, - http::{header, StatusCode}, - web::{Data, Json}, - HttpRequest, HttpResponse, HttpResponseBuilder, +mod error; +mod findex; +mod permissions; +mod utils; +mod version; + +pub(crate) use findex::{ + delete_chains, delete_entries, dump_tokens, fetch_chains, fetch_entries, insert_chains, + upsert_entries, }; -use clap::crate_version; -use openssl::version; -use tracing::{error, info, warn}; - -use crate::{database::FServer, error::FindexServerError, result::FResult}; - -impl actix_web::error::ResponseError for FindexServerError { - fn status_code(&self) -> StatusCode { - match self { - Self::Unauthorized(_) => StatusCode::UNAUTHORIZED, - - Self::DatabaseError(_) - | Self::ConversionError(_) - | Self::CryptographicError(_) - | Self::Redis(_) - | Self::Findex(_) - | Self::Certificate(_) - | Self::ServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, - - Self::InvalidRequest(_) | Self::ClientConnectionError(_) | Self::UrlError(_) => { - StatusCode::UNPROCESSABLE_ENTITY - } - } - } - - fn error_response(&self) -> HttpResponse { - let status_code = self.status_code(); - let message = self.to_string(); - - if status_code >= StatusCode::INTERNAL_SERVER_ERROR { - error!("{status_code} - {message}"); - } else { - warn!("{status_code} - {message}"); - } - - HttpResponseBuilder::new(status_code) - .insert_header((header::CONTENT_TYPE, "text/html; charset=utf-8")) - .body(message) - } -} - -/// Get the Findex server version -#[get("/version")] -pub(crate) async fn get_version( - req: HttpRequest, - findex_server: Data>, -) -> FResult> { - info!("GET /version {}", findex_server.get_user(&req)); - Ok(Json(format!( - "{} ({})", - crate_version!().to_owned(), - version::version() - ))) -} +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 new file mode 100644 index 0000000..afe983a --- /dev/null +++ b/crate/server/src/routes/permissions.rs @@ -0,0 +1,101 @@ +use std::{str::FromStr, sync::Arc}; + +use actix_web::{ + post, + web::{self, Data, Json}, + HttpRequest, +}; +use serde::{Deserialize, Serialize}; +use tracing::info; + +use crate::{ + core::{FindexServer, Permission}, + error::{result::FResult, server::FindexServerError}, + routes::get_index_id, +}; + +#[derive(Deserialize, Serialize, Debug)] // Debug is required by ok_json() +struct SuccessResponse { + pub success: String, +} + +#[post("/create/index")] +pub(crate) async fn create_index_id( + req: HttpRequest, + findex_server: Data>, +) -> FResult> { + let user = findex_server.get_user(&req); + info!("user {user}: POST /permission/create"); + + // Check if the user has the right to grant permission: only admins can do that + let index_id = findex_server.db.create_index_id(&user).await?; + + Ok(Json(SuccessResponse { + success: format!("[{user}] New admin permission successfully created on index: {index_id}"), + })) +} + +#[post("/permission/grant/{user_id}/{permission}/{index_id}")] +pub(crate) async fn grant_permission( + req: HttpRequest, + params: web::Path<(String, String, String)>, + findex_server: Data>, +) -> FResult> { + let user = findex_server.get_user(&req); + let (user_id, permission, index_id) = params.into_inner(); + info!("user {user}: POST /permission/grant/{user_id}/{permission}/{index_id}"); + + // Check if the user has the right to grant permission: only admins can do that + let user_permission = findex_server.get_permission(&user, &index_id).await?; + if Permission::Admin != user_permission { + return Err(FindexServerError::Unauthorized(format!( + "Delegating permission to an index requires an admin permission. User {user} with \ + permission {user_permission} does not allow granting permission to index {index_id} \ + with permission {permission}", + ))); + } + + findex_server + .db + .grant_permission( + &user_id, + Permission::from_str(permission.as_str())?, + &get_index_id(&index_id)?, + ) + .await?; + + Ok(Json(SuccessResponse { + success: format!( + "[{user_id}] permission {permission} on index {index_id} successfully added" + ), + })) +} + +#[post("/permission/revoke/{user_id}/{index_id}")] +pub(crate) async fn revoke_permission( + req: HttpRequest, + params: web::Path<(String, String)>, + findex_server: Data>, +) -> FResult> { + let user = findex_server.get_user(&req); + let (user_id, index_id) = params.into_inner(); + info!("user {user}: POST /permission/revoke/{user_id}/{index_id}"); + + // Check if the user has the right to revoke permission: only admins can do that + let user_permission = findex_server.get_permission(&user, &index_id).await?; + if Permission::Admin != user_permission { + return Err(FindexServerError::Unauthorized(format!( + "Revoking permission to an index requires an admin permission. User {user} with \ + permission {user_permission} does not allow revoking permission to index {index_id}", + ))); + } + + findex_server + .db + .revoke_permission(&user_id, &get_index_id(&index_id)?) + .await?; + + Ok(Json(SuccessResponse { + success: format!("Permission for {user_id} on index {index_id} successfully added"), + })) +} diff --git a/crate/server/src/routes/utils.rs b/crate/server/src/routes/utils.rs new file mode 100644 index 0000000..1067c35 --- /dev/null +++ b/crate/server/src/routes/utils.rs @@ -0,0 +1,9 @@ +use uuid::Uuid; + +use crate::error::{result::FResult, server::FindexServerError}; + +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}")) + }) +} diff --git a/crate/server/src/routes/version.rs b/crate/server/src/routes/version.rs new file mode 100644 index 0000000..afcd20a --- /dev/null +++ b/crate/server/src/routes/version.rs @@ -0,0 +1,26 @@ +use std::sync::Arc; + +use actix_web::{ + get, + web::{Data, Json}, + HttpRequest, +}; +use clap::crate_version; +use openssl::version; +use tracing::info; + +use crate::{core::FindexServer, error::result::FResult}; + +/// Get the Findex server version +#[get("/version")] +pub(crate) async fn get_version( + req: HttpRequest, + findex_server: Data>, +) -> FResult> { + info!("GET /version {}", findex_server.get_user(&req)); + Ok(Json(format!( + "{} ({})", + crate_version!().to_owned(), + version::version() + ))) +} diff --git a/crate/server/src/secret.rs b/crate/server/src/secret.rs deleted file mode 100644 index 88626d2..0000000 --- a/crate/server/src/secret.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, -}; - -use num_bigint_dig::BigUint; -use openssl::rand::rand_bytes; -use serde::Deserialize; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -use crate::result::FResult; - -/// Guarantees to be zeroized on drop with -/// feature `zeroize` enabled from `num_bigint_dig` crate. -#[derive(Debug, Eq, PartialEq, Clone, Deserialize)] -pub struct SafeBigUint(BigUint); - -impl SafeBigUint { - /// Creates a new `SafeBigUint` from raw bytes encoded in big endian. - #[must_use] - pub fn from_bytes_be(bytes: &[u8]) -> Self { - Self(BigUint::from_bytes_be(bytes)) - } -} - -impl Drop for SafeBigUint { - fn drop(&mut self) { - self.0.zeroize(); - } -} - -impl From for SafeBigUint { - fn from(value: BigUint) -> Self { - Self(value) - } -} - -impl Deref for SafeBigUint { - type Target = BigUint; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Holds a secret information of `LENGTH` bytes. -/// -/// This secret is stored on the heap and is guaranteed to be zeroized on drop. -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct Secret(Pin>); - -impl Secret { - /// Creates a new secret and returns it. - /// - /// All bytes are initially set to 0. - #[must_use] - #[allow(unsafe_code)] - pub fn new() -> Self { - // heap-allocate and turn into `Box` but looses `LENGTH`-constraint in type - let data = vec![0_u8; LENGTH].into_boxed_slice(); - // cast the raw pointer back to our fixed-length type - // it is considered safe because `data` is initialized in the previous line - let data = unsafe { Box::from_raw(Box::into_raw(data).cast::<[u8; LENGTH]>()) }; - Self(Pin::new(data)) - } - - /// Creates a new random secret. - /// # Errors - /// If the random bytes could not be generated. - pub fn new_random() -> FResult { - let mut secret = Self::new(); - rand_bytes(&mut secret)?; - Ok(secret) - } - - /// Returns the bytes of the secret. - /// - /// # Safety - /// - /// Once returned the secret bytes are *not* protected. It is the caller's - /// responsibility to guarantee they are not leaked in the memory. - pub fn to_unprotected_bytes(&self, dest: &mut [u8; LENGTH]) { - dest.copy_from_slice(self); - } - - /// Creates a secret from the given unprotected bytes, and zeroize the - /// source bytes. - /// - /// Do not take ownership of the bytes to avoid stack copying. - pub fn from_unprotected_bytes(bytes: &mut [u8; LENGTH]) -> Self { - let mut secret = Self::new(); - secret.copy_from_slice(bytes.as_slice()); - bytes.zeroize(); - secret - } -} - -impl Default for Secret { - fn default() -> Self { - Self::new() - } -} - -impl Deref for Secret { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &*self.0 - } -} - -impl DerefMut for Secret { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut *self.0 - } -} - -impl Zeroize for Secret { - fn zeroize(&mut self) { - self.0.deref_mut().zeroize(); - } -} - -impl Drop for Secret { - fn drop(&mut self) { - self.zeroize(); - } -} - -impl ZeroizeOnDrop for Secret {} diff --git a/crate/server/src/tests/mod.rs b/crate/server/src/tests/mod.rs index c052ead..0f5eade 100644 --- a/crate/server/src/tests/mod.rs +++ b/crate/server/src/tests/mod.rs @@ -1,16 +1,14 @@ use std::path::PathBuf; -use crate::config::{ClapConfig, DBConfig, HttpConfig, JwtAuthConfig, WorkspaceConfig}; +use crate::config::{ClapConfig, DBConfig, DatabaseType, HttpConfig, JwtAuthConfig}; #[test] fn test_toml() { let config = ClapConfig { db: DBConfig { - database_type: Some("[redis-findex, ...]".to_owned()), + database_type: Some(DatabaseType::Redis), database_url: Some("[redis urls]".to_owned()), sqlite_path: PathBuf::from("[sqlite path]"), - redis_master_password: Some("[redis master password]".to_owned()), - redis_findex_label: Some("[redis findex label]".to_owned()), clear_database: false, }, http: HttpConfig { @@ -31,10 +29,6 @@ fn test_toml() { "[jwt audience 2]".to_owned(), ]), }, - workspace: WorkspaceConfig { - root_data_path: PathBuf::from("[root data path]"), - tmp_path: PathBuf::from("[tmp path]"), - }, default_username: "[default username]".to_owned(), force_default_username: false, }; @@ -44,11 +38,9 @@ default_username = "[default username]" force_default_username = false [db] -database_type = "[redis-findex, ...]" +database_type = "Redis" database_url = "[redis urls]" sqlite_path = "[sqlite path]" -redis_master_password = "[redis master password]" -redis_findex_label = "[redis findex label]" clear_database = false [http] @@ -63,10 +55,6 @@ jwt_issuer_uri = ["[jwt issuer uri 1]", "[jwt issuer uri 2]"] jwks_uri = ["[jwks uri 1]", "[jwks uri 2]"] jwt_audience = ["[jwt audience 1]", "[jwt audience 2]"] -[workspace] -root_data_path = "[root data path]" -tmp_path = "[tmp path]" - "#; assert_eq!(toml_string.trim(), toml::to_string(&config).unwrap().trim()); diff --git a/crate/test_server/Cargo.toml b/crate/test_server/Cargo.toml index 582f609..0a9124c 100644 --- a/crate/test_server/Cargo.toml +++ b/crate/test_server/Cargo.toml @@ -14,12 +14,11 @@ doctest = false [dependencies] actix-server = { workspace = true } -cosmian_findex_client = { path = "../client" } cosmian_findex_server = { path = "../server", features = [ "insecure", ], default-features = false } cosmian_logger = { path = "../logger" } -tempfile = "3.1" +cosmian_rest_client = { path = "../client" } tokio = { workspace = true, features = ["rt-multi-thread"] } tracing = { workspace = true } diff --git a/crate/test_server/certificates/README.md b/crate/test_server/certificates/README.md index 113934a..e6f3e53 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:9998/objects/owned +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 ``` diff --git a/crate/test_server/certificates/generate_certs.sh b/crate/test_server/certificates/generate_certs.sh index eddebad..d272f49 100755 --- a/crate/test_server/certificates/generate_certs.sh +++ b/crate/test_server/certificates/generate_certs.sh @@ -35,7 +35,7 @@ openssl x509 -req -days 3650 -in owner.client.acme.com.csr -CA ca.crt -CAkey ca. # Generate a PKCS12 file openssl pkcs12 -export -out owner.client.acme.com.p12 -inkey owner.client.acme.com.key -in owner.client.acme.com.crt -certfile ca.crt -password pass:password - +openssl pkcs12 -legacy -export -out owner.client.acme.com.old.format.p12 -inkey owner.client.acme.com.key -in owner.client.acme.com.crt -certfile ca.crt -password pass:password ## "user" client cert @@ -50,3 +50,4 @@ openssl x509 -req -days 3650 -in user.client.acme.com.csr -CA ca.crt -CAkey ca.k # Generate a PKCS12 file openssl pkcs12 -export -out user.client.acme.com.p12 -inkey user.client.acme.com.key -in user.client.acme.com.crt -certfile ca.crt -password pass:password +openssl pkcs12 -legacy -export -out user.client.acme.com.old.format.p12 -inkey user.client.acme.com.key -in user.client.acme.com.crt -certfile ca.crt -password pass:password diff --git a/crate/test_server/src/lib.rs b/crate/test_server/src/lib.rs index 00e02db..7e2b303 100644 --- a/crate/test_server/src/lib.rs +++ b/crate/test_server/src/lib.rs @@ -1,9 +1,8 @@ -pub use cosmian_findex_server::config::{DBConfig, DEFAULT_SQLITE_PATH}; +pub use cosmian_findex_server::config::{DBConfig, DatabaseType, DEFAULT_SQLITE_PATH}; pub use test_server::{ start_default_test_findex_server, start_default_test_findex_server_with_cert_auth, start_test_server_with_options, AuthenticationOptions, TestsContext, }; -mod test_server; - mod test_jwt; +mod test_server; diff --git a/crate/test_server/src/test_jwt.rs b/crate/test_server/src/test_jwt.rs index a1cd908..5ecb9ac 100644 --- a/crate/test_server/src/test_jwt.rs +++ b/crate/test_server/src/test_jwt.rs @@ -1,8 +1,8 @@ use cosmian_findex_server::config::JwtAuthConfig; // Test auth0 Config -pub(crate) const AUTH0_JWT_ISSUER_URI: &str = "https://kms-cosmian.eu.auth0.com/"; -pub(crate) const AUTH0_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A"; +pub(crate) const AUTH0_JWT_ISSUER_URI: &str = "https://findex-server.eu.auth0.com/"; +pub(crate) const AUTH0_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVPNXRnb25CVWtYdFcyei1TM2VqNiJ9.eyJmaW5kZXhTZXJ2ZXJVc2VyR3JvdXAiOiJjMmY2MzYwM2M4MTdiYmEzMjgxOGJjMTc4N2IyZTgxMzk5YzY1MTIyOWQ2MGUxZmVhZTNjMTg0ZWJlZWIwNTI5Iiwibmlja25hbWUiOiJ0ZWNoIiwibmFtZSI6InRlY2hAY29zbWlhbi5jb20iLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvNTJmYjMxYzhjY2FkMzQ1ODEyM2Q0ZmFkMTQwODU0Y2Y_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZ0ZS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyNC0xMC0yNVQxMToyODowNC4xMzBaIiwiZW1haWwiOiJ0ZWNoQGNvc21pYW4uY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJpc3MiOiJodHRwczovL2ZpbmRleC1zZXJ2ZXIuZXUuYXV0aDAuY29tLyIsImF1ZCI6InRmM0F6OTB0eTJUck5yeEo5M3hpRnBDRkJuVU9sSXhEIiwiaWF0IjoxNzI5ODU1Njg0LCJleHAiOjE3NjEzOTE2ODQsInN1YiI6ImF1dGgwfDY3MWI4MGE4MDMwMGM5NjhhM2UyOTk2ZiIsInNpZCI6IlRrMkpfRDFiRTdQdjhWczhUMWRPbGJjdHBJTnJYQmdVIiwibm9uY2UiOiJXRGd6UW00d1dHcElSaTFIZERVMVRESitMak5OZEZKSmVqUnlTMkZIVTJzeVFuYzJlSEZUY0d0M1J3PT0ifQ.HRlV79HTrxzXp1mGhLgyni6LV4l1wTBieOMoapvr9Yi5BF3tiKKaHaQsPFsxigAiAZ2J1q5AiUooyT-M3NMggwwW5RMhbeyDYnJwp6ucDMOTdT2dXmDvtn0BMzbUx3wmedve_oEjzfO8vRk_RhD9wDxfB1ZAZRytYblJFHxWUdaKTJkH7f3VMkpl9cVjqkw1U3j2FLp9BuZ-q5jkiQLqCFTBvTAJ1jQaZqwcIlXA547sYzkTPg8otv8IS3nief0TTvv-GpvCXxETHoQVWmpO-g8PW9UkeHe3cHZ57A7juVOHgH_9zh8-uzXfZxMZo69gfjs-tVydoDOsmYhWTEgf5w"; pub(crate) fn get_auth0_jwt_config() -> JwtAuthConfig { JwtAuthConfig { diff --git a/crate/test_server/src/test_server.rs b/crate/test_server/src/test_server.rs index 7f24b57..d6a7803 100644 --- a/crate/test_server/src/test_server.rs +++ b/crate/test_server/src/test_server.rs @@ -7,14 +7,15 @@ use std::{ }; use actix_server::ServerHandle; -use cosmian_findex_client::{ - client_bail, client_error, write_json_object_to_file, ClientConf, ClientError, FindexClient, -}; use cosmian_findex_server::{ - config::{ClapConfig, DBConfig, HttpConfig, HttpParams, JwtAuthConfig, ServerParams}, + config::{ + ClapConfig, DBConfig, DatabaseType, HttpConfig, HttpParams, JwtAuthConfig, ServerParams, + }, findex_server::start_findex_server, }; -use tempfile::TempDir; +use cosmian_rest_client::{ + client_bail, client_error, write_json_object_to_file, ClientConf, ClientError, RestClient, +}; use tokio::sync::OnceCell; use tracing::{info, trace}; @@ -28,46 +29,25 @@ 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(); -fn sqlite_db_config() -> DBConfig { - trace!("TESTS: using sqlite"); - let tmp_dir = TempDir::new().unwrap(); - let file_path = tmp_dir.path().join("test_sqlite.db"); - // let file_path = PathBuf::from("test_sqlite.db"); - if file_path.exists() { - std::fs::remove_file(&file_path).unwrap(); - } - DBConfig { - database_type: Some("sqlite".to_string()), - clear_database: true, - sqlite_path: file_path, - ..DBConfig::default() - } -} - -fn redis_findex_db_config() -> DBConfig { - trace!("TESTS: using redis-findex"); +fn redis_db_config() -> DBConfig { let url = if let Ok(var_env) = env::var("REDIS_HOST") { format!("redis://{var_env}:6379") } else { "redis://localhost:6379".to_owned() }; + trace!("TESTS: using redis on {url}"); DBConfig { - database_type: Some("redis-findex".to_string()), + database_type: Some(DatabaseType::Redis), clear_database: true, database_url: Some(url), sqlite_path: Default::default(), - redis_master_password: Some("password".to_string()), - redis_findex_label: Some("label".to_string()), } } fn get_db_config() -> DBConfig { - env::var_os("FINDEX_TEST_DB").map_or_else(sqlite_db_config, |v| { - match v.to_str().unwrap_or("") { - "redis-findex" => redis_findex_db_config(), - "sqlite" => sqlite_db_config(), - _ => sqlite_db_config(), - } + env::var_os("FINDEX_TEST_DB").map_or_else(redis_db_config, |v| match v.to_str().unwrap_or("") { + "redis" => redis_db_config(), + _ => redis_db_config(), }) } @@ -78,7 +58,7 @@ pub async fn start_default_test_findex_server() -> &'static TestsContext { ONCE.get_or_try_init(|| { start_test_server_with_options( get_db_config(), - 6660, + 6666, AuthenticationOptions { use_jwt_token: false, use_https: false, @@ -96,7 +76,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(), - 9991, + 6668, AuthenticationOptions { use_jwt_token: false, use_https: true, @@ -121,7 +101,7 @@ impl TestsContext { self.server_handle.stop(false).await; self.thread_handle .join() - .map_err(|_e| client_error!("failed joining th stop thread"))? + .map_err(|_e| client_error!("failed joining the stop thread"))? } } @@ -192,9 +172,10 @@ fn start_test_findex_server( } /// Wait for the server to start by reading the version -async fn wait_for_server_to_start(findex_client: &FindexClient) -> Result<(), ClientError> { - // 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. +async fn wait_for_server_to_start(findex_client: &RestClient) -> Result<(), ClientError> { + // 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. let mut retry = true; let mut timeout = 5; let mut waiting = 1; @@ -320,7 +301,8 @@ fn generate_owner_conf(server_params: &ServerParams) -> Result<(String, ClientCo None }, - // We use the private key since the private key is the public key with additional information. + // We use the private key since the private key is the public key with additional + // information. ..ClientConf::default() }; // write the conf to a file @@ -330,7 +312,8 @@ fn generate_owner_conf(server_params: &ServerParams) -> Result<(String, ClientCo Ok((owner_client_conf_path, owner_client_conf)) } -/// Generate a user configuration for user.client@acme.com and return the file path +/// 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 { // This create root dir let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -358,18 +341,37 @@ fn generate_user_conf(port: u16, owner_client_conf: &ClientConf) -> Result Result<(), ClientError> { - let context = start_test_server_with_options( - sqlite_db_config(), - 6660, - AuthenticationOptions { - use_jwt_token: false, - use_https: true, - use_client_cert: true, - }, - ) - .await?; - context.stop_server().await +mod test { + use cosmian_rest_client::ClientError; + use tracing::trace; + + use crate::{ + start_test_server_with_options, test_server::redis_db_config, AuthenticationOptions, + }; + + #[tokio::test] + async fn test_server_auth_matrix() -> Result<(), ClientError> { + let test_cases = vec![ + (false, false, false, "all_disabled"), + (true, false, false, "https_no_auth"), + (true, false, true, "https_cert"), + (false, true, false, "https_jwt"), + (true, true, true, "all_enabled"), + ]; + for (use_https, use_jwt_token, use_client_cert, description) in test_cases { + trace!("Running test case: {}", description); + let context = start_test_server_with_options( + redis_db_config(), + 6667, + AuthenticationOptions { + use_https, + use_jwt_token, + use_client_cert, + }, + ) + .await?; + context.stop_server().await?; + } + Ok(()) + } } diff --git a/deny.toml b/deny.toml index 255e30f..23900bb 100644 --- a/deny.toml +++ b/deny.toml @@ -105,7 +105,7 @@ allow = [ "AGPL-3.0", "CC0-1.0", "MPL-2.0", - "OpenSSL" + "OpenSSL", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the @@ -136,8 +136,8 @@ expression = "MIT AND ISC AND OpenSSL" # and the crate will be checked normally, which may produce warnings or errors # depending on the rest of your configuration license-files = [ - # Each entry is a crate relative path, and the (opaque) hash of its contents - { path = "LICENSE", hash = 0xbd0eed23 }, + # Each entry is a crate relative path, and the (opaque) hash of its contents + { path = "LICENSE", hash = 0xbd0eed23 }, ] [licenses.private] diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..a872b04 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +stable-2024-10-17