diff --git a/Cargo.lock b/Cargo.lock index 954ba09fab9751f18ae9bdc0e715deffd3699587..a8e35d800aad1c690fb6dc71a3c0aab31c1cda6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" dependencies = [ "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -213,7 +213,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -285,6 +285,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "argon2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "ascii" version = "0.9.3" @@ -299,7 +310,7 @@ checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -325,12 +336,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[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.2" @@ -542,7 +568,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -754,7 +780,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.99", ] [[package]] @@ -765,7 +791,7 @@ checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -778,7 +804,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.99", ] [[package]] @@ -789,6 +815,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -952,7 +979,7 @@ checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -1215,6 +1242,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + [[package]] name = "indexmap" version = "1.9.1" @@ -1297,10 +1330,13 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" dependencies = [ + "async-trait", "base64", "email-encoding", "email_address", "fastrand", + "futures-io", + "futures-util", "httpdate", "idna", "mime", @@ -1310,19 +1346,22 @@ dependencies = [ "rustls", "rustls-pemfile", "socket2", + "tokio", + "tokio-rustls", "webpki-roots", ] [[package]] name = "libc" -version = "0.2.132" +version = "0.2.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "libpod" version = "0.4.4" dependencies = [ + "argon2", "chacha20poly1305", "clap 4.0.26", "criterion", @@ -1348,6 +1387,7 @@ dependencies = [ "reqwest", "rusqlite", "scheduled-thread-pool", + "secrecy", "serde", "serde_json", "serde_path_to_error", @@ -1358,6 +1398,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "validator", "zeroize", ] @@ -1475,7 +1516,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -1624,7 +1665,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -1682,7 +1723,18 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.36.1", +] + +[[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]] @@ -1714,7 +1766,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -1817,7 +1869,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.99", "version_check", ] @@ -1834,18 +1886,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.43" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -1900,9 +1952,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -1981,7 +2033,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn", + "syn 1.0.99", ] [[package]] @@ -2144,7 +2196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -2172,6 +2224,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + [[package]] name = "security-framework" version = "2.7.0" @@ -2228,7 +2290,7 @@ checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -2282,7 +2344,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -2358,9 +2420,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -2395,6 +2457,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tempfile" version = "3.3.0" @@ -2436,7 +2509,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901a55b0a7a06ebc4a674dcca925170da8e613fa3b163a1df804ed10afb154d" dependencies = [ "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -2447,7 +2520,7 @@ checksum = "38f0c854faeb68a048f0f2dc410c5ddae3bf83854ef0e4977d58306a5edef50e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -2476,7 +2549,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -2533,34 +2606,32 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.0" +version = "1.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.15", ] [[package]] @@ -2634,7 +2705,7 @@ checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", ] [[package]] @@ -2753,6 +2824,48 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.99", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.99", +] + [[package]] name = "valuable" version = "0.1.0" @@ -2825,7 +2938,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.99", "wasm-bindgen-shared", ] @@ -2859,7 +2972,7 @@ checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2936,43 +3049,109 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 37c53cd5c9dea04b6f757a3211b4ef7932ee52c5..d09ecc1eeedd475b26cb008dc8ca9df5cbac213f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ sha2 = "0.10.2" serde = { version = "1.0.130" } serde_json = "1.0.72" serde_path_to_error = "0.1.5" -tokio = { version = "1.14.0", features = ["full"] } +tokio = { version = "1.28.0", features = ["full"] } tracing = "0.1.36" tracing-subscriber = { version = "0.3.15" } futures = "0.3.24" diff --git a/libpod/Cargo.toml b/libpod/Cargo.toml index ec76540c9607e7ef2da5d709cdea95d159d9c9cf..35318b08541a0d83e71532bd5dedb6f6e2c430b4 100644 --- a/libpod/Cargo.toml +++ b/libpod/Cargo.toml @@ -15,6 +15,8 @@ lettre = { version = "0.10.0-rc.3", default-features = false, features = [ "builder", "rustls-tls", "smtp-transport", + "tokio1-rustls-tls", + "tokio1" ] } libc = "0.2.108" md5 = "0.7.0" @@ -54,6 +56,9 @@ futures = { workspace = true } thiserror = "1.0.38" mime = "0.3.16" scheduled-thread-pool = "0.2.7" +secrecy = { version = "0.8.0", features = ["serde"] } +argon2 = "0.5.0" +validator = { version = "0.16.0", features = ["derive"] } [dev-dependencies] criterion = {version = "0.3.5", features = ["async_tokio"]} diff --git a/libpod/res/migrations/V15__item_UserAccount.sql b/libpod/res/migrations/V15__item_UserAccount.sql new file mode 100644 index 0000000000000000000000000000000000000000..ee5f96f45b7ed32173469ab3eed3ac9fee7a8378 --- /dev/null +++ b/libpod/res/migrations/V15__item_UserAccount.sql @@ -0,0 +1,103 @@ +-- PodUserAccount { +-- login_hash: String, +-- password_hash: String, +-- salt: String, +-- state: String, +-- code: String +-- } + + +-- login: String +INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES( + "d894f98c-20cc-4de1-a7dc-b48590d2cadd", + "ItemPropertySchema", 0, 0, 0, 0 +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "d894f98c-20cc-4de1-a7dc-b48590d2cadd"), + "itemType", "PodUserAccount" +); + +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "d894f98c-20cc-4de1-a7dc-b48590d2cadd"), + "propertyName", "loginHash" +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "d894f98c-20cc-4de1-a7dc-b48590d2cadd"), + "valueType", "Text" +); + +-- passwordHash: String +INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES( + "c9eff54e-8164-4b21-a00e-4c4e9a1fda09", + "ItemPropertySchema", 0, 0, 0, 0 +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "c9eff54e-8164-4b21-a00e-4c4e9a1fda09"), + "itemType", "PodUserAccount" +); + +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "c9eff54e-8164-4b21-a00e-4c4e9a1fda09"), + "propertyName", "passwordHash" +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "c9eff54e-8164-4b21-a00e-4c4e9a1fda09"), + "valueType", "Text" +); + +-- salt: String +INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES( + "6defe71b-d43e-494c-abf4-423dc6c46acc", + "ItemPropertySchema", 0, 0, 0, 0 +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "6defe71b-d43e-494c-abf4-423dc6c46acc"), + "itemType", "PodUserAccount" +); + +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "6defe71b-d43e-494c-abf4-423dc6c46acc"), + "propertyName", "salt" +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "6defe71b-d43e-494c-abf4-423dc6c46acc"), + "valueType", "Text" +); + +-- state: String +INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES( + "570dd24e-fe70-4566-8e45-e3de7a0e0d55", + "ItemPropertySchema", 0, 0, 0, 0 +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "570dd24e-fe70-4566-8e45-e3de7a0e0d55"), + "itemType", "PodUserAccount" +); + +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "570dd24e-fe70-4566-8e45-e3de7a0e0d55"), + "propertyName", "state" +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "570dd24e-fe70-4566-8e45-e3de7a0e0d55"), + "valueType", "Text" +); + +-- code: String +INSERT INTO items(id, type, dateCreated, dateModified, dateServerModified, deleted) VALUES( + "be8d0096-b635-4de4-8b44-6f2a2acec0fa", + "ItemPropertySchema", 0, 0, 0, 0 +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "be8d0096-b635-4de4-8b44-6f2a2acec0fa"), + "itemType", "PodUserAccount" +); + +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "be8d0096-b635-4de4-8b44-6f2a2acec0fa"), + "propertyName", "code" +); +INSERT INTO strings(item, name, value) VALUES( + (SELECT rowid FROM items WHERE id = "be8d0096-b635-4de4-8b44-6f2a2acec0fa"), + "valueType", "Text" +); \ No newline at end of file diff --git a/libpod/src/api_model.rs b/libpod/src/api_model.rs index b4f602ef83f26ec2eba76463629425c8a4d7ecb7..1d5055143e9819426a6852a885ddb285f352f5c1 100644 --- a/libpod/src/api_model.rs +++ b/libpod/src/api_model.rs @@ -1,16 +1,19 @@ use reqwest::Method; +use secrecy::Secret; use serde::de::{self, Visitor}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; +use std::ops::Deref; use std::str::FromStr; use std::{collections::HashMap, fmt::Debug, fmt::Display}; // // Wrapper structs: // + +// TODO: create a macro for implementing truncated logging #[derive(Clone, Serialize, Deserialize, Debug)] pub struct PodOwner(String); - impl Display for PodOwner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = self.0.as_str(); @@ -22,7 +25,6 @@ impl Display for PodOwner { } } -use std::ops::Deref; impl Deref for PodOwner { type Target = String; @@ -45,6 +47,43 @@ impl FromStr for PodOwner { } } +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct PodUserLogin(String); + +/// In order to get full value use deref and reborrow: (&*login) +impl Display for PodUserLogin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = self.0.as_str(); + if s.len() < 7 { + write!(f, "{s}") + } else { + write!(f, "{}..{}", &s[..5], &s[s.len() - 5..]) + } + } +} + +impl Deref for PodUserLogin { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<String> for PodUserLogin { + fn from(s: String) -> Self { + Self(s) + } +} + +impl FromStr for PodUserLogin { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(PodUserLogin(s.into())) + } +} + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct PluginAuthData { @@ -365,11 +404,44 @@ pub struct PluginStatusRes { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct CreateUserReq { +pub struct PodCredentials { pub owner_key: PodOwner, pub database_key: String, } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserAccountCredentials { + #[serde(deserialize_with = "deserialize_login")] + pub login: PodUserLogin, + pub password: Secret<String>, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RegisterVerifyAccountReq { + #[serde(deserialize_with = "deserialize_login")] + pub login: PodUserLogin, + pub code: Secret<String>, +} + +fn deserialize_login<'de, D>(deserializer: D) -> std::result::Result<PodUserLogin, D::Error> +where + D: Deserializer<'de>, +{ + let mail: &str = Deserialize::deserialize(deserializer)?; + // Even though local part of the mail IS case sensitive, we convert to + // lowercase. Major mail service providers correctly assume it + // should be insensitive. + let login = PodUserLogin(mail.to_lowercase()); + + if !validator::validate_email(&login.0) { + return Err(serde::de::Error::custom("Invalid email format")); + } + + Ok(login) +} + #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct PluginGetApiReq { diff --git a/libpod/src/command_line_interface.rs b/libpod/src/command_line_interface.rs index da71460931901d0345ebe0b431c75953f524b625..1ce42695069130585c5f89cd5058917371008a22 100644 --- a/libpod/src/command_line_interface.rs +++ b/libpod/src/command_line_interface.rs @@ -2,6 +2,11 @@ use clap::Parser; use lazy_static::lazy_static; use std::net::IpAddr; +pub const DEFAULT_POD_DB_KEY: &str = + "54fec98071c745d0b812f97cb315f665e497301b39af499686ad8f0464663952"; +pub const DEFAULT_POD_OWNER_KEY: &str = + "ad2e49e832214740af7ff3fa67d7b45bc0d8179346d443c9894b3ebe45978f54"; + #[derive(Parser, Debug, Clone)] #[command( name = "Pod, the open-source backend for Memri project.", @@ -168,12 +173,22 @@ pub struct CliOptions { pub email_smtp_password: Option<String>, /// DB credentials used by the POD to store non volatile information - #[arg(long, env = "POD_OWNER_KEY_FOR_POD", requires = "db_key_for_pod")] - pub owner_key_for_pod: Option<String>, + #[arg( + long, + env = "POD_OWNER_KEY_FOR_POD", + requires = "db_key_for_pod", + default_value = DEFAULT_POD_DB_KEY + )] + pub owner_key_for_pod: String, /// DB credentials used by the POD to store non volatile information - #[arg(long, env = "POD_DB_KEY_FOR_POD", requires = "owner_key_for_pod")] - pub db_key_for_pod: Option<String>, + #[arg( + long, + env = "POD_DB_KEY_FOR_POD", + requires = "owner_key_for_pod", + default_value = DEFAULT_POD_OWNER_KEY + )] + pub db_key_for_pod: String, #[arg(long, env = "POD_SHARED_PLUGINS", action(clap::ArgAction::Append))] pub shared_plugins: Vec<String>, @@ -240,8 +255,8 @@ pub mod tests { email_smtp_port: 465, email_smtp_user: None, email_smtp_password: None, - owner_key_for_pod: None, - db_key_for_pod: None, + owner_key_for_pod: Default::default(), + db_key_for_pod: Default::default(), shared_plugins: Vec::new(), opened_connections: None, } diff --git a/libpod/src/constants.rs b/libpod/src/constants.rs index f565f21a5d668c75a3e6699111edf750d5c3e6f8..64739fd3e6a102ff16102c1c3a8a8f2ea9c8e25d 100644 --- a/libpod/src/constants.rs +++ b/libpod/src/constants.rs @@ -7,8 +7,6 @@ pub const FILES_DIR: &str = "./data/files"; /// (in future, the files should also be s3-uploaded). pub const FILES_FINAL_SUBDIR: &str = "final"; -pub const PLUGIN_EMAIL_SUBJECT_PREFIX: &str = "Memri plugin message: "; -pub const PLUGIN_EMAIL_FOOTER: &str = - "This is an automated message from a Memri plugin, do not reply. +pub const PLUGIN_EMAIL_FOOTER: &str = "This is an automated message from Memri, do not reply. "; diff --git a/libpod/src/database_pool.rs b/libpod/src/database_pool.rs index a98945642f296dc4ccc7ccbc8e4924707d7993e3..e79059e598af7e09ac5466aca271a25cb7a99048 100644 --- a/libpod/src/database_pool.rs +++ b/libpod/src/database_pool.rs @@ -1,7 +1,7 @@ use crate::{ any_error, async_db_connection::AsyncConnection, - bad_request, command_line_interface, constants, database_migrate_refinery, + command_line_interface, constants, database_migrate_refinery, error::{ErrorContext, Result}, internal_error, plugin_auth_crypto::{DatabaseKey, SHA256Output}, @@ -52,7 +52,8 @@ pub async fn initialize_db( ) -> Result<()> { let mut db = init_db.write().await; if owner_database_path(owner).exists() { - return Err(bad_request! { "Account for {owner} already exist" }); + // Database is already set up + return Ok(()); } else if db.get(owner).is_some() { return Err( internal_error! { "DB file does not exist but connection pool does for {owner}" }, diff --git a/libpod/src/db_model.rs b/libpod/src/db_model.rs index fa7324cfeb77871b550f40bb3d10d626259144c4..baa3ccbe609e2ee4a0c2f3eb0a16f2ccd81bb0e8 100644 --- a/libpod/src/db_model.rs +++ b/libpod/src/db_model.rs @@ -102,3 +102,28 @@ pub struct Oauth2Flow { #[serde(flatten)] pub base: ItemBase, } + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub enum RegisterState { + VerifyEmailSent, + RegistrationComplete, + EnforcePasswordReset, +} + +pub const POD_ACCOUNT: &str = "PodUserAccount"; +pub const LOGIN_HASH: &str = "loginHash"; +pub const PASSWORD_HASH: &str = "passwordHash"; +pub const SALT: &str = "salt"; +pub const STATE: &str = "state"; +pub const CODE: &str = "code"; +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PodUserAccount { + pub login_hash: String, + /// In PHC format + pub password_hash: String, + pub salt: String, + pub state: RegisterState, + pub code: String, +} diff --git a/libpod/src/email.rs b/libpod/src/email.rs index 2886e4a14158d3056d7d364152770ac544664a0a..ef58684d43f8f76ba4f1d9d1be5dd7aad4b3b3c5 100644 --- a/libpod/src/email.rs +++ b/libpod/src/email.rs @@ -1,14 +1,15 @@ use crate::{ - api_model::SendEmail, - command_line_interface::CliOptions, - constants::{PLUGIN_EMAIL_FOOTER, PLUGIN_EMAIL_SUBJECT_PREFIX}, + api_model::SendEmail, command_line_interface::CliOptions, constants::PLUGIN_EMAIL_FOOTER, error::Result, }; -use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport}; -use tracing::{info, trace}; +use lettre::{ + transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncTransport, Message, + Tokio1Executor, +}; +use tracing::{debug, info}; -pub fn send_email(email: SendEmail, cli: &CliOptions) -> Result<()> { - trace!("Starting to send email: {:?}", email); +pub async fn send_email(email: SendEmail, cli: &CliOptions) -> Result<()> { + debug!("Starting to send email: {:?}", email); match ( &cli.email_smtp_relay, &cli.email_smtp_user, @@ -16,17 +17,18 @@ pub fn send_email(email: SendEmail, cli: &CliOptions) -> Result<()> { ) { (Some(relay), Some(user), Some(password)) => { let email = Message::builder() - .from(format!("Memri plugin <{user}>").parse()?) + .from(format!("Memri <{user}>").parse()?) .to(email.to.parse()?) - .subject(format!("{}{}", PLUGIN_EMAIL_SUBJECT_PREFIX, email.subject)) - .body(format!("{}{}", PLUGIN_EMAIL_FOOTER, email.body))?; + .subject(email.subject.to_string()) + .body(format!("{}\n{}", email.body, PLUGIN_EMAIL_FOOTER))?; let credentials: Credentials = Credentials::new(user.to_string(), password.to_string()); - let server = SmtpTransport::relay(relay)? + let server = AsyncSmtpTransport::<Tokio1Executor>::relay(relay)? .port(cli.email_smtp_port) .credentials(credentials) .timeout(Some(core::time::Duration::from_millis(5000))) .build(); - server.send(&email)?; + server.send(email).await?; + debug!("Mail sent"); Ok(()) } _ => { diff --git a/libpod/src/lib.rs b/libpod/src/lib.rs index 13eda9e1e3db504865e944f86f0ae2b78fed45a8..0f8707567233baccd33c81c94e3919a243d53495 100644 --- a/libpod/src/lib.rs +++ b/libpod/src/lib.rs @@ -19,6 +19,7 @@ pub mod plugin_auth_crypto; pub mod plugin_run; mod plugin_trigger; pub mod schema; -pub mod shared_plugins; +pub mod shared_state; pub mod test_helpers; pub mod triggers; +pub mod user_account; diff --git a/libpod/src/plugin_run.rs b/libpod/src/plugin_run.rs index 984593520220a49c1240330cbde5b7d376b31b07..7c999ac3464c5225156dc6f417d3edbf8574917a 100644 --- a/libpod/src/plugin_run.rs +++ b/libpod/src/plugin_run.rs @@ -512,12 +512,7 @@ fn kubernetes_set_resource_limits(cli_options: &CliOptions, plugin: &PluginRunIt /// Returns true if pod_owner account is used to store shared data fn is_pod_share_account(pod_owner: &str, cli: &CliOptions) -> bool { - pod_owner.to_ascii_lowercase() - == cli - .owner_key_for_pod - .as_ref() - .unwrap_or(&"".to_string()) - .to_ascii_lowercase() + pod_owner.to_ascii_lowercase() == cli.owner_key_for_pod.to_ascii_lowercase() } pub fn get_logs(tx: &AsyncTx, plugin_run_id: &str, cli_options: &CliOptions) -> Result<Value> { diff --git a/libpod/src/shared_plugins.rs b/libpod/src/shared_state.rs similarity index 64% rename from libpod/src/shared_plugins.rs rename to libpod/src/shared_state.rs index c70dba5da02f7847337e85e921d908ef7262c531..df50c8383f47296ccde5ed3bfa92f59ba373324e 100644 --- a/libpod/src/shared_plugins.rs +++ b/libpod/src/shared_state.rs @@ -2,47 +2,62 @@ use crate::{ api_model::{AuthKey, ClientAuth, CreateItem, Search}, async_db_connection::{AsyncConnection, AsyncTx}, command_line_interface::PARSED, - database_api::{self, dangerous_permament_remove_item}, + database_api, database_pool::{get_db_connection, initialize_db, InitDb}, db_model::ItemBase, - error::Result, - internal_api::{self, search}, + error::{ErrorContext, Result}, + internal_api, plugin_auth_crypto::auth_to_database_key, }; use std::ops::Deref; use tracing::{debug, info, warn}; +pub async fn initialize(init_db: &InitDb) -> Result<()> { + initialize_database(init_db) + .await + .context_str("While initializing shared database connection")?; + initialize_plugins(init_db) + .await + .context_str("While initializing shared plugins") +} + +pub async fn initialize_database(init_db: &InitDb) -> Result<()> { + let (owner_key, database_key) = ( + &PARSED.owner_key_for_pod, + auth_to_database_key(AuthKey::ClientAuth(ClientAuth { + database_key: PARSED.db_key_for_pod.clone(), + }))?, + ); + + initialize_db(owner_key, init_db, &database_key).await +} + +pub async fn db_connection(init_db: &InitDb) -> Result<AsyncConnection> { + let (owner, database_key) = ( + &PARSED.owner_key_for_pod, + auth_to_database_key(AuthKey::ClientAuth(ClientAuth { + database_key: PARSED.db_key_for_pod.clone(), + }))?, + ); + + get_db_connection(owner, init_db, &database_key).await +} + // TODO: there is no proper plugin state management // if pod restarts - plugins are unable to reach it, plugin auth is broken // plugins listens for that, and shuts down // garbage in the db stays // if plugin dies, pod does nothing with it -pub async fn db_connection(init_db: &InitDb) -> Result<Option<AsyncConnection>> { - if let (Some(owner), Some(database_key)) = ( - PARSED.owner_key_for_pod.clone(), - PARSED.db_key_for_pod.clone(), - ) { - let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth { database_key }))?; - let conn = get_db_connection(&owner, init_db, &database_key).await?; - - Ok(Some(conn)) - } else { - Ok(None) - } -} - -pub async fn initialize(init_db: &InitDb) -> Result<()> { - let (Some(owner_key), Some(database_key)) = (PARSED.owner_key_for_pod.clone(), PARSED.db_key_for_pod.clone()) else { - info!("No POD credentials provided, skipping startup of shared plugins"); - return Ok(()); - }; - - let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth { database_key }))?; - - let _ = initialize_db(&owner_key, init_db, &database_key).await; +pub async fn initialize_plugins(init_db: &InitDb) -> Result<()> { + let (owner_key, database_key) = ( + &PARSED.owner_key_for_pod, + auth_to_database_key(AuthKey::ClientAuth(ClientAuth { + database_key: PARSED.db_key_for_pod.clone(), + }))?, + ); - let mut conn = get_db_connection(&owner_key, init_db, &database_key).await?; + let mut conn = db_connection(init_db).await?; // Remove PluginRun from the DB before starting new plugins // Currently plugins have listeners that will detect lack of POD connection @@ -59,7 +74,7 @@ pub async fn initialize(init_db: &InitDb) -> Result<()> { match internal_api::create_item( payload, &mut conn, - &owner_key, + owner_key, &database_key, PARSED.deref(), ) @@ -85,7 +100,7 @@ async fn clean_db_from_plugins(conn: &mut AsyncConnection) -> Result<()> { ..Default::default() }; - let plugins = search(&tx, &schema, query)? + let plugins = internal_api::search(&tx, &schema, query)? .into_iter() .map(|element| { serde_json::from_value::<ItemBase>(element).unwrap_or_else(|err| { @@ -99,7 +114,7 @@ async fn clean_db_from_plugins(conn: &mut AsyncConnection) -> Result<()> { for plugin in plugins { debug!("Removing {}", plugin.id); - if let Err(e) = dangerous_permament_remove_item(&tx, plugin.rowid) { + if let Err(e) = database_api::dangerous_permament_remove_item(&tx, plugin.rowid) { warn!( "Failed to remove PluginRun with id {}, reason {e:?}", plugin.id diff --git a/libpod/src/test_helpers.rs b/libpod/src/test_helpers.rs index 03f17e26cf0f9883500ec33fdeae16d72fc0beda..ec83e605ee29a1a3877d5603c34203f3555bdf78 100644 --- a/libpod/src/test_helpers.rs +++ b/libpod/src/test_helpers.rs @@ -45,8 +45,8 @@ pub fn default_cli() -> CliOptions { email_smtp_port: 465, email_smtp_user: None, email_smtp_password: None, - owner_key_for_pod: None, - db_key_for_pod: None, + owner_key_for_pod: Default::default(), + db_key_for_pod: Default::default(), shared_plugins: Vec::new(), opened_connections: None, } diff --git a/libpod/src/user_account.rs b/libpod/src/user_account.rs new file mode 100644 index 0000000000000000000000000000000000000000..847cbe3984f8e2639dc1d364bb28ffae6006d1e1 --- /dev/null +++ b/libpod/src/user_account.rs @@ -0,0 +1,292 @@ +use std::collections::HashMap; + +use argon2::password_hash::{Salt, SaltString}; +use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use rand::Rng; +use secrecy::{ExposeSecret, Secret}; +use serde_json::json; +use sha2::{Digest, Sha256}; +use tracing::{debug, info, instrument}; + +use crate::api_model::{ + AuthKey, ClientAuth, CreateItem, PodCredentials, PodOwner, RegisterVerifyAccountReq, Search, + SendEmail, UserAccountCredentials, +}; +use crate::async_db_connection::{AsyncConnection, AsyncTx}; +use crate::command_line_interface::CliOptions; +use crate::database_pool::{initialize_db, InitDb}; +use crate::db_model::{ + ItemWithBase, PodUserAccount, RegisterState, CODE, LOGIN_HASH, PASSWORD_HASH, POD_ACCOUNT, + SALT, STATE, +}; +use crate::email::send_email; +use crate::error::{ErrorContext, Result}; +use crate::plugin_auth_crypto::auth_to_database_key; +use crate::{ + bad_request, database_api, database_utils, internal_api, internal_error, shared_state, +}; + +#[instrument(fields(login=%body.login), skip_all)] +pub async fn register( + cli: &CliOptions, + init_db: &InitDb, + body: &UserAccountCredentials, +) -> Result<RegisterState> { + let mut conn = shared_state::db_connection(init_db).await?; + if let Some(acc) = get_account_from_db(&mut conn, &body.login).await? { + Err(bad_request!( + "Account already exists, status {:?}", + acc.item.state + )) + } else { + let mut rnd = rand::thread_rng(); + + let code: Vec<i32> = (0..4).map(|_| rnd.gen_range(0..10)).collect(); + + let email = SendEmail { + to: (*body.login).to_string(), + subject: "Create new account".to_string(), + body: format!( + "Hey, use this token to continue with registration {} {} {} {}", + code[0], code[1], code[2], code[3] + ), + }; + + send_email(email, cli).await?; + + let password_hash = hash_password(body.password.clone()).await?; + let login_hash = hex::encode(Sha256::new_with_prefix(body.login.as_bytes()).finalize()); + + let code_str = format!("{}{}{}{}", code[0], code[1], code[2], code[3]); + + let salt_for_db_keys = SaltString::generate(&mut rand::thread_rng()); + let salt_string = salt_for_db_keys.to_string(); + create_account_in_db( + &mut conn, + password_hash, + &salt_string, + &login_hash, + &code_str, + cli, + ) + .await + } +} + +#[instrument(fields(login=%body.login), skip_all)] +pub async fn verify(init_db: &InitDb, body: &RegisterVerifyAccountReq) -> Result<()> { + let mut conn = shared_state::db_connection(init_db).await?; + if let Some(mut acc) = get_account_from_db(&mut conn, &body.login).await? { + // Account already exists + if acc.item.state == RegisterState::VerifyEmailSent { + debug!("Verifying the account"); + if &acc.item.code == body.code.expose_secret() { + debug!("Registration completed"); + acc.item.state = RegisterState::RegistrationComplete; + update_account_in_db(&mut conn, &acc).await + } else { + Err(bad_request!("Invalid token provided")) + } + } else { + Err(bad_request!( + "Cannot verify account, invalid state {:?}", + acc.item.state + )) + } + } else { + Err(bad_request!("Account does not exist")) + } +} + +/// Get POD keys from user credentials +#[instrument(fields(login=%body.login), skip_all)] +pub async fn get_pod_keys( + init_db: &InitDb, + body: &UserAccountCredentials, +) -> Result<PodCredentials> { + debug!("Deriving keys"); + + let mut conn = shared_state::db_connection(init_db).await?; + let Some(acc) = get_account_from_db(&mut conn, &body.login).await? else { + return Err(bad_request!("Account does not exist")); + }; + + if acc.item.state != RegisterState::RegistrationComplete { + return Err(bad_request!("Account is not verified")); + } + + validate_password(body.password.clone(), acc.item.password_hash.clone()).await?; + + let salt = SaltString::from_b64(&acc.item.salt).expect("Invalid salt format"); + let owner_key = derive_pod_key(Secret::new(body.login.to_string()), salt.clone()).await?; + let database_key = derive_pod_key(body.password.clone(), salt).await?; + + Ok(PodCredentials { + owner_key: PodOwner::from(owner_key.expose_secret().clone()), + database_key: database_key.expose_secret().clone(), + }) +} +#[instrument(fields(owner=%body.owner_key), skip_all)] +pub async fn open_pod(init_db: &InitDb, body: PodCredentials) -> Result<PodOwner> { + info!("Opening POD DB"); + database_utils::check_owner(&body.owner_key)?; + + let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth { + database_key: body.database_key, + }))?; + + initialize_db(&body.owner_key, init_db, &database_key).await?; + + debug!("POD DB ready"); + + Ok(body.owner_key) +} + +async fn validate_password(password: Secret<String>, expected_hash: String) -> Result<()> { + tokio::task::spawn_blocking(move || { + Argon2::default() + .verify_password( + password.expose_secret().as_bytes(), + &PasswordHash::new(&expected_hash).expect("Invalid PHC string format"), + ) + .map_err(|e| bad_request!("Invalid password {e}"))?; + Ok(()) + }) + .await? +} + +/// Generate POD key from the input and provided salt. Returns hexstring. +async fn derive_pod_key(input: Secret<String>, salt: SaltString) -> Result<Secret<String>> { + tokio::task::spawn_blocking(move || { + let mut raw_key = [0u8; 32]; + let mut raw_salt = [0u8; Salt::RECOMMENDED_LENGTH]; + salt.decode_b64(&mut raw_salt) + .map_err(|e| internal_error!("While decoding salt {e}"))?; + + // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + Argon2::default() + .hash_password_into(input.expose_secret().as_bytes(), &raw_salt, &mut raw_key) + .map_err(|e| internal_error!("While deriving the pod key {}", e))?; + + Ok(Secret::new(hex::encode(raw_key))) + }) + .await? +} + +/// Generate argon2 hash with unique salt. Returns PHC string. +async fn hash_password(password: Secret<String>) -> Result<String> { + tokio::task::spawn_blocking(move || { + let salt = SaltString::generate(&mut rand::thread_rng()); + // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html + let phc_hash = Argon2::default() + .hash_password(password.expose_secret().as_bytes(), salt.as_salt()) + .map_err(|e| internal_error!("While hashing the password {}", e))? + .to_string(); + + Ok(phc_hash) + }) + .await? +} + +async fn create_account_in_db( + conn: &mut AsyncConnection, + password_hash: String, + salt: &str, + login_hash: &str, + code: &str, + cli: &CliOptions, +) -> Result<RegisterState> { + conn.in_write_transaction(|tx: AsyncTx| async move { + let mut schema = database_api::get_schema(&tx)?; + let state = RegisterState::VerifyEmailSent; + let item = CreateItem { + _type: POD_ACCOUNT.to_string(), + fields: json!({ + LOGIN_HASH: login_hash, + PASSWORD_HASH: password_hash, + SALT: salt, + CODE: code, + STATE: state, + }) + .as_object() + .expect("Invalid json object for PodAccount") + .clone() + .into_iter() + .collect(), + ..Default::default() + }; + + let id = internal_api::create_item_tx(&tx, &mut schema, item, "", cli).await?; + debug!("PodAccount created {id}"); + Ok(state) + }) + .await +} + +async fn get_account_from_db( + conn: &mut AsyncConnection, + login: &str, +) -> Result<Option<ItemWithBase<PodUserAccount>>> { + let login_hash = hex::encode(Sha256::new_with_prefix(login.as_bytes()).finalize()); + + let mut res = conn + .in_read_transaction(|tx: AsyncTx| async move { + let schema = database_api::get_schema(&tx)?; + + let query = Search { + _type: Some(POD_ACCOUNT.to_string()), + deleted: Some(false), + limit: u64::MAX, + other_properties: HashMap::from([( + LOGIN_HASH.to_string(), + serde_json::to_value(login_hash).expect("Cannot serialize email_hash to Value"), + )]), + ..Default::default() + }; + + internal_api::search(&tx, &schema, query) + }) + .await?; + + debug_assert!( + res.len() <= 1, + "There is more than one PodAccount entry in the database for {:?}", + login + ); + + if let Some(value) = res.pop() { + let pod_account: ItemWithBase<PodUserAccount> = serde_json::from_value(value) + .context(|| format!("Cannot deserialize PodAccount for {:?}", login))?; + + debug!("Found PodAccount item {:#?}", pod_account); + Ok(Some(pod_account)) + } else { + Ok(None) + } +} + +async fn update_account_in_db( + conn: &mut AsyncConnection, + acc: &ItemWithBase<PodUserAccount>, +) -> Result<()> { + conn.in_write_transaction(|tx: AsyncTx| async move { + let schema = database_api::get_schema(&tx)?; + + internal_api::update_item_tx( + &tx, + &schema, + &acc.base.id, + serde_json::to_value(acc.item.clone()) + .expect("Unable to convert PodAccount to Value") + .as_object() + .expect("Invalid json object for PodAccount") + .clone() + .into_iter() + .collect(), + )?; + + debug!("PodAccount {} updated", acc.item.login_hash); + Ok(()) + }) + .await +} diff --git a/pod/Cargo.toml b/pod/Cargo.toml index 57785d35eef823c8e1f15eea879fd43491f0e21c..7881d1e1cb76d335f6e092bce495c85f01030f97 100644 --- a/pod/Cargo.toml +++ b/pod/Cargo.toml @@ -76,8 +76,8 @@ required-features = ["include_slow_tests"] [[test]] -name = "test_create_account" -path = "tests/test_create_account.rs" +name = "test_pod_keys" +path = "tests/test_pod_keys.rs" required-features = ["include_slow_tests"] diff --git a/pod/src/actix_api.rs b/pod/src/actix_api.rs index 4b8898170c9476851de6aca2653534bffb0ce80f..b6d66183575bdc5aeeed8ebff588ed30717fdad2 100644 --- a/pod/src/actix_api.rs +++ b/pod/src/actix_api.rs @@ -1,9 +1,10 @@ use crate::actix_endpoints::{ - bulk, create_account, create_edge, create_item, delete_edge_by_source_target, delete_item, - delete_user, get_edges, get_file, get_item, get_logs, graphql, not_found, oauth1_access_token, - oauth1_request_token, oauth2_access_token, oauth2_auth_url, oauth2_authorize, plugin_api, - plugin_api_call, plugin_attach, plugins_status, search, trace_filter, trigger_status, - update_item, upload_file, upload_file_b, version, + account, account_derive_pod_keys, account_register, account_verify, bulk, create_edge, + create_item, delete_edge_by_source_target, delete_item, delete_pod, get_edges, get_file, + get_item, get_logs, graphql, not_found, oauth1_access_token, oauth1_request_token, + oauth2_access_token, oauth2_auth_url, oauth2_authorize, open_pod, plugin_api, plugin_api_call, + plugin_attach, plugins_status, search, send_email, trace_filter, trigger_status, update_item, + upload_file, upload_file_b, version, }; use actix_cors::Cors; use actix_web::{ @@ -15,7 +16,7 @@ use libpod::{ command_line_interface::CliOptions, database_pool::ConnectionPool, error::{ErrorContext, Result}, - internal_error, shared_plugins, + internal_error, shared_state, }; use rustls::{Certificate, PrivateKey, ServerConfig}; use rustls_pemfile::{certs, pkcs8_private_keys}; @@ -43,7 +44,7 @@ pub async fn run_server<S: 'static>( HashMap::<String, ConnectionPool>::new(), )); - let shared_plugin_db = init_db.clone(); + let shared_state = init_db.clone(); let cli_options = web::Data::new(cli_options); let trace_handle = web::Data::new(trace_handle); @@ -65,6 +66,7 @@ pub async fn run_server<S: 'static>( .route("/trace_filter", web::post().to(trace_filter::<S>)) .service( web::scope("/v4") + .service(send_email) .service(create_item) .service(get_item) .service(oauth1_request_token) @@ -75,8 +77,9 @@ pub async fn run_server<S: 'static>( .service(update_item) .service(bulk) .service(delete_item) - .service(delete_user) - .service(create_account) + .service(delete_pod) + .service(open_pod) + .service(account) .service(create_edge) .service(delete_edge_by_source_target) .service(get_edges) @@ -109,7 +112,10 @@ pub async fn run_server<S: 'static>( .service(plugin_attach), ) .service(get_logs) - .service(trigger_status), + .service(trigger_status) + .service(account_register) + .service(account_verify) + .service(account_derive_pod_keys), ) }); @@ -165,10 +171,10 @@ pub async fn run_server<S: 'static>( Ok(()) }); - // Note initializing shared plugins must happen after webserver is up and ready, + // Note initializing shared state and plugins in particular must happen after webserver is up and ready, // otherwise starting plugin might not be able to reach the pod - RC for the webserver - if let Err(e) = shared_plugins::initialize(shared_plugin_db.deref()).await { - warn!("Failed to initialize shared plugins: {e}"); + if let Err(e) = shared_state::initialize(shared_state.deref()).await { + warn!("Failed to initialize shared state: {e}"); } server_handle.await? diff --git a/pod/src/actix_endpoints.rs b/pod/src/actix_endpoints.rs index f0790d7ce502a8ef2f9b481e794b87ee06501703..cbd82a2e0113277dc784627fb907641520af3036 100644 --- a/pod/src/actix_endpoints.rs +++ b/pod/src/actix_endpoints.rs @@ -169,26 +169,81 @@ pub async fn delete_item( } #[instrument(fields(uid=trace_uid(), %owner), skip_all)] -#[post("{owner}/delete_user")] -pub async fn delete_user( +#[post("{owner}/delete_pod")] +pub async fn delete_pod( owner: web::Path<PodOwner>, init_db: web::Data<InitDb>, body: web::Bytes, ) -> actix_web::Result<impl Responder> { let body = extract_json(&body)?; - let result = pod_handlers::delete_user(owner.to_owned(), &init_db.to_owned(), body).await; + let result = pod_handlers::delete_pod(owner.to_owned(), &init_db.to_owned(), body).await; let result = result.map(|()| web::Json(serde_json::json!({}))); respond_with_result(result) } +// TODO: deprecated #[instrument(fields(uid=trace_uid()), skip_all)] #[post("/account")] -pub async fn create_account( +pub async fn account( + init_db: web::Data<InitDb>, + body: web::Bytes, +) -> actix_web::Result<impl Responder> { + do_open_pod(init_db, body).await +} + +#[post("/account/pod/open")] +pub async fn open_pod( + init_db: web::Data<InitDb>, + body: web::Bytes, +) -> actix_web::Result<impl Responder> { + do_open_pod(init_db, body).await +} + +// TODO: current implementation allows everyone to reach this endpoint and +// do as many pods as you like. Changing this will break the pymemri and plugins tests. +async fn do_open_pod( + init_db: web::Data<InitDb>, + body: web::Bytes, +) -> actix_web::Result<impl Responder> { + let body = extract_json(&body)?; + let result = pod_handlers::open_pod(&init_db.into_inner(), body).await; + let result = result.map(|result| web::Json(serde_json::json!(result))); + respond_with_result(result) +} + +#[instrument(fields(uid=trace_uid()), skip_all)] +#[post("/account/register")] +pub async fn account_register( + cli: web::Data<CliOptions>, + init_db: web::Data<InitDb>, + body: web::Bytes, +) -> actix_web::Result<impl Responder> { + let body = extract_json(&body)?; + let result = pod_handlers::account_register(&cli, &init_db.into_inner(), body).await; + let result = result.map(|result| web::Json(serde_json::json!(result))); + respond_with_result(result) +} + +#[instrument(fields(uid=trace_uid()), skip_all)] +#[post("/account/verify")] +pub async fn account_verify( + init_db: web::Data<InitDb>, + body: web::Bytes, +) -> actix_web::Result<impl Responder> { + let body = extract_json(&body)?; + let result = pod_handlers::account_verify(&init_db.into_inner(), body).await; + let result = result.map(|result| web::Json(serde_json::json!(result))); + respond_with_result(result) +} + +#[instrument(fields(uid=trace_uid()), skip_all)] +#[post("/account/derive_pod_keys")] +pub async fn account_derive_pod_keys( init_db: web::Data<InitDb>, body: web::Bytes, ) -> actix_web::Result<impl Responder> { let body = extract_json(&body)?; - let result = pod_handlers::create_account(&init_db.into_inner(), body).await; + let result = pod_handlers::account_derive_pod_keys(&init_db.into_inner(), body).await; let result = result.map(|result| web::Json(serde_json::json!(result))); respond_with_result(result) } diff --git a/pod/src/pod_handlers.rs b/pod/src/pod_handlers.rs index ce46aa1fa7e0396ccc5795dfdff5e8c722e01624..50d0f65c370954b17563313239ec9e4221092790 100644 --- a/pod/src/pod_handlers.rs +++ b/pod/src/pod_handlers.rs @@ -5,25 +5,25 @@ use http::StatusCode; use libpod::{ any_error, api_model::{ - AuthKey, Bulk, BulkResponse, ClientAuth, CreateEdge, CreateItem, CreateUserReq, - DeleteEdgeBySourceTarget, GetEdges, GetFile, Oauth2AccessTokenRequest, - Oauth2AccessTokenResponse, Oauth2AuthUrlRequest, Oauth2AuthUrlResponse, - Oauth2AuthorizeTokenRequest, OauthAccessTokenPayload, OauthRequestTokenPayload, - PayloadWrapper, PluginAttachReq, PluginCallApiReq, PluginCallApiRes, PluginGetApiReq, - PluginGetApiRes, PluginStatusReq, PluginStatusRes, PodOwner, Search, SendEmail, - TraceRequest, UpdateItem, + AuthKey, Bulk, BulkResponse, CreateEdge, CreateItem, DeleteEdgeBySourceTarget, GetEdges, + GetFile, Oauth2AccessTokenRequest, Oauth2AccessTokenResponse, Oauth2AuthUrlRequest, + Oauth2AuthUrlResponse, Oauth2AuthorizeTokenRequest, OauthAccessTokenPayload, + OauthRequestTokenPayload, PayloadWrapper, PluginAttachReq, PluginCallApiReq, + PluginCallApiRes, PluginGetApiReq, PluginGetApiRes, PluginStatusReq, PluginStatusRes, + PodCredentials, PodOwner, RegisterVerifyAccountReq, Search, SendEmail, TraceRequest, + UpdateItem, UserAccountCredentials, }, async_db_connection::AsyncTx, bad_request, command_line_interface::CliOptions, database_api::{self}, - database_pool::{initialize_db, InitDb}, + database_pool::InitDb, database_utils, email, error::Result, error::{ErrorContext, ErrorType}, file_api, internal_api, internal_error, oauth1_api, oauth2_api, plugin_auth_crypto::{auth_to_database_key, DatabaseKey}, - plugin_run, shared_plugins, triggers, + plugin_run, shared_state, triggers, user_account, }; use serde_json::Value; use sha2::{Digest, Sha256}; @@ -31,7 +31,7 @@ use std::{ collections::HashMap, sync::{atomic::AtomicU16, Arc}, }; -use tokio::task; + use tracing::{debug, info, warn}; use tracing_subscriber::{reload::Handle, EnvFilter}; @@ -209,7 +209,7 @@ pub async fn delete_item( .await } #[inline(always)] -pub async fn delete_user( +pub async fn delete_pod( owner: PodOwner, init_db: &InitDb, body: PayloadWrapper<String>, @@ -220,21 +220,42 @@ pub async fn delete_user( database_utils::check_owner_and_delete_db(&owner, init_db, &database_key).await?; Result::Ok(()) } + #[inline(always)] -pub async fn create_account(init_db: &InitDb, body: CreateUserReq) -> Result<PodOwner> { - info!("Creating account for {}", body.owner_key); - database_utils::check_owner(&body.owner_key)?; +pub async fn open_pod(init_db: &InitDb, body: PodCredentials) -> Result<PodOwner> { + user_account::open_pod(init_db, body).await +} - let database_key = auth_to_database_key(AuthKey::ClientAuth(ClientAuth { - database_key: body.database_key, - }))?; +#[inline(always)] +pub async fn account_register( + cli: &CliOptions, + init_db: &InitDb, + body: UserAccountCredentials, +) -> Result<()> { + info!("Register account for {:?}", body.login); - initialize_db(&body.owner_key, init_db, &database_key).await?; + user_account::register(cli, init_db, &body).await?; debug!("Account created"); - Ok(body.owner_key) + Ok(()) +} + +#[inline(always)] +pub async fn account_verify(init_db: &InitDb, body: RegisterVerifyAccountReq) -> Result<()> { + info!("Verify account for {:?}", body.login); + + user_account::verify(init_db, &body).await +} + +#[inline(always)] +pub async fn account_derive_pod_keys( + init_db: &InitDb, + body: UserAccountCredentials, +) -> Result<PodCredentials> { + user_account::get_pod_keys(init_db, &body).await } + #[inline(always)] pub async fn create_edge( owner: PodOwner, @@ -410,12 +431,14 @@ pub async fn send_email( ) -> Result<()> { let auth = body.auth; let payload = body.payload; + + // Get conn to validate creds let database_key = auth_to_database_key(auth)?; let _conn = database_utils::check_owner_and_initialize_db(&owner, init_db, &database_key).await?; let cli = cli.clone(); - task::spawn_blocking(move || email::send_email(payload, &cli)).await? + email::send_email(payload, &cli).await } #[inline(always)] @@ -459,11 +482,9 @@ pub async fn plugins_status( let plugins = plugin_run::get_plugins_status(&mut conn, cli, &body.payload).await?; - let shared_plugins = if let Some(mut conn) = shared_plugins::db_connection(init_db).await? { - plugin_run::get_plugins_status(&mut conn, cli, &body.payload).await? - } else { - HashMap::new() - }; + let mut conn = shared_state::db_connection(init_db).await?; + + let shared_plugins = plugin_run::get_plugins_status(&mut conn, cli, &body.payload).await?; Ok(PluginStatusRes { plugins, @@ -492,9 +513,8 @@ pub async fn plugin_api( if plugin_not_found { // Plugin not found, maybe it's shared plugin? - if let Some(mut conn) = shared_plugins::db_connection(init_db).await? { - api = plugin_run::get_plugin_api(&mut conn, cli, &body.payload).await; - } + let mut conn = shared_state::db_connection(init_db).await?; + api = plugin_run::get_plugin_api(&mut conn, cli, &body.payload).await; } api @@ -521,9 +541,8 @@ pub async fn plugin_api_call( if plugin_not_found { // Plugin not found, maybe it's shared plugin? - if let Some(mut conn) = shared_plugins::db_connection(init_db).await? { - api_response = plugin_run::call_plugin_api(&mut conn, cli, &body.payload).await - } + let mut conn = shared_state::db_connection(init_db).await?; + api_response = plugin_run::call_plugin_api(&mut conn, cli, &body.payload).await; } api_response diff --git a/pod/tests/common/pod_client.rs b/pod/tests/common/pod_client.rs index f1b3b67cecc635b21eb3dc5faad71008a8932bef..cb2b92f2982bd853645c7c55eda74571f0f1aaf9 100644 --- a/pod/tests/common/pod_client.rs +++ b/pod/tests/common/pod_client.rs @@ -31,12 +31,10 @@ impl PodClient { } /// Convenient function to do POST request with given payload to the POD - pub async fn post_to<T>(&self, payload: T, endpoint: &str) -> Response + pub async fn post_to_with_owner<T>(&self, payload: T, endpoint: &str) -> Response where T: Serialize + Debug, { - debug!("POST: {endpoint}, body {payload:#?}"); - let body = PayloadWrapper { auth: AuthKey::ClientAuth(libpod::api_model::ClientAuth { database_key: self.database_key.clone(), @@ -44,12 +42,19 @@ impl PodClient { payload, }; + self.post_to(body, &format!("{}/{endpoint}", self.owner_key)) + .await + } + + pub async fn post_to<T>(&self, payload: T, endpoint: &str) -> Response + where + T: Serialize + Debug, + { + debug!("POST: {endpoint}, body {payload:#?}"); + self.client - .post(&format!( - "{}/v4/{}/{endpoint}", - self.pod_url, self.owner_key - )) - .json(&body) + .post(&format!("{}/v4/{endpoint}", self.pod_url)) + .json(&payload) .send() .await .unwrap() diff --git a/pod/tests/common/pod_request.rs b/pod/tests/common/pod_request.rs index c3c82f177f341b0daa2493dfdd90ff2f8959dedc..189a4bc6cc5fac2ea9fbe18ee5499256266bae7a 100644 --- a/pod/tests/common/pod_request.rs +++ b/pod/tests/common/pod_request.rs @@ -34,7 +34,7 @@ pub async fn register_account(client: &PodClient) { pub async fn migrate_plugin_definition(client: &PodClient) { let plugin_def = serde_json::from_str::<Bulk>(include_str!("plugin_migration.json")).unwrap(); - let resp = client.post_to(plugin_def, "bulk").await; + let resp = client.post_to_with_owner(plugin_def, "bulk").await; assert_eq!(resp.status(), reqwest::StatusCode::OK); } @@ -62,7 +62,7 @@ pub async fn start_plugin(ctx: &mut TestData) -> (String, String) { } ); - let resp = ctx.pod_client.post_to(payload, "bulk").await; + let resp = ctx.pod_client.post_to_with_owner(payload, "bulk").await; assert_eq!(resp.status(), reqwest::StatusCode::OK); // Starting container takes time... @@ -134,7 +134,9 @@ pub async fn start_plugin(ctx: &mut TestData) -> (String, String) { pub async fn delete_plugin(pod_client: &PodClient) { let plugin_id = "4fa2094c-51d6-4874-a135-9017fcdedf16"; - let resp = pod_client.post_to(plugin_id, "delete_item").await; + let resp = pod_client + .post_to_with_owner(plugin_id, "delete_item") + .await; assert_eq!(resp.status(), reqwest::StatusCode::OK); let plugin_item = get_db_item(plugin_id, pod_client).await; @@ -178,7 +180,7 @@ pub async fn register_trigger(client: &PodClient) -> String { ] }); - let resp = client.post_to(payload, "bulk").await; + let resp = client.post_to_with_owner(payload, "bulk").await; assert_eq!(resp.status(), reqwest::StatusCode::OK); trigger_id @@ -192,7 +194,7 @@ pub async fn send_fake_msg(client: &PodClient) { "dateSent": 1655170839993u64 }); - let resp = client.post_to(payload, "create_item").await; + let resp = client.post_to_with_owner(payload, "create_item").await; assert_eq!(resp.status(), reqwest::StatusCode::OK); } @@ -202,7 +204,7 @@ pub async fn get_db_item(item_id: &str, client: &PodClient) -> Value { limit: u64::MAX, ..Default::default() }; - let resp = client.post_to(payload, "search").await; + let resp = client.post_to_with_owner(payload, "search").await; resp.json::<Vec<Value>>() .await diff --git a/pod/tests/common/test_data.rs b/pod/tests/common/test_data.rs index 4c2dd692886a754d772b493471f25aaa180f7fce..9ca330eca1d61f5735024ea474ac42d66d54b714 100644 --- a/pod/tests/common/test_data.rs +++ b/pod/tests/common/test_data.rs @@ -66,7 +66,7 @@ fn get_pod_path_executable() -> PathBuf { build_mode = "release"; } - // From root path go to partent workspace root, where target dir is present + // From root path go to parent workspace root, where target dir is present root_path() .parent() .unwrap() diff --git a/pod/tests/test_create_item.rs b/pod/tests/test_create_item.rs index 52584febecafa6ee2e683a01fe6d70d0d600b742..ec113c836c0ba2c35902785b88eabeb0672f6fa5 100644 --- a/pod/tests/test_create_item.rs +++ b/pod/tests/test_create_item.rs @@ -18,7 +18,10 @@ async fn test_cannot_create_item_outside_schema(ctx: &mut TestData) { "dateSent": 1655170839993u64 }); - let resp = ctx.pod_client.post_to(payload, "create_item").await; + let resp = ctx + .pod_client + .post_to_with_owner(payload, "create_item") + .await; assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST); assert_eq!( resp.json::<String>().await.unwrap(), @@ -39,7 +42,10 @@ async fn test_cannot_create_item_with_props_outside_schema(ctx: &mut TestData) { }); - let resp = ctx.pod_client.post_to(payload, "create_item").await; + let resp = ctx + .pod_client + .post_to_with_owner(payload, "create_item") + .await; assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST); assert!(resp .json::<String>() @@ -64,7 +70,10 @@ async fn test_cannot_create_item_with_props_defined_for_other_object(ctx: &mut T }); - let resp = ctx.pod_client.post_to(payload, "create_item").await; + let resp = ctx + .pod_client + .post_to_with_owner(payload, "create_item") + .await; assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST); assert!(resp.json::<String>().await.unwrap().contains( "Failure: Error 400 Bad Request, Property triggerOn not defined in Schema for PluginRun" diff --git a/pod/tests/test_plugin.rs b/pod/tests/test_plugin.rs index 2c15a1190d2c15264e17b83c8298848970f1c4a1..6ffe47aed0346927b8052726f74456777449d60e 100644 --- a/pod/tests/test_plugin.rs +++ b/pod/tests/test_plugin.rs @@ -57,7 +57,7 @@ async fn test_start_plugin_by_bulk(ctx: &mut TestData) { ] }); - let resp = ctx.pod_client.post_to(payload, "bulk").await; + let resp = ctx.pod_client.post_to_with_owner(payload, "bulk").await; assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST); diff --git a/pod/tests/test_plugin_api.rs b/pod/tests/test_plugin_api.rs index 1d6d0734ff5fb92a70b9ad76daedca21d7bd7ef4..ba395d34ce2b49454f9fec6ff1fe95c4354f5ce2 100644 --- a/pod/tests/test_plugin_api.rs +++ b/pod/tests/test_plugin_api.rs @@ -23,7 +23,10 @@ async fn test_plugin_api(ctx: &mut TestData) { async fn test_plugin_get_api(id: String, alias: String, ctx: &TestData) { let payload = json!({ "id": id }); - let response = ctx.pod_client.post_to(payload, "plugin/api").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api") + .await; assert!(response.status().is_success()); @@ -57,7 +60,10 @@ async fn test_plugin_get_api(id: String, alias: String, ctx: &TestData) { // Same, but using alias let payload = json!({ "id": alias }); - let response = ctx.pod_client.post_to(payload, "plugin/api").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api") + .await; assert!(response.status().is_success()); @@ -94,7 +100,10 @@ async fn test_plugin_get_api(id: String, alias: String, ctx: &TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/api").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api") + .await; assert!(!response.status().is_success()); @@ -132,7 +141,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) { } }); - let response = ctx.pod_client.post_to(payload, "plugin/api/call").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api/call") + .await; assert!(response.status().is_success()); @@ -181,7 +193,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) { } }); - let response = ctx.pod_client.post_to(payload, "plugin/api/call").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api/call") + .await; assert!(response.status().is_success()); @@ -230,7 +245,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) { } }); - let response = ctx.pod_client.post_to(payload, "plugin/api/call").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api/call") + .await; assert!(response.status().is_success()); @@ -255,7 +273,10 @@ async fn test_plugin_call_api(id: String, alias: String, ctx: &TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/api/call").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/api/call") + .await; assert!(!response.status().is_success()); diff --git a/pod/tests/test_plugin_state.rs b/pod/tests/test_plugin_state.rs index 13f887fe9c9f7194e057272e523903c2d3c91252..c63ff84ae5c83620e640d03ec6b6087afddb7996 100644 --- a/pod/tests/test_plugin_state.rs +++ b/pod/tests/test_plugin_state.rs @@ -20,7 +20,10 @@ async fn test_plugin_state(ctx: &mut TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); assert_eq!( @@ -42,7 +45,10 @@ async fn test_plugin_state(ctx: &mut TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); @@ -66,7 +72,10 @@ async fn test_plugin_state(ctx: &mut TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); @@ -87,7 +96,10 @@ async fn test_plugin_state(ctx: &mut TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); @@ -108,7 +120,10 @@ async fn test_plugin_state(ctx: &mut TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); @@ -124,7 +139,10 @@ async fn test_plugin_state(ctx: &mut TestData) { } ); - let response = ctx.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); diff --git a/pod/tests/test_create_account.rs b/pod/tests/test_pod_keys.rs similarity index 64% rename from pod/tests/test_create_account.rs rename to pod/tests/test_pod_keys.rs index 85fa1cfa3e89129b5e6e9db1906b4faeb9187f07..41e8ca9cdd4366531e9dc4cf2ecaba5cff103ee7 100644 --- a/pod/tests/test_create_account.rs +++ b/pod/tests/test_pod_keys.rs @@ -16,7 +16,7 @@ async fn use_pod(client: &PodClient) -> Response { "valueType": "Text" }); - client.post_to(payload, "create_item").await + client.post_to_with_owner(payload, "create_item").await } // The truth table of possible states. @@ -29,14 +29,14 @@ async fn use_pod(client: &PodClient) -> Response { // 2 | 0 | 0 | 1 | db, and pool created // 3 | 0 | 1 | 1 | unreachable -> return internal server error // 4 | 0 | 1 | 1 | unreachable -> will return rusqlite error upon trying to access the db -// 5 | 1 | 0 | 1 | error -> user already exists +// 5 | 1 | 0 | 1 | ignore -> user already exists // 6 | 1 | 0 | 1 | create connection pool, validate credentials, proceed -// 7 | 1 | 1 | 1 | error -> user already exists +// 7 | 1 | 1 | 1 | ignore -> user already exists // 8 | 1 | 1 | 1 | check credentials, proceed #[test_context(TestData)] #[tokio::test] -async fn test_account_use_not_registered(ctx: &mut TestData) { +async fn test_db_not_registered(ctx: &mut TestData) { let valid_credentials = PodClient::new(&ctx.database_key, &ctx.owner_key, &ctx.pod_url); // State #1, trying to use pod, while not yet registered @@ -46,49 +46,25 @@ async fn test_account_use_not_registered(ctx: &mut TestData) { #[test_context(TestData)] #[tokio::test] -async fn test_account_create(ctx: &mut TestData) { - let valid_credentials = PodClient::new(&ctx.database_key, &ctx.owner_key, &ctx.pod_url); - +async fn test_create_pod_db(ctx: &mut TestData) { // State #2, register from the list - the happy path let register_request = json!( { - "ownerKey": valid_credentials.owner_key, - "databaseKey": valid_credentials.database_key + "ownerKey": ctx.owner_key, + "databaseKey": ctx.database_key }); - let res = ctx - .pod_client - .client - .post(format!("{}/v4/account", valid_credentials.pod_url)) - .json(®ister_request) - .send() - .await - .unwrap(); + let res = ctx.pod_client.post_to(®ister_request, "account").await; assert_eq!(res.status(), StatusCode::OK); - // Cannot register twice, state #7 - let res = ctx - .pod_client - .client - .post(format!("{}/v4/account", valid_credentials.pod_url)) - .json(®ister_request) - .send() - .await - .unwrap(); + // Cannot create pod twice, state #7 + let res = ctx.pod_client.post_to(®ister_request, "account").await; - assert_eq!(res.status(), StatusCode::BAD_REQUEST); - - assert_eq!( - res.json::<String>().await.unwrap(), - format!( - "Failure: Error 400 Bad Request, Account for {} already exist", - valid_credentials.owner_key - ) - ); + assert_eq!(res.status(), StatusCode::OK); // Can use the POD, state #8 - let res = use_pod(&valid_credentials).await; + let res = use_pod(&ctx.pod_client).await; assert_eq!(res.status(), StatusCode::OK); // Cannot use the POD with invalid password @@ -104,14 +80,7 @@ async fn test_account_create(ctx: &mut TestData) { // db disappeared, user tries incorrectly to register again ctx.remove_db().await; - let res = ctx - .pod_client - .client - .post(format!("{}/v4/account", valid_credentials.pod_url)) - .json(®ister_request) - .send() - .await - .unwrap(); + let res = ctx.pod_client.post_to(®ister_request, "account").await; assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); @@ -119,47 +88,38 @@ async fn test_account_create(ctx: &mut TestData) { res.json::<String>().await.unwrap(), format!( "Failure: Error 500 Internal Server Error, DB file does not exist but connection pool does for {}", - valid_credentials.owner_key + ctx.owner_key ) ); // State #4: db disappeared, user tries to use the pod - let res = use_pod(&valid_credentials).await; + let res = use_pod(&ctx.pod_client).await; assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!( res.json::<String>().await.unwrap(), format!( "Failure: Error 500 Internal Server Error, No DB, but connection pool exist for {}", - valid_credentials.owner_key + ctx.owner_key ) ); } #[test_context(TestData)] #[tokio::test] -async fn test_account_works_after_pod_restart(ctx: &mut TestData) { - let valid_credentials = PodClient::new(&ctx.database_key, &ctx.owner_key, &ctx.pod_url); - +async fn test_pod_keys_works_after_restart(ctx: &mut TestData) { // # 2 register from the list - the happy path let register_request = json!( { - "ownerKey": valid_credentials.owner_key, - "databaseKey": valid_credentials.database_key + "ownerKey": ctx.owner_key, + "databaseKey": ctx.database_key }); - let res = ctx - .pod_client - .client - .post(format!("{}/v4/account", valid_credentials.pod_url)) - .json(®ister_request) - .send() - .await - .unwrap(); + let res = ctx.pod_client.post_to(®ister_request, "account").await; assert_eq!(res.status(), StatusCode::OK); - let res = use_pod(&valid_credentials).await; + let res = use_pod(&ctx.pod_client).await; assert_eq!(res.status(), StatusCode::OK); // Restart the POD @@ -177,28 +137,21 @@ async fn test_account_works_after_pod_restart(ctx: &mut TestData) { ctx.pod_logs = pod_logs; // Can use the POD, state #6 - let res = use_pod(&valid_credentials).await; + let res = use_pod(&ctx.pod_client).await; assert_eq!(res.status(), StatusCode::OK); - // Cannot register again, state #5 + // Cannot create again, state #5 let res = ctx .pod_client .client - .post(format!("{}/v4/account", valid_credentials.pod_url)) + .post(format!("{}/v4/account", ctx.pod_url)) .json(®ister_request) .send() .await .unwrap(); - assert_eq!(res.status(), StatusCode::BAD_REQUEST); - - assert_eq!( - res.json::<String>().await.unwrap(), - format!( - "Failure: Error 400 Bad Request, Account for {} already exist", - valid_credentials.owner_key - ) - ); + // Ignore + assert_eq!(res.status(), StatusCode::OK); } #[test_context(TestData)] diff --git a/pod/tests/test_shared_plugin.rs b/pod/tests/test_shared_plugin.rs index 6cd9bcca8703accea8ec409b25b32d6e9c74292c..0c750a4a28d67660f1214d106889cb167fa484c1 100644 --- a/pod/tests/test_shared_plugin.rs +++ b/pod/tests/test_shared_plugin.rs @@ -52,7 +52,11 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) { } ); - let resp = ctx.base.pod_client.post_to(payload, "bulk").await; + let resp = ctx + .base + .pod_client + .post_to_with_owner(payload, "bulk") + .await; assert_eq!(resp.status(), reqwest::StatusCode::OK); @@ -64,7 +68,11 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) { "plugins" : [] } ); - let response = ctx.base.pod_client.post_to(payload, "plugin/status").await; + let response = ctx + .base + .pod_client + .post_to_with_owner(payload, "plugin/status") + .await; assert!(response.status().is_success()); let response_plugins = response.json::<PluginStatusRes>().await.unwrap(); @@ -95,7 +103,11 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) { // It is possible to get API of shared plugin let payload = json!({ "id": SHARED_PLUGIN1 }); - let response = ctx.base.pod_client.post_to(payload, "plugin/api").await; + let response = ctx + .base + .pod_client + .post_to_with_owner(payload, "plugin/api") + .await; assert!(response.status().is_success()); @@ -127,7 +139,7 @@ async fn test_shared_plugin(ctx: &mut TestDataSharedPlugin) { let response = ctx .base .pod_client - .post_to(payload, "plugin/api/call") + .post_to_with_owner(payload, "plugin/api/call") .await; assert!(response.status().is_success());