diff --git a/.env.example b/.env.example index bfe8157..7ad33bc 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ -NSC_PATH = "" -HOST_CREDS_FILE_PATH = "ops/admin.creds" -MONGO_URI = "mongodb://:" -NATS_HUB_SERVER_URL = "nats://:" -LEAF_SERVER_USER = "test-user" -LEAF_SERVER_PW = "pw-123456789" +NSC_PATH="" +HOST_NKEY_PATH="/host.nk" +SYS_NKEY_PATH="/sys.nk" +NATS_URL="nats:/:" +NATS_LISTEN_PORT="" +LEAF_SERVER_DEFAULT_LISTEN_PORT="" +MONGO_URI="mongodb://:" +HPOS_CONFIG_PATH="path/to/file.config"; +DEVICE_SEED_DEFAULT_PASSWORD="device_pw_1234" diff --git a/.gitignore b/.gitignore index 41cbad9..1812acf 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ rust/*/*/leaf_server.conf rust/*/*/resolver.conf leaf_server.conf .local - +rust/*/*/*/tmp/ +rust/*/*/*/*/tmp/ diff --git a/Cargo.lock b/Cargo.lock index 49d31e8..052aa50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ahash" version = "0.8.11" @@ -24,7 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -39,6 +45,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -110,12 +122,50 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "argon2min" +version = "0.3.0" +source = "git+https://github.com/Holo-Host/argon2min?rev=28e765e4369e19bc0126bb46acaacadf1303de22#28e765e4369e19bc0126bb46acaacadf1303de22" +dependencies = [ + "blake2-rfc", +] + [[package]] name = "array-init" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "assert_cmd" version = "2.0.16" @@ -147,7 +197,7 @@ dependencies = [ "once_cell", "pin-project", "portable-atomic", - "rand", + "rand 0.8.5", "regex", "ring", "rustls-native-certs 0.7.3", @@ -179,6 +229,43 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "authentication" +version = "0.0.1" +dependencies = [ + "anyhow", + "async-nats", + "async-trait", + "bson", + "bytes", + "chrono", + "data-encoding", + "dotenv", + "env_logger", + "futures", + "jsonwebtoken", + "log", + "mongodb", + "nats-jwt", + "nkeys", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.11", + "tokio", + "url", + "util_libs", +] + +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.4.0", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -200,6 +287,28 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base36" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9c26bddc1271f7112e5ec797e8eeba6de2de211c1488e506b9500196dbf77c5" +dependencies = [ + "base-x", + "failure", +] + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "base64" version = "0.13.1" @@ -272,6 +381,27 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec 0.4.12", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.3.1", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -295,7 +425,7 @@ dependencies = [ "indexmap 2.7.1", "js-sys", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_bytes", "serde_json", @@ -411,6 +541,15 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -438,11 +577,23 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -475,6 +626,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -493,6 +653,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.3" @@ -522,6 +688,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -570,6 +737,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "dary_heap" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" + [[package]] name = "data-encoding" version = "2.7.0" @@ -619,6 +792,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -699,6 +883,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ + "pkcs8", + "serde", "signature", ] @@ -710,9 +896,11 @@ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", + "serde", "sha2", "signature", "subtle", + "zeroize", ] [[package]] @@ -772,6 +960,28 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -784,6 +994,18 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "flate2" version = "1.0.35" @@ -809,6 +1031,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "2.0.0" @@ -914,6 +1142,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -921,8 +1160,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -943,12 +1184,37 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "hc_seed_bundle" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f930e251000e258ff14c36c4d045c9ec1dcbf2a6fff53b1432342b9a34df5ae" +dependencies = [ + "futures", + "one_err", + "rmp-serde", + "rmpv", + "serde", + "serde_bytes", + "sodoken", +] + [[package]] name = "heck" version = "0.3.3" @@ -964,6 +1230,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hex" version = "0.4.3" @@ -986,7 +1258,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand", + "rand 0.8.5", "thiserror 1.0.69", "tinyvec", "tokio", @@ -1007,7 +1279,7 @@ dependencies = [ "lru-cache", "once_cell", "parking_lot", - "rand", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror 1.0.69", @@ -1030,21 +1302,30 @@ version = "0.0.1" dependencies = [ "anyhow", "async-nats", + "authentication", "bson", "bytes", "chrono", "clap", + "data-encoding", "dotenv", + "ed25519-dalek", "env_logger", "futures", + "hpos-config-core", + "hpos-config-seed-bundle-explorer", "hpos-hal", + "jsonwebtoken", "log", "mongodb", + "nats-jwt", "nkeys", - "rand", + "rand 0.8.5", "serde", "serde_json", + "sha2", "tempfile", + "textnonce", "thiserror 2.0.11", "tokio", "url", @@ -1063,6 +1344,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "hpos-config-core" +version = "0.2.1" +source = "git+https://github.com/holo-host/hpos-config.git?rev=77d740c83a02e322e670e360eb450076b593b328#77d740c83a02e322e670e360eb450076b593b328" +dependencies = [ + "argon2min", + "arrayref", + "base36", + "base64 0.13.1", + "blake2b_simd", + "ed25519-dalek", + "failure", + "lazy_static", + "rand 0.6.5", + "serde", + "url", +] + +[[package]] +name = "hpos-config-seed-bundle-explorer" +version = "0.2.1" +source = "git+https://github.com/holo-host/hpos-config.git?rev=77d740c83a02e322e670e360eb450076b593b328#77d740c83a02e322e670e360eb450076b593b328" +dependencies = [ + "base36", + "base64 0.13.1", + "ed25519-dalek", + "hc_seed_bundle", + "hpos-config-core", + "one_err", + "rmp-serde", + "serde_json", + "sodoken", + "thiserror 1.0.69", +] + [[package]] name = "hpos-hal" version = "0.1.0" @@ -1279,7 +1595,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "autocfg", + "autocfg 1.4.0", "hashbrown 0.12.3", "serde", ] @@ -1335,12 +1651,85 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +[[package]] +name = "libflate" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" +dependencies = [ + "adler32", + "core2", + "crc32fast", + "dary_heap", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" +dependencies = [ + "core2", + "hashbrown 0.14.5", + "rle-decode-fast", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.8.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsodium-sys-stable" +version = "1.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7717550bb3ec725f7b312848902d1534f332379b1d575d2347ec265c8814566" +dependencies = [ + "cc", + "libc", + "libflate", + "minisign-verify", + "pkg-config", + "tar", + "ureq", + "vcpkg", + "zip", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1365,10 +1754,16 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ - "autocfg", + "autocfg 1.4.0", "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.25" @@ -1454,6 +1849,12 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minisign-verify" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6367d84fb54d4242af283086402907277715b8fe46976963af5ebf173f8efba3" + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -1470,7 +1871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1501,7 +1902,7 @@ dependencies = [ "once_cell", "pbkdf2", "percent-encoding", - "rand", + "rand 0.8.5", "rustc_version_runtime", "rustls 0.21.12", "rustls-pemfile 1.0.4", @@ -1557,19 +1958,35 @@ dependencies = [ "data-encoding", "ed25519", "ed25519-dalek", - "getrandom", + "getrandom 0.2.15", "log", - "rand", + "rand 0.8.5", "signatory", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nuid" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "rand", + "num-integer", + "num-traits", ] [[package]] @@ -1578,13 +1995,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg", + "autocfg 1.4.0", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -1602,6 +2038,18 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "one_err" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e81851974d8bb6cc9a643cca68afdce7f0a3b80e08a4620388836bb99a680554" +dependencies = [ + "indexmap 1.9.3", + "libc", + "serde", + "serde_json", +] + [[package]] name = "openssl-probe" version = "0.1.6" @@ -1614,6 +2062,7 @@ version = "0.0.1" dependencies = [ "anyhow", "async-nats", + "authentication", "bson", "bytes", "chrono", @@ -1623,7 +2072,7 @@ dependencies = [ "log", "mongodb", "nkeys", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror 2.0.11", @@ -1662,6 +2111,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -1671,6 +2126,16 @@ dependencies = [ "digest", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1728,6 +2193,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "portable-atomic" version = "1.10.0" @@ -1831,6 +2302,38 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + [[package]] name = "rand" version = "0.8.5" @@ -1838,8 +2341,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -1849,7 +2372,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -1858,7 +2405,87 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -1917,13 +2544,53 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "rmpv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" +dependencies = [ + "num-traits", + "rmp", + "serde", + "serde_bytes", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2286,7 +2953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" dependencies = [ "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "zeroize", ] @@ -2298,7 +2965,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.11", + "time", ] [[package]] @@ -2307,7 +2992,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.4.0", ] [[package]] @@ -2326,6 +3011,21 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "sodoken" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907e0ea9699b846c2586ea5685e9abf5963fca64a5179a406e6ac02b94564e30" +dependencies = [ + "libc", + "libsodium-sys-stable", + "num_cpus", + "once_cell", + "one_err", + "parking_lot", + "tokio", +] + [[package]] name = "spin" version = "0.9.8" @@ -2399,6 +3099,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -2422,6 +3134,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.15.0" @@ -2430,7 +3153,7 @@ checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", - "getrandom", + "getrandom 0.2.15", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2453,6 +3176,16 @@ dependencies = [ "touch", ] +[[package]] +name = "textnonce" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f8d70cd784ed1dc33106a18998d77758d281dc40dc3e6d050cf0f5286683" +dependencies = [ + "base64 0.12.3", + "rand 0.7.3", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2600,7 +3333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" dependencies = [ "pin-project", - "rand", + "rand 0.8.5", "tokio", ] @@ -2650,7 +3383,7 @@ dependencies = [ "futures-sink", "http", "httparse", - "rand", + "rand 0.8.5", "ring", "rustls-native-certs 0.8.1", "rustls-pki-types", @@ -2772,6 +3505,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "log", + "once_cell", + "url", +] + [[package]] name = "url" version = "2.5.4" @@ -2838,10 +3583,16 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "serde", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2857,6 +3608,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3138,7 +3895,7 @@ dependencies = [ "log", "mongodb", "nkeys", - "rand", + "rand 0.8.5", "semver", "serde", "serde_json", @@ -3169,6 +3926,17 @@ dependencies = [ "tap", ] +[[package]] +name = "xattr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yoke" version = "0.7.5" @@ -3190,7 +3958,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.96", - "synstructure", + "synstructure 0.13.1", ] [[package]] @@ -3232,7 +4000,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.96", - "synstructure", + "synstructure 0.13.1", ] [[package]] @@ -3262,3 +4030,34 @@ dependencies = [ "quote", "syn 2.0.96", ] + +[[package]] +name = "zip" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.7.1", + "memchr", + "thiserror 2.0.11", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 4af3e41..015b493 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "rust/hpos-hal", "rust/clients/host_agent", "rust/clients/orchestrator", + "rust/services/authentication", "rust/services/workload", "rust/util_libs", ] diff --git a/rust/clients/host_agent/Cargo.toml b/rust/clients/host_agent/Cargo.toml index 5ed82ab..32a1b08 100644 --- a/rust/clients/host_agent/Cargo.toml +++ b/rust/clients/host_agent/Cargo.toml @@ -14,15 +14,24 @@ log = { workspace = true } dotenv = { workspace = true } clap = { workspace = true } thiserror = { workspace = true } +env_logger = { workspace = true } url = { version = "2", features = ["serde"] } bson = { version = "2.6.1", features = ["chrono-0_4"] } -env_logger = { workspace = true } -mongodb = "3.1" +ed25519-dalek = { version = "2.1.1" } +nkeys = "=0.4.4" +sha2 = "=0.10.8" +nats-jwt = "0.3.0" +data-encoding = "2.7.0" +jsonwebtoken = "9.3.0" +textnonce = "1.0.0" chrono = "0.4.0" +mongodb = "3.1" bytes = "1.8.0" -nkeys = "=0.4.4" rand = "0.8.5" +tempfile = "3.15.0" util_libs = { path = "../../util_libs" } workload = { path = "../../services/workload" } +authentication = { path = "../../services/authentication" } hpos-hal = { path = "../../hpos-hal" } -tempfile = "3.15.0" +hpos-config-core = { git = "https://github.com/holo-host/hpos-config.git", rev = "77d740c83a02e322e670e360eb450076b593b328" } +hpos-config-seed-bundle-explorer = { git = "https://github.com/holo-host/hpos-config.git", rev = "77d740c83a02e322e670e360eb450076b593b328" } diff --git a/rust/clients/host_agent/src/agent_cli.rs b/rust/clients/host_agent/src/agent_cli.rs index ed786ac..cdc8fe0 100644 --- a/rust/clients/host_agent/src/agent_cli.rs +++ b/rust/clients/host_agent/src/agent_cli.rs @@ -37,6 +37,9 @@ pub struct DaemonzeArgs { #[arg(long, help = "directory to contain the NATS persistence")] pub(crate) store_dir: Option, + #[arg(help = "path to NATS credentials used for the LeafNode SYS user management")] + pub(crate) nats_leafnode_client_sys_creds_path: Option, + #[arg( long, help = "path to NATS credentials used for the LeafNode client connection" diff --git a/rust/clients/host_agent/src/auth/config.rs b/rust/clients/host_agent/src/auth/config.rs new file mode 100644 index 0000000..15d8a1d --- /dev/null +++ b/rust/clients/host_agent/src/auth/config.rs @@ -0,0 +1,60 @@ +use anyhow::{anyhow, Context, Result}; +use ed25519_dalek::*; +use hpos_config_core::public_key; +use hpos_config_core::Config; +use hpos_config_seed_bundle_explorer::unlock; +use std::env; +use std::fs::File; + +pub struct HosterConfig { + pub email: String, + #[allow(dead_code)] + keypair: SigningKey, + pub hc_pubkey: String, + #[allow(dead_code)] + pub holoport_id: String, +} + +impl HosterConfig { + pub async fn new() -> Result { + let (keypair, email) = get_from_config().await?; + let hc_pubkey = public_key::to_holochain_encoded_agent_key(&keypair.verifying_key()); + let holoport_id = public_key::to_base36_id(&keypair.verifying_key()); + + Ok(Self { + email, + keypair, + hc_pubkey, + holoport_id, + }) + } +} + +async fn get_from_config() -> Result<(SigningKey, String)> { + let config_path = + env::var("HPOS_CONFIG_PATH").context("Cannot read HPOS_CONFIG_PATH from env var")?; + + let password = env::var("DEVICE_SEED_DEFAULT_PASSWORD") + .context("Cannot read bundle password from env var")?; + + let config_file = + File::open(&config_path).context(format!("Failed to open config file {}", config_path))?; + + match serde_json::from_reader(config_file)? { + Config::V2 { + device_bundle, + settings, + .. + } => { + // take in password + let signing_key = unlock(&device_bundle, Some(password)) + .await + .context(format!( + "unable to unlock the device bundle from {}", + &config_path + ))?; + Ok((signing_key, settings.admin.email)) + } + _ => Err(anyhow!("Unsupported version of hpos config")), + } +} diff --git a/rust/clients/host_agent/src/auth/init.rs b/rust/clients/host_agent/src/auth/init.rs new file mode 100644 index 0000000..b566d52 --- /dev/null +++ b/rust/clients/host_agent/src/auth/init.rs @@ -0,0 +1,177 @@ +/* +This client is associated with the: + - ADMIN account + - auth guard user + +Nb: Once the host and hoster are validated, and the host creds file is created, +...this client should close and the hostd workload manager should spin up. + +This client is responsible for: + - generating new key for host / and accessing hoster key from provided config file + - registering with the host auth service to: + - get hub operator jwt and hub sys account jwt + - send "nkey" version of host pubkey as file to hub + - get user jwt from hub and create user creds file with provided file path + - publishing to `auth.start` to initilize the auth handshake and validate the host/hoster + - returning the host pubkey and closing client cleanly +*/ + +use super::utils::json_to_base64; +use crate::{ + auth::config::HosterConfig, + keys::{AuthCredType, Keys}, +}; +use anyhow::Result; +use async_nats::{HeaderMap, HeaderName, HeaderValue}; +use authentication::types::{AuthApiResult, AuthGuardPayload, AuthJWTPayload, AuthResult}; // , AUTH_SRV_DESC, AUTH_SRV_NAME, AUTH_SRV_SUBJ, AUTH_SRV_VERSION +use futures::StreamExt; +use hpos_hal::inventory::HoloInventory; +use std::str::FromStr; +use textnonce::TextNonce; +use util_libs::nats_js_client; + +// pub const HOST_AUTH_CLIENT_NAME: &str = "Host Auth"; +pub const HOST_AUTH_CLIENT_INBOX_PREFIX: &str = "_AUTH_INBOX"; + +pub async fn run( + mut host_agent_keys: Keys, +) -> Result<(Keys, async_nats::Client), async_nats::Error> { + log::info!("Host Auth Client: Connecting to server..."); + + // ==================== Fetch Config File & Call NATS AuthCallout Service to Authenticate Host & Hoster ============================================= + let nonce = TextNonce::new().to_string(); + let unique_inbox = &format!( + "{}.{}", + HOST_AUTH_CLIENT_INBOX_PREFIX, host_agent_keys.host_pubkey + ); + println!(">>> unique_inbox : {}", unique_inbox); + let user_unique_auth_subject = &format!("AUTH.{}.>", host_agent_keys.host_pubkey); + println!( + ">>> user_unique_auth_subject : {}", + user_unique_auth_subject + ); + + // Fetch Hoster Pubkey and email (from config) + let mut auth_guard_payload = AuthGuardPayload::default(); + match HosterConfig::new().await { + Ok(config) => { + auth_guard_payload.host_pubkey = host_agent_keys.host_pubkey.to_string(); + auth_guard_payload.hoster_hc_pubkey = Some(config.hc_pubkey); + auth_guard_payload.email = Some(config.email); + auth_guard_payload.nonce = nonce; + } + Err(e) => { + log::error!("Failed to locate Hoster config. Err={e}"); + auth_guard_payload.host_pubkey = host_agent_keys.host_pubkey.to_string(); + auth_guard_payload.nonce = nonce; + } + }; + auth_guard_payload = auth_guard_payload.try_add_signature(|p| host_agent_keys.host_sign(p))?; + + let user_auth_json = serde_json::to_string(&auth_guard_payload)?; + let user_auth_token = json_to_base64(&user_auth_json)?; + let user_creds = if let AuthCredType::Guard(creds) = host_agent_keys.creds.clone() { + creds + } else { + return Err(async_nats::Error::from( + "Failed to locate Auth Guard credentials", + )); + }; + + // Connect to Nats server as auth guard and call NATS AuthCallout + let nats_url = nats_js_client::get_nats_url(); + let auth_guard_client = + async_nats::ConnectOptions::with_credentials(&user_creds.to_string_lossy())? + .token(user_auth_token) + .custom_inbox_prefix(unique_inbox.to_string()) + .connect(nats_url) + .await?; + + println!( + "User connected to server on port {}. Connection State: {:#?}", + auth_guard_client.server_info().port, + auth_guard_client.connection_state() + ); + + let server_node_id = auth_guard_client.server_info().server_id; + log::trace!("Host Auth Client: Retrieved Node ID: {}", server_node_id); + + // ==================== Handle Host User and SYS Authoriation ============================================================ + let auth_guard_client_clone = auth_guard_client.clone(); + tokio::spawn({ + let mut auth_inbox_msgs = auth_guard_client_clone + .subscribe(unique_inbox.to_string()) + .await?; + async move { + while let Some(msg) = auth_inbox_msgs.next().await { + println!("got an AUTH INBOX msg: {:?}", &msg); + } + } + }); + + let payload = AuthJWTPayload { + host_pubkey: host_agent_keys.host_pubkey.to_string(), + maybe_sys_pubkey: host_agent_keys.local_sys_pubkey.clone(), + nonce: TextNonce::new().to_string(), + }; + + let payload_bytes = serde_json::to_vec(&payload)?; + let signature = host_agent_keys.host_sign(&payload_bytes)?; + let mut headers = HeaderMap::new(); + headers.insert( + HeaderName::from_static("X-Signature"), + HeaderValue::from_str(&format!("{:?}", signature.as_bytes()))?, + ); + + println!("About to send out the {} message", user_unique_auth_subject); + let response = auth_guard_client + .request_with_headers( + user_unique_auth_subject.to_string(), + headers, + payload_bytes.into(), + ) + .await?; + + println!( + "got an AUTH response: {:?}", + std::str::from_utf8(&response.payload).expect("failed to deserialize msg response") + ); + + match serde_json::from_slice::(&response.payload) { + Ok(auth_response) => match auth_response.result { + AuthResult::Authorization(r) => { + host_agent_keys = host_agent_keys + .save_host_creds(r.host_jwt, r.sys_jwt) + .await?; + + if let Some(_reply) = response.reply { + // Publish the Awk resp to the Orchestrator... (JS) + } + } + _ => { + log::error!("got unexpected AUTH RESPONSE : {:?}", auth_response); + } + }, + Err(e) => { + // TODO: Check to see if error is due to auth error.. if so then try to publish to Diagnostics Subject to ensure has correct permissions + println!("got an AUTH RES ERROR: {:?}", e); + + let unauthenticated_user_diagnostics_subject = format!( + "DIAGNOSTICS.unauthenticated.{}", + host_agent_keys.host_pubkey + ); + let diganostics = HoloInventory::from_host(); + let payload_bytes = serde_json::to_vec(&diganostics)?; + auth_guard_client + .publish( + unauthenticated_user_diagnostics_subject, + payload_bytes.into(), + ) + .await?; + } + }; + + log::trace!("host_agent_keys: {:#?}", host_agent_keys); + + Ok((host_agent_keys, auth_guard_client)) +} diff --git a/rust/clients/host_agent/src/auth/mod.rs b/rust/clients/host_agent/src/auth/mod.rs new file mode 100644 index 0000000..2bc32b9 --- /dev/null +++ b/rust/clients/host_agent/src/auth/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod init; +pub mod utils; diff --git a/rust/clients/host_agent/src/auth/utils.rs b/rust/clients/host_agent/src/auth/utils.rs new file mode 100644 index 0000000..cef4ffe --- /dev/null +++ b/rust/clients/host_agent/src/auth/utils.rs @@ -0,0 +1,9 @@ +use data_encoding::BASE64URL_NOPAD; + +/// Encode a json string into a b64 string +pub fn json_to_base64(json_data: &str) -> Result { + let parsed_json: serde_json::Value = serde_json::from_str(json_data)?; + let json_string = serde_json::to_string(&parsed_json)?; + let encoded = BASE64URL_NOPAD.encode(json_string.as_bytes()); + Ok(encoded) +} diff --git a/rust/clients/host_agent/src/hostd/mod.rs b/rust/clients/host_agent/src/hostd/mod.rs index 6a971dc..21ed86c 100644 --- a/rust/clients/host_agent/src/hostd/mod.rs +++ b/rust/clients/host_agent/src/hostd/mod.rs @@ -1,2 +1,2 @@ -pub mod workload_manager; pub mod gen_leaf_server; +pub mod workloads; diff --git a/rust/clients/host_agent/src/hostd/workload_manager.rs b/rust/clients/host_agent/src/hostd/workloads.rs similarity index 81% rename from rust/clients/host_agent/src/hostd/workload_manager.rs rename to rust/clients/host_agent/src/hostd/workloads.rs index 0d9fe39..a9f1760 100644 --- a/rust/clients/host_agent/src/hostd/workload_manager.rs +++ b/rust/clients/host_agent/src/hostd/workloads.rs @@ -15,17 +15,18 @@ use async_nats::Message; use std::{path::PathBuf, sync::Arc, time::Duration}; use util_libs::{ js_stream_service::JsServiceParamsPartial, - nats_js_client::{self, EndpointType}, + nats_js_client::{self, Credentials, EndpointType}, }; use workload::{ - WorkloadServiceApi, host_api::HostWorkloadApi, WORKLOAD_SRV_DESC, WORKLOAD_SRV_NAME, WORKLOAD_SRV_SUBJ, WORKLOAD_SRV_VERSION, - types::{WorkloadServiceSubjects, WorkloadApiResult} + host_api::HostWorkloadApi, + types::{WorkloadApiResult, WorkloadServiceSubjects}, + WorkloadServiceApi, WORKLOAD_SRV_DESC, WORKLOAD_SRV_NAME, WORKLOAD_SRV_SUBJ, + WORKLOAD_SRV_VERSION, }; const HOST_AGENT_CLIENT_NAME: &str = "Host Agent"; -const HOST_AGENT_INBOX_PREFIX: &str = "_host_inbox"; +const HOST_AGENT_INBOX_PREFIX: &str = "_workload_inbox"; -// TODO: Use _host_creds_path for auth once we add in the more resilient auth pattern. pub async fn run( host_pubkey: &str, host_creds_path: &Option, @@ -53,6 +54,8 @@ pub async fn run( // Spin up Nats Client and loaded in the Js Stream Service // Nats takes a moment to become responsive, so we try to connect in a loop for a few seconds. // TODO: how do we recover from a connection loss to Nats in case it crashes or something else? + let creds = host_creds_path.to_owned().map(Credentials::Path); + let host_workload_client = tokio::select! { client = async {loop { let host_workload_client = nats_js_client::JsClient::new(nats_js_client::NewJsClientParams { @@ -60,12 +63,10 @@ pub async fn run( name: HOST_AGENT_CLIENT_NAME.to_string(), inbox_prefix: format!("{}_{}", HOST_AGENT_INBOX_PREFIX, host_pubkey), service_params: vec![workload_stream_service_params.clone()], - credentials_path: host_creds_path - .as_ref() - .map(|path| path.to_string_lossy().to_string()), - opts: vec![nats_js_client::with_event_listeners(event_listeners.clone())], + credentials: creds.clone(), ping_interval: Some(Duration::from_secs(10)), request_timeout: Some(Duration::from_secs(29)), + listeners: vec![nats_js_client::with_event_listeners(event_listeners.clone())], }) .await .map_err(|e| anyhow::anyhow!("connecting to NATS via {nats_url}: {e}")); @@ -90,13 +91,14 @@ pub async fn run( // ==================== Setup API & Register Endpoints ==================== // Instantiate the Workload API let workload_api = HostWorkloadApi::default(); - + // Register Workload Streams for Host Agent to consume and process // NB: Subjects are published by orchestrator let workload_start_subject = serde_json::to_string(&WorkloadServiceSubjects::Start)?; let workload_send_status_subject = serde_json::to_string(&WorkloadServiceSubjects::SendStatus)?; let workload_uninstall_subject = serde_json::to_string(&WorkloadServiceSubjects::Uninstall)?; - let workload_update_installed_subject = serde_json::to_string(&WorkloadServiceSubjects::UpdateInstalled)?; + let workload_update_installed_subject = + serde_json::to_string(&WorkloadServiceSubjects::UpdateInstalled)?; let workload_service = host_workload_client .get_js_service(WORKLOAD_SRV_NAME.to_string()) @@ -107,12 +109,12 @@ pub async fn run( workload_service .add_consumer::( - "start_workload", // consumer name + "start_workload", // consumer name &format!("{}.{}", host_pubkey, workload_start_subject), // consumer stream subj EndpointType::Async( workload_api.call(|api: HostWorkloadApi, msg: Arc| async move { api.start_workload(msg).await - }) + }), ), None, ) @@ -125,7 +127,7 @@ pub async fn run( EndpointType::Async( workload_api.call(|api: HostWorkloadApi, msg: Arc| async move { api.update_workload(msg).await - }) + }), ), None, ) @@ -133,26 +135,26 @@ pub async fn run( workload_service .add_consumer::( - "uninstall_workload", // consumer name + "uninstall_workload", // consumer name &format!("{}.{}", host_pubkey, workload_uninstall_subject), // consumer stream subj - EndpointType::Async( - workload_api.call(|api: HostWorkloadApi, msg: Arc| async move { + EndpointType::Async(workload_api.call( + |api: HostWorkloadApi, msg: Arc| async move { api.uninstall_workload(msg).await - }), - ), + }, + )), None, ) .await?; workload_service .add_consumer::( - "send_workload_status", // consumer name + "send_workload_status", // consumer name &format!("{}.{}", host_pubkey, workload_send_status_subject), // consumer stream subj - EndpointType::Async( - workload_api.call(|api: HostWorkloadApi, msg: Arc| async move { + EndpointType::Async(workload_api.call( + |api: HostWorkloadApi, msg: Arc| async move { api.send_workload_status(msg).await - }) - ), + }, + )), None, ) .await?; diff --git a/rust/clients/host_agent/src/keys.rs b/rust/clients/host_agent/src/keys.rs new file mode 100644 index 0000000..b92dc41 --- /dev/null +++ b/rust/clients/host_agent/src/keys.rs @@ -0,0 +1,303 @@ +use anyhow::{anyhow, Context, Result}; +use data_encoding::BASE64URL_NOPAD; +use nkeys::KeyPair; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; +use util_libs::nats_js_client::{get_path_buf_from_current_dir, get_nats_creds_by_nsc}; + +impl std::fmt::Debug for Keys { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let creds_type = match self.creds { + AuthCredType::Guard(_) => "Guard", + AuthCredType::Authenticated(_) => "Authenticated", + }; + f.debug_struct("Keys") + .field("host_keypair", &"[redacted]") + .field("host_pubkey", &self.host_pubkey) + .field( + "local_sys_keypair", + if self.local_sys_keypair.is_some() { + &"[redacted]" + } else { + &false + }, + ) + .field("local_sys_pubkey", &self.local_sys_pubkey) + .field("creds", &creds_type) + .finish() + } +} + +#[derive(Clone)] +pub struct CredPaths { + host_creds_path: PathBuf, + #[allow(dead_code)] + sys_creds_path: Option, +} + +#[derive(Clone)] +pub enum AuthCredType { + Guard(PathBuf), // Default + Authenticated(CredPaths), // only assiged after successful hoster authentication +} + +#[derive(Clone)] +pub struct Keys { + host_keypair: KeyPair, + pub host_pubkey: String, + local_sys_keypair: Option, + pub local_sys_pubkey: Option, + pub creds: AuthCredType, +} + +impl Keys { + pub fn new() -> Result { + let host_key_path = + std::env::var("HOST_NKEY_PATH").context("Cannot read HOST_NKEY_PATH from env var")?; + let host_kp = KeyPair::new_user(); + write_keypair_to_file(PathBuf::from_str(&host_key_path)?, host_kp.clone())?; + let host_pk = host_kp.public_key(); + + let sys_key_path = + std::env::var("SYS_NKEY_PATH").context("Cannot read SYS_NKEY_PATH from env var")?; + let local_sys_kp = KeyPair::new_user(); + write_keypair_to_file(PathBuf::from_str(&sys_key_path)?, local_sys_kp.clone())?; + let local_sys_pk = local_sys_kp.public_key(); + + let auth_guard_creds = PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "AUTH", "auth_guard"))?; + + Ok(Self { + host_keypair: host_kp, + host_pubkey: host_pk, + local_sys_keypair: Some(local_sys_kp), + local_sys_pubkey: Some(local_sys_pk), + creds: AuthCredType::Guard(auth_guard_creds), + }) + } + + // NB: Only call when trying to load an already authenticated Host and Sys User + pub fn try_from_storage( + maybe_host_creds_path: &Option, + maybe_sys_creds_path: &Option, + ) -> Result { + let host_key_path = + std::env::var("HOST_NKEY_PATH").context("Cannot read HOST_NKEY_PATH from env var")?; + let host_keypair = + try_read_keypair_from_file(PathBuf::from_str(&host_key_path.clone())?)? + .ok_or_else(|| anyhow!("Host keypair not found at path {:?}", host_key_path))?; + let host_pk = host_keypair.public_key(); + let sys_key_path = + std::env::var("SYS_NKEY_PATH").context("Cannot read SYS_NKEY_PATH from env var")?; + let host_creds_path = maybe_host_creds_path + .to_owned() + .map_or_else(|| PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "HPOS", "host")), Ok)?; + let sys_creds_path = maybe_sys_creds_path + .to_owned() + .map_or_else(|| PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "HPOS", "sys")), Ok)?; + + // Set auth_guard_creds as default: + let auth_guard_creds = + PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "AUTH", "auth_guard"))?; + + let keys = match try_read_keypair_from_file(PathBuf::from_str(&sys_key_path)?)? { + Some(kp) => { + let local_sys_pk = kp.public_key(); + Self { + host_keypair, + host_pubkey: host_pk, + local_sys_keypair: Some(kp), + local_sys_pubkey: Some(local_sys_pk), + creds: AuthCredType::Guard(auth_guard_creds), + } + } + None => Self { + host_keypair, + host_pubkey: host_pk, + local_sys_keypair: None, + local_sys_pubkey: None, + creds: AuthCredType::Guard(auth_guard_creds), + }, + }; + + Ok(keys.clone().add_creds_paths(host_creds_path, Some(sys_creds_path)).unwrap_or_else(move |e| { + log::error!("Error: Cannot locate authenticated cred files. Defaulting to auth_guard_creds. Err={}",e); + keys + })) + } + + pub fn _add_local_sys(mut self, sys_key_path: Option) -> Result { + let sys_key_path = sys_key_path + .map_or_else(|| PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "HPOS", "sys")), Ok)?; + + let mut is_new_key = false; + + let local_sys_kp = try_read_keypair_from_file(sys_key_path.clone())?.unwrap_or_else(|| { + is_new_key = true; + KeyPair::new_user() + }); + + if is_new_key { + write_keypair_to_file(sys_key_path, local_sys_kp.clone())?; + } + + let local_sys_pk = local_sys_kp.public_key(); + + self.local_sys_keypair = Some(local_sys_kp); + self.local_sys_pubkey = Some(local_sys_pk); + + Ok(self) + } + + pub fn add_creds_paths( + mut self, + host_creds_file_path: PathBuf, + sys_creds_file_path: Option, + ) -> Result { + match host_creds_file_path.try_exists() { + Ok(is_ok) => { + if !is_ok { + return Err(anyhow!( + "Failed to locate host creds path. Found broken sym link. Path={:?}", + host_creds_file_path + )); + } + + let creds = match sys_creds_file_path { + Some(sys_path) => match sys_path.try_exists() { + Ok(is_ok) => { + if !is_ok { + return Err(anyhow!("Failed to locate sys creds path. Found broken sym link. Path={:?}", sys_path)); + } + CredPaths { + host_creds_path: host_creds_file_path, + sys_creds_path: Some(sys_path), + } + } + Err(e) => { + return Err(anyhow!( + "Failed to locate sys creds path. Path={:?} Err={}", + sys_path, + e + )); + } + }, + None => CredPaths { + host_creds_path: host_creds_file_path, + sys_creds_path: None, + }, + }; + self.creds = AuthCredType::Authenticated(creds); + Ok(self) + } + Err(e) => Err(anyhow!( + "Failed to locate host creds path. Path={:?} Err={}", + host_creds_file_path, + e + )), + } + } + + pub async fn save_host_creds( + &self, + host_user_jwt: String, + host_sys_user_jwt: String, + ) -> Result { + // Save user jwt and sys jwt local to hosting agent + let host_path = PathBuf::from_str(&format!("{}.{}", "output_dir", "host.jwt"))?; + write_to_file(host_path, host_user_jwt.as_bytes())?; + let sys_path = PathBuf::from_str(&format!("{}.{}", "output_dir", "host_sys.jwt"))?; + write_to_file(sys_path, host_sys_user_jwt.as_bytes())?; + + // Save user creds and sys creds local to hosting agent + let host_creds_file_name = "host.creds"; + Command::new("nsc") + .arg(format!( + "generate creds --name user_host_{} --account {} > {}", + self.host_pubkey, "WORKLOAD", host_creds_file_name + )) + .output() + .context("Failed to add new operator signing key on hosting agent")?; + + let mut sys_creds_file_name = None; + if let Some(sys_pubkey) = self.local_sys_pubkey.as_ref() { + let file_name = "host_sys.creds"; + sys_creds_file_name = Some(get_path_buf_from_current_dir(file_name)); + Command::new("nsc") + .arg(format!( + "generate creds --name user_host_{} --account {} > {}", + sys_pubkey, "SYS", file_name + )) + .output() + .context("Failed to add new operator signing key on hosting agent")?; + } + + self.to_owned() + .add_creds_paths(get_path_buf_from_current_dir(host_creds_file_name), sys_creds_file_name) + } + + pub fn get_host_creds_path(&self) -> Option { + if let AuthCredType::Authenticated(creds) = self.to_owned().creds { + return Some(creds.host_creds_path); + }; + None + } + + pub fn _get_sys_creds_path(&self) -> Option { + if let AuthCredType::Authenticated(creds) = self.to_owned().creds { + return creds.sys_creds_path; + }; + None + } + + pub fn host_sign(&self, payload: &[u8]) -> Result { + let signature = self.host_keypair.sign(payload)?; + + Ok(BASE64URL_NOPAD.encode(&signature)) + } +} + +fn write_keypair_to_file(key_file_path: PathBuf, keypair: KeyPair) -> Result<()> { + let seed = keypair.seed()?; + write_to_file(key_file_path, seed.as_bytes()) +} + +fn write_to_file(file_path: PathBuf, data: &[u8]) -> Result<()> { + // TODO: ensure dirs already exist and create them if not... + let mut file = File::create(&file_path)?; + file.write_all(data)?; + Ok(()) +} + +fn try_read_keypair_from_file(key_file_path: PathBuf) -> Result> { + match try_read_from_file(key_file_path)? { + Some(kps) => Ok(Some(KeyPair::from_seed(&kps)?)), + None => Ok(None), + } +} + +fn try_read_from_file(file_path: PathBuf) -> Result> { + match file_path.try_exists() { + Ok(link_is_ok) => { + if !link_is_ok { + return Err(anyhow!( + "Failed to read path {:?}. Found broken sym link.", + file_path + )); + } + + let mut file_content = File::open(&file_path) + .context(format!("Failed to open config file {:#?}", file_path))?; + + let mut s = String::new(); + file_content.read_to_string(&mut s)?; + Ok(Some(s.trim().to_string())) + } + Err(_) => { + log::debug!("No user file found at {:?}.", file_path); + Ok(None) + } + } +} diff --git a/rust/clients/host_agent/src/main.rs b/rust/clients/host_agent/src/main.rs index 1e107fc..e642d9d 100644 --- a/rust/clients/host_agent/src/main.rs +++ b/rust/clients/host_agent/src/main.rs @@ -10,14 +10,17 @@ This client is responsible for subscribing the host agent to workload stream end - sending workload status upon request */ -mod hostd; pub mod agent_cli; +mod auth; pub mod host_cmds; +mod hostd; +mod keys; pub mod support_cmds; use agent_cli::DaemonzeArgs; use anyhow::Result; use clap::Parser; use dotenv::dotenv; +use hpos_hal::inventory::HoloInventory; use thiserror::Error; #[derive(Error, Debug)] @@ -47,19 +50,34 @@ async fn main() -> Result<(), AgentCliError> { } async fn daemonize(args: &DaemonzeArgs) -> Result<(), async_nats::Error> { - // let host_pubkey = auth::init_agent::run().await?; - - let _ = hostd::gen_leaf_server::run( + let mut host_agent_keys = keys::Keys::try_from_storage( &args.nats_leafnode_client_creds_path, + &args.nats_leafnode_client_sys_creds_path, + ) + .or_else(|_| { + keys::Keys::new().map_err(|e| { + eprintln!("Failed to create new keys: {:?}", e); + async_nats::Error::from(e) + }) + })?; + + // If user cred file is for the auth_guard user, run loop to authenticate host & hoster... + if let keys::AuthCredType::Guard(_) = host_agent_keys.creds { + host_agent_keys = run_auth_loop(host_agent_keys).await?; + } + + // Once authenticated, start leaf server and run workload api calls. + let _ = hostd::gen_leaf_server::run( + &host_agent_keys.get_host_creds_path(), &args.store_dir, args.hub_url.clone(), args.hub_tls_insecure, ) .await; - let host_workload_client = hostd::workload_manager::run( - "host_id_placeholder>", - &args.nats_leafnode_client_creds_path, + let host_workload_client = hostd::workloads::run( + &host_agent_keys.host_pubkey, + &host_agent_keys.get_host_creds_path(), args.nats_connect_timeout_secs, ) .await?; @@ -72,3 +90,47 @@ async fn daemonize(args: &DaemonzeArgs) -> Result<(), async_nats::Error> { Ok(()) } + +async fn run_auth_loop(mut keys: keys::Keys) -> Result { + let mut start = chrono::Utc::now(); + loop { + log::debug!("About to run the Hosting Agent Authentication Service"); + let auth_guard_client: async_nats::Client; + (keys, auth_guard_client) = auth::init::run(keys).await?; + + // If authenicated creds exist, then auth call was successful. + // Close buffer, exit loop, and return. + if let keys::AuthCredType::Authenticated(_) = keys.creds { + auth_guard_client.drain().await?; + break; + } + + // Otherwise, send diagonostics every 1hr for the next 24hrs, then exit while loop and retry auth. + // TODO: Discuss interval for sending diagnostic reports and wait duration before retrying auth with team. + let now = chrono::Utc::now(); + let max_time_interval = chrono::TimeDelta::days(1); + + while max_time_interval > now.signed_duration_since(start) { + let unauthenticated_user_diagnostics_subject = + format!("DIAGNOSTICS.unauthenticated.{}", keys.host_pubkey); + let diganostics = HoloInventory::from_host(); + let payload_bytes = serde_json::to_vec(&diganostics)?; + if let Err(e) = auth_guard_client + .publish( + unauthenticated_user_diagnostics_subject, + payload_bytes.into(), + ) + .await + { + log::error!("Encountered error when sending diganostics. Err={:#?}", e); + }; + tokio::time::sleep(chrono::TimeDelta::hours(1).to_std()?).await; + } + + // Close and drain internal buffer before exiting to make sure all messages are sent. + auth_guard_client.drain().await?; + start = chrono::Utc::now(); + } + + Ok(keys) +} diff --git a/rust/clients/orchestrator/Cargo.toml b/rust/clients/orchestrator/Cargo.toml index ae867cb..3c2ee94 100644 --- a/rust/clients/orchestrator/Cargo.toml +++ b/rust/clients/orchestrator/Cargo.toml @@ -23,3 +23,4 @@ nkeys = "=0.4.4" rand = "0.8.5" util_libs = { path = "../../util_libs" } workload = { path = "../../services/workload" } +authentication = { path = "../../services/authentication" } diff --git a/rust/clients/orchestrator/src/auth.rs b/rust/clients/orchestrator/src/auth.rs new file mode 100644 index 0000000..96e8385 --- /dev/null +++ b/rust/clients/orchestrator/src/auth.rs @@ -0,0 +1,289 @@ +/* +This client is associated with the: + - ADMIN account + - orchestrator user + +This client is responsible for: + - initalizing connection and handling interface with db + - registering with the host auth service to: + - handling auth requests by: + - validating user signature + - validating hoster pubkey + - validating hoster email + - bidirectionally pairing hoster and host + - interfacing with hub nsc resolver and hub credential files + - adding user to hub + - creating signed jwt for user + - adding user jwt file to user collection (with ttl) + - keeping service running until explicitly cancelled out +*/ + +use anyhow::{anyhow, Context, Result}; +use async_nats::Message; +use authentication::{ + self, + types::{self, AuthApiResult}, + AuthServiceApi, AUTH_SRV_DESC, AUTH_SRV_NAME, AUTH_SRV_SUBJ, AUTH_SRV_VERSION, +}; +use mongodb::{options::ClientOptions, Client as MongoDBClient}; +use nkeys::KeyPair; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::str::FromStr; +use util_libs::{ + db::mongodb::get_mongodb_url, + js_stream_service::{JsServiceParamsPartial, ResponseSubjectsGenerator}, + nats_js_client::{ + get_event_listeners, get_nats_url, with_event_listeners, get_nats_creds_by_nsc, Credentials, + EndpointType, JsClient, NewJsClientParams, + }, +}; + +pub const ORCHESTRATOR_AUTH_CLIENT_NAME: &str = "Orchestrator Auth Manager"; +pub const ORCHESTRATOR_AUTH_CLIENT_INBOX_PREFIX: &str = "_auth_inbox_orchestrator"; + +pub async fn run() -> Result<(), async_nats::Error> { + println!("inside auth... 0"); + + // let admin_account_creds_path = PathBuf::from_str("/home/za/Documents/holo-v2/holo-host/rust/clients/orchestrator/src/tmp/test_admin.creds")?; + let admin_account_creds_path = PathBuf::from_str(&get_nats_creds_by_nsc( + "HOLO", + "AUTH", + "auth", + ))?; + println!( + " >>>> admin_account_creds_path: {:#?} ", + admin_account_creds_path + ); + + println!("inside auth... 1"); + + // Root Keypair associated with AUTH account + let root_account_key_path = std::env::var("ROOT_AUTH_NKEY_PATH") + .context("Cannot read ROOT_AUTH_NKEY_PATH from env var")?; + let root_account_keypair = Arc::new( + try_read_keypair_from_file(PathBuf::from_str(&root_account_key_path.clone())?)?.ok_or_else( + || { + anyhow!( + "Root AUTH Account keypair not found at path {:?}", + root_account_key_path + ) + }, + )?, + ); + + println!("inside auth... 2"); + + // TODO: REMOVE + // let root_account_keypair = Arc::new(KeyPair::from_seed( + // "<>", + // )?); + let root_account_pubkey = root_account_keypair.public_key().clone(); + println!("inside auth... 3"); + + // AUTH Account Signing Keypair associated with the `auth` user + let signing_account_key_path = std::env::var("SIGNING_AUTH_NKEY_PATH") + .context("Cannot read SIGNING_AUTH_NKEY_PATH from env var")?; + println!("inside auth... 4"); + + let signing_account_keypair = Arc::new( + try_read_keypair_from_file(PathBuf::from_str(&signing_account_key_path.clone())?)? + .ok_or_else(|| { + anyhow!( + "Signing AUTH Account keypair not found at path {:?}", + signing_account_key_path + ) + })?, + ); + println!("inside auth... 5"); + + // TODO: REMOVE + // let signing_account_keypair = Arc::new(KeyPair::from_seed( + // "<>", + // )?); + let signing_account_pubkey = signing_account_keypair.public_key().clone(); + println!( + ">>>>>>>>> signing_account pubkey: {:?}", + signing_account_pubkey + ); + println!("inside auth... 6"); + + + // ==================== Setup NATS ==================== + // Setup JS Stream Service + let auth_stream_service_params = JsServiceParamsPartial { + name: AUTH_SRV_NAME.to_string(), + description: AUTH_SRV_DESC.to_string(), + version: AUTH_SRV_VERSION.to_string(), + service_subject: AUTH_SRV_SUBJ.to_string(), + }; + println!("inside auth... 7"); + let nats_url = get_nats_url(); + let nats_connect_timeout_secs: u64 = 180; + + let orchestrator_auth_client = JsClient::new(NewJsClientParams { + nats_url: get_nats_url(), + name: ORCHESTRATOR_AUTH_CLIENT_NAME.to_string(), + inbox_prefix: ORCHESTRATOR_AUTH_CLIENT_INBOX_PREFIX.to_string(), + service_params: vec![auth_stream_service_params], + credentials: Some(Credentials::Path(admin_account_creds_path)), + listeners: vec![with_event_listeners(get_event_listeners())], + ping_interval: Some(Duration::from_secs(10)), + request_timeout: Some(Duration::from_secs(5)), + }) + .await?; + + // let orchestrator_auth_client = tokio::select! { + // client = async {loop { + // let orchestrator_auth_client = JsClient::new(NewJsClientParams { + // nats_url: nats_url.clone(), + // name: ORCHESTRATOR_AUTH_CLIENT_NAME.to_string(), + // inbox_prefix: ORCHESTRATOR_AUTH_CLIENT_INBOX_PREFIX.to_string(), + // service_params: vec![auth_stream_service_params.clone()], + // credentials: Some(Credentials::Path(admin_account_creds_path.clone())), + // listeners: vec![with_event_listeners(get_event_listeners())], + // ping_interval: Some(Duration::from_secs(10)), + // request_timeout: Some(Duration::from_secs(5)), + // }) + // .await + // .map_err(|e| anyhow::anyhow!("connecting to NATS via {nats_url}: {e}")); + + // match orchestrator_auth_client { + // Ok(client) => break client, + // Err(e) => { + // let duration = tokio::time::Duration::from_millis(100); + // log::warn!("{}, retrying in {duration:?}", e); + // tokio::time::sleep(duration).await; + // } + // } + // }} => client, + // _ = { + // log::debug!("will time out waiting for NATS after {nats_connect_timeout_secs:?}"); + // tokio::time::sleep(tokio::time::Duration::from_secs(nats_connect_timeout_secs)) + // } => { + // return Err(format!("timed out waiting for NATS on {nats_url}").into()); + // } + // }; + + println!("inside auth... 8"); + + // ==================== Setup DB ==================== + // Create a new MongoDB Client and connect it to the cluster + let mongo_uri = get_mongodb_url(); + let client_options = ClientOptions::parse(mongo_uri).await?; + let client = MongoDBClient::with_options(client_options)?; + println!("inside auth... 9"); + + // ==================== Setup API & Register Endpoints ==================== + // Generate the Auth API with access to db + let auth_api = AuthServiceApi::new(&client).await?; + println!("inside auth... 10"); + + // Register Auth Stream for Orchestrator to consume and process + let auth_service = orchestrator_auth_client + .get_js_service(AUTH_SRV_NAME.to_string()) + .await + .ok_or(anyhow!( + "Failed to locate Auth Service. Unable to spin up Orchestrator Auth Client." + ))?; + println!("inside auth... 11"); + + auth_service + .add_consumer::( + "auth_callout", + types::AUTH_CALLOUT_SUBJECT, // consumer stream subj + EndpointType::Async(auth_api.call({ + move |api: AuthServiceApi, msg: Arc| { + let signing_account_kp = Arc::clone(&signing_account_keypair); + let signing_account_pk = signing_account_pubkey.clone(); + let root_account_kp = Arc::clone(&root_account_keypair); + let root_account_pk = root_account_pubkey.clone(); + + async move { + api.handle_auth_callout( + msg, + signing_account_kp, + signing_account_pk, + root_account_kp, + root_account_pk, + ) + .await + } + } + })), + None, + ) + .await?; + println!("inside auth... 12"); + + auth_service + .add_consumer::( + "authorize_host_and_sys", + types::AUTHORIZE_SUBJECT, // consumer stream subj + EndpointType::Async(auth_api.call( + |api: AuthServiceApi, msg: Arc| async move { + api.handle_handshake_request(msg).await + }, + )), + Some(create_callback_subject_to_host("host_pubkey".to_string())), + ) + .await?; + println!("inside auth... 13"); + + println!("Orchestrator Auth Service is running. Waiting for requests..."); + + // ==================== Close and Clean Client ==================== + // Only exit program when explicitly requested + tokio::signal::ctrl_c().await?; + + println!("inside auth... 14... closing"); + + // Close client and drain internal buffer before exiting to make sure all messages are sent + orchestrator_auth_client.close().await?; + println!("inside auth... 15... closed"); + + Ok(()) +} + +pub fn create_callback_subject_to_host(tag_name: String) -> ResponseSubjectsGenerator { + Arc::new(move |tag_map: HashMap| -> Vec { + if let Some(tag) = tag_map.get(&tag_name) { + return vec![format!("AUTH.{}", tag)]; + } + log::error!("Auth Error: Failed to find {}. Unable to send orchestrator response to hosting agent for subject 'AUTH.validate'. Fwding response to `AUTH.ERROR.INBOX`.", tag_name); + vec!["AUTH.ERROR.INBOX".to_string()] + }) +} + +fn try_read_keypair_from_file(key_file_path: PathBuf) -> Result> { + match try_read_from_file(key_file_path)? { + Some(kps) => Ok(Some(KeyPair::from_seed(&kps)?)), + None => Ok(None), + } +} + +fn try_read_from_file(file_path: PathBuf) -> Result> { + match file_path.try_exists() { + Ok(link_is_ok) => { + if !link_is_ok { + return Err(anyhow!( + "Failed to read path {:?}. Found broken sym link.", + file_path + )); + } + + let mut file_content = File::open(&file_path) + .context(format!("Failed to open config file {:#?}", file_path))?; + + let mut s = String::new(); + file_content.read_to_string(&mut s)?; + Ok(Some(s.trim().to_string())) + } + Err(_) => { + log::debug!("No user file found at {:?}.", file_path); + Ok(None) + } + } +} diff --git a/rust/clients/orchestrator/src/main.rs b/rust/clients/orchestrator/src/main.rs index 8e5aa3e..286e0dc 100644 --- a/rust/clients/orchestrator/src/main.rs +++ b/rust/clients/orchestrator/src/main.rs @@ -1,17 +1,23 @@ -mod workloads; +mod auth; +// mod workloads; use anyhow::Result; use dotenv::dotenv; +use tokio::task::spawn; #[tokio::main] async fn main() -> Result<(), async_nats::Error> { dotenv().ok(); env_logger::init(); - // Run auth service - // TODO: invoke auth service (once ready) + println!("starting auth..."); - // Run workload service - if let Err(e) = workloads::run().await { - log::error!("{}", e) - } + auth::run().await?; + + println!("finished auth..."); + + // spawn(async move { + // if let Err(e) = workloads::run().await { + // log::error!("{}", e) + // } + // }); Ok(()) } diff --git a/rust/clients/orchestrator/src/workloads.rs b/rust/clients/orchestrator/src/workloads.rs index c5bd37e..291eadc 100644 --- a/rust/clients/orchestrator/src/workloads.rs +++ b/rust/clients/orchestrator/src/workloads.rs @@ -15,22 +15,34 @@ This client is responsible for: */ use anyhow::{anyhow, Result}; -use std::{collections::HashMap, sync::Arc, time::Duration}; use async_nats::Message; use mongodb::{options::ClientOptions, Client as MongoDBClient}; -use workload::{ - WorkloadServiceApi, orchestrator_api::OrchestratorWorkloadApi, WORKLOAD_SRV_DESC, WORKLOAD_SRV_NAME, WORKLOAD_SRV_SUBJ, WORKLOAD_SRV_VERSION, types::{WorkloadServiceSubjects, WorkloadApiResult} -}; +use std::path::PathBuf; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::str::FromStr; use util_libs::{ db::mongodb::get_mongodb_url, js_stream_service::{JsServiceParamsPartial, ResponseSubjectsGenerator}, - nats_js_client::{self, EndpointType, JsClient, NewJsClientParams}, + nats_js_client::{ + self, get_event_listeners, get_nats_creds_by_nsc, get_nats_url, + Credentials, EndpointType, JsClient, NewJsClientParams, + }, +}; +use workload::{ + orchestrator_api::OrchestratorWorkloadApi, + types::{WorkloadApiResult, WorkloadServiceSubjects}, + WorkloadServiceApi, WORKLOAD_SRV_DESC, WORKLOAD_SRV_NAME, WORKLOAD_SRV_SUBJ, + WORKLOAD_SRV_VERSION, }; -const ORCHESTRATOR_WORKLOAD_CLIENT_NAME: &str = "Orchestrator Workload Agent"; -const ORCHESTRATOR_WORKLOAD_CLIENT_INBOX_PREFIX: &str = "_orchestrator_workload_inbox"; +const ORCHESTRATOR_WORKLOAD_CLIENT_NAME: &str = "Orchestrator Workload Manager"; +const ORCHESTRATOR_WORKLOAD_CLIENT_INBOX_PREFIX: &str = "_workload_inbox_orchestrator"; -pub fn create_callback_subject_to_host(is_prefix: bool, tag_name: String, sub_subject_name: String) -> ResponseSubjectsGenerator { +pub fn create_callback_subject_to_host( + is_prefix: bool, + tag_name: String, + sub_subject_name: String, +) -> ResponseSubjectsGenerator { Arc::new(move |tag_map: HashMap| -> Vec { if is_prefix { let matching_tags = tag_map.into_iter().fold(vec![], |mut acc, (k, v)| { @@ -50,9 +62,13 @@ pub fn create_callback_subject_to_host(is_prefix: bool, tag_name: String, sub_su pub async fn run() -> Result<(), async_nats::Error> { // ==================== Setup NATS ==================== - let nats_url = nats_js_client::get_nats_url(); - let creds_path = nats_js_client::get_nats_client_creds("HOLO", "WORKLOAD", "orchestrator"); - let event_listeners = nats_js_client::get_event_listeners(); + let nats_url = get_nats_url(); + let creds_path = Credentials::Path(PathBuf::from_str(&get_nats_creds_by_nsc( + "HOLO", + "ADMIN", + "admin", + ))?); + let event_listeners = get_event_listeners(); // Setup JS Stream Service let workload_stream_service_params = JsServiceParamsPartial { @@ -62,25 +78,24 @@ pub async fn run() -> Result<(), async_nats::Error> { service_subject: WORKLOAD_SRV_SUBJ.to_string(), }; - let orchestrator_workload_client = - JsClient::new(NewJsClientParams { - nats_url, - name: ORCHESTRATOR_WORKLOAD_CLIENT_NAME.to_string(), - inbox_prefix: ORCHESTRATOR_WORKLOAD_CLIENT_INBOX_PREFIX.to_string(), - service_params: vec![workload_stream_service_params], - credentials_path: Some(creds_path), - opts: vec![nats_js_client::with_event_listeners(event_listeners)], - ping_interval: Some(Duration::from_secs(10)), - request_timeout: Some(Duration::from_secs(5)), - }) - .await?; + let orchestrator_workload_client = JsClient::new(NewJsClientParams { + nats_url, + name: ORCHESTRATOR_WORKLOAD_CLIENT_NAME.to_string(), + inbox_prefix: ORCHESTRATOR_WORKLOAD_CLIENT_INBOX_PREFIX.to_string(), + service_params: vec![workload_stream_service_params], + credentials: Some(creds_path), + request_timeout: Some(Duration::from_secs(5)), + ping_interval: Some(Duration::from_secs(10)), + listeners: vec![nats_js_client::with_event_listeners(event_listeners)], + }) + .await?; // ==================== Setup DB ==================== // Create a new MongoDB Client and connect it to the cluster let mongo_uri = get_mongodb_url(); let client_options = ClientOptions::parse(mongo_uri).await?; let client = MongoDBClient::with_options(client_options)?; - + // ==================== Setup API & Register Endpoints ==================== // Instantiate the Workload API (requires access to db client) let workload_api = OrchestratorWorkloadApi::new(&client).await?; @@ -92,9 +107,11 @@ pub async fn run() -> Result<(), async_nats::Error> { let workload_remove_subject = serde_json::to_string(&WorkloadServiceSubjects::Remove)?; let workload_db_insert_subject = serde_json::to_string(&WorkloadServiceSubjects::Insert)?; let workload_db_modification_subject = serde_json::to_string(&WorkloadServiceSubjects::Modify)?; - let workload_handle_status_subject = serde_json::to_string(&WorkloadServiceSubjects::HandleStatusUpdate)?; + let workload_handle_status_subject = + serde_json::to_string(&WorkloadServiceSubjects::HandleStatusUpdate)?; let workload_start_subject = serde_json::to_string(&WorkloadServiceSubjects::Start)?; - let workload_update_installed_subject = serde_json::to_string(&WorkloadServiceSubjects::UpdateInstalled)?; + let workload_update_installed_subject = + serde_json::to_string(&WorkloadServiceSubjects::UpdateInstalled)?; let workload_service = orchestrator_workload_client .get_js_service(WORKLOAD_SRV_NAME.to_string()) @@ -106,84 +123,91 @@ pub async fn run() -> Result<(), async_nats::Error> { // Published by Developer workload_service .add_consumer::( - "add_workload", // consumer name - &workload_add_subject, // consumer stream subj - EndpointType::Async(workload_api.call(|api: OrchestratorWorkloadApi, msg: Arc| { - async move { + "add_workload", // consumer name + &workload_add_subject, // consumer stream subj + EndpointType::Async(workload_api.call( + |api: OrchestratorWorkloadApi, msg: Arc| async move { api.add_workload(msg).await - } - })), + }, + )), None, ) .await?; - workload_service + workload_service .add_consumer::( - "update_workload", // consumer name - &workload_update_subject, // consumer stream subj - EndpointType::Async(workload_api.call(|api: OrchestratorWorkloadApi, msg: Arc| { - async move { + "update_workload", // consumer name + &workload_update_subject, // consumer stream subj + EndpointType::Async(workload_api.call( + |api: OrchestratorWorkloadApi, msg: Arc| async move { api.update_workload(msg).await - } - })), + }, + )), None, ) .await?; - workload_service .add_consumer::( - "remove_workload", // consumer name - &workload_remove_subject, // consumer stream subj - EndpointType::Async(workload_api.call(|api: OrchestratorWorkloadApi, msg: Arc| { - async move { + "remove_workload", // consumer name + &workload_remove_subject, // consumer stream subj + EndpointType::Async(workload_api.call( + |api: OrchestratorWorkloadApi, msg: Arc| async move { api.remove_workload(msg).await - } - })), + }, + )), None, ) .await?; - + // Automatically published by the Nats-DB-Connector workload_service .add_consumer::( - "handle_db_insertion", // consumer name - &workload_db_insert_subject, // consumer stream subj - EndpointType::Async(workload_api.call(|api: OrchestratorWorkloadApi, msg: Arc| { - async move { + "handle_db_insertion", // consumer name + &workload_db_insert_subject, // consumer stream subj + EndpointType::Async(workload_api.call( + |api: OrchestratorWorkloadApi, msg: Arc| async move { api.handle_db_insertion(msg).await - } - })), - Some(create_callback_subject_to_host(true, "assigned_hosts".to_string(), workload_start_subject)), + }, + )), + Some(create_callback_subject_to_host( + true, + "assigned_hosts".to_string(), + workload_start_subject, + )), ) .await?; workload_service .add_consumer::( - "handle_db_modification", // consumer name - &workload_db_modification_subject, // consumer stream subj - EndpointType::Async(workload_api.call(|api: OrchestratorWorkloadApi, msg: Arc| { - async move { + "handle_db_modification", // consumer name + &workload_db_modification_subject, // consumer stream subj + EndpointType::Async(workload_api.call( + |api: OrchestratorWorkloadApi, msg: Arc| async move { api.handle_db_modification(msg).await - } - })), - Some(create_callback_subject_to_host(true, "assigned_hosts".to_string(), workload_update_installed_subject)), + }, + )), + Some(create_callback_subject_to_host( + true, + "assigned_hosts".to_string(), + workload_update_installed_subject, + )), ) .await?; // Published by the Host Agent workload_service - .add_consumer::( - "handle_status_update", // consumer name - &workload_handle_status_subject, // consumer stream subj - EndpointType::Async(workload_api.call(|api: OrchestratorWorkloadApi, msg: Arc| { - async move { - api.handle_status_update(msg).await - } - })), - None, - ) - .await?; + .add_consumer::( + "handle_status_update", // consumer name + &workload_handle_status_subject, // consumer stream subj + EndpointType::Async(workload_api.call( + |api: OrchestratorWorkloadApi, msg: Arc| async move { + api.handle_status_update(msg).await + }, + )), + None, + ) + .await?; // ==================== Close and Clean Client ==================== // Only exit program when explicitly requested diff --git a/rust/services/authentication/Cargo.toml b/rust/services/authentication/Cargo.toml new file mode 100644 index 0000000..2bdc024 --- /dev/null +++ b/rust/services/authentication/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "authentication" +version = "0.0.1" +edition = "2021" + +[dependencies] +async-nats = { workspace = true } +anyhow = { workspace = true } +futures = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +env_logger = { workspace = true } +log = { workspace = true } +dotenv = { workspace = true } +thiserror = { workspace = true } +async-trait = "0.1.83" +mongodb = "3.1" +bson = { version = "2.6.1", features = ["chrono-0_4"] } +url = { version = "2", features = ["serde"] } +nkeys = "=0.4.4" +sha2 = "=0.10.8" +nats-jwt = "0.3.0" +data-encoding = "2.7.0" +jsonwebtoken = "9.3.0" +bytes = "1.8.0" +chrono = "0.4.0" +util_libs = { path = "../../util_libs" } \ No newline at end of file diff --git a/rust/services/authentication/src/lib.rs b/rust/services/authentication/src/lib.rs new file mode 100644 index 0000000..86d9b42 --- /dev/null +++ b/rust/services/authentication/src/lib.rs @@ -0,0 +1,456 @@ +/* +Service Name: AUTH +Subject: "AUTH.>" +Provisioning Account: ADMIN Account (ie: This service is exclusively permissioned to the ADMIN account.) +Users: orchestrator & noauth +Endpoints & Managed Subjects: + - handle_handshake_request: AUTH.validate +*/ + +pub mod types; +pub mod utils; +use anyhow::{Context, Result}; +use async_nats::jetstream::ErrorCode; +use async_nats::HeaderValue; +use async_nats::{AuthError, Message}; +use bson::{self, doc, to_document}; +use core::option::Option::None; +use mongodb::{options::UpdateModifications, Client as MongoDBClient}; +use nkeys::KeyPair; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::future::Future; +use std::process::Command; +use std::sync::Arc; +use types::{AuthApiResult, WORKLOAD_SK_ROLE}; +use util_libs::db::{ + mongodb::{IntoIndexes, MongoCollection, MongoDbAPI}, + schemas::{self, Host, Hoster, Role, RoleInfo, User}, +}; +use util_libs::nats_js_client::{AsyncEndpointHandler, JsServiceResponse, ServiceError}; +use utils::handle_internal_err; + +pub const AUTH_SRV_NAME: &str = "AUTH"; +pub const AUTH_SRV_SUBJ: &str = "AUTH"; +pub const AUTH_SRV_VERSION: &str = "0.0.1"; +pub const AUTH_SRV_DESC: &str = + "This service handles the Authentication flow the Host and the Orchestrator."; + +#[derive(Clone, Debug)] +pub struct AuthServiceApi { + pub user_collection: MongoCollection, + pub hoster_collection: MongoCollection, + pub host_collection: MongoCollection, +} + +impl AuthServiceApi { + pub async fn new(client: &MongoDBClient) -> Result { + Ok(Self { + user_collection: Self::init_collection(client, schemas::USER_COLLECTION_NAME).await?, + hoster_collection: Self::init_collection(client, schemas::HOSTER_COLLECTION_NAME) + .await?, + host_collection: Self::init_collection(client, schemas::HOST_COLLECTION_NAME).await?, + }) + } + + pub async fn handle_auth_callout( + &self, + msg: Arc, + auth_signing_account_keypair: Arc, + auth_signing_account_pubkey: String, + auth_root_account_keypair: Arc, + auth_root_account_pubkey: String, + ) -> Result { + // 1. Verify expected data was received + let auth_request_token = String::from_utf8_lossy(&msg.payload).to_string(); + println!("auth_request_token : {:?}", auth_request_token); + + let auth_request_claim = + utils::decode_jwt::(&auth_request_token) + .map_err(|e| ServiceError::Authentication(AuthError::new(e)))?; + println!( + "\nauth REQUEST - main claim : {}", + serde_json::to_string_pretty(&auth_request_claim).unwrap() + ); + + let auth_request_user_claim = utils::decode_jwt::( + &auth_request_claim.auth_request.connect_opts.user_jwt, + ) + .map_err(|e| ServiceError::Authentication(AuthError::new(e)))?; + println!( + "\nauth REQUEST - user claim : {}", + serde_json::to_string_pretty(&auth_request_user_claim).unwrap() + ); + + let user_data: types::AuthGuardPayload = utils::base64_to_data::( + &auth_request_claim.auth_request.connect_opts.user_auth_token, + ) + .map_err(|e| ServiceError::Authentication(AuthError::new(e)))?; + println!("user_data TO VALIDATE : {:#?}", user_data); + + // 2. Validate Host signature, returning validation error if not successful + let host_pubkey = user_data.host_pubkey.as_ref(); + let host_signature = user_data.get_host_signature(); + let user_verifying_keypair = KeyPair::from_public_key(host_pubkey) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + let raw_payload = serde_json::to_vec(&user_data.clone().without_signature()) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + if let Err(e) = user_verifying_keypair.verify(raw_payload.as_ref(), &host_signature) { + log::error!( + "Error: Failed to validate Signature. Subject='{}'. Err={}", + msg.subject, + e + ); + return Err(ServiceError::Authentication(AuthError::new(e))); + }; + + // 3. If provided, authenticate the Hoster pubkey and email and assign full permissions if successful + let is_hoster_valid = if user_data.email.is_some() && user_data.hoster_hc_pubkey.is_some() { + true + // let hoster_hc_pubkey = user_data.hoster_hc_pubkey.unwrap(); // unwrap is safe here as checked above + // let hoster_email = user_data.email.unwrap(); // unwrap is safe here as checked above + + // let is_valid: bool = match self + // .user_collection + // .get_one_from(doc! { "roles.role.Hoster": hoster_hc_pubkey.clone() }) + // .await? + // { + // Some(u) => { + // let mut is_valid = true; + // // If hoster exists with pubkey, verify email + // if u.email != hoster_email { + // log::error!( + // "Error: Failed to validate hoster email. Email='{}'.", + // hoster_email + // ); + // is_valid = false; + // } + + // // ...then find the host collection that contains the provided host pubkey + // match self + // .host_collection + // .get_one_from(doc! { "pubkey": host_pubkey }) + // .await? + // { + // Some(host) => { + // // ...and pair the host with hoster pubkey (if the hoster is not already assiged to host) + // if host.assigned_hoster != hoster_hc_pubkey { + // let host_query: bson::Document = doc! { "_id": host._id.clone() }; + // let updated_host_doc = to_document(&Host { + // assigned_hoster: hoster_hc_pubkey, + // ..host + // }) + // .map_err(|e| ServiceError::Internal(e.to_string()))?; + + // self.host_collection + // .update_one_within( + // host_query, + // UpdateModifications::Document(updated_host_doc), + // ) + // .await?; + // } + // } + // None => { + // log::error!( + // "Error: Failed to locate Host record. Subject='{}'.", + // msg.subject + // ); + // is_valid = false; + // } + // } + + // // Find the mongo_id ref for the hoster associated with this user + // let RoleInfo { ref_id, role: _ } = u.roles.into_iter().find(|r| matches!(r.role, Role::Hoster(_))).ok_or_else(|| { + // let err_msg = format!("Error: Failed to locate Hoster record id in User collection. Subject='{}'.", msg.subject); + // handle_internal_err(&err_msg) + // })?; + + // // Finally, find the hoster collection + // match self + // .hoster_collection + // .get_one_from(doc! { "_id": ref_id.clone() }) + // .await? + // { + // Some(hoster) => { + // // ...and pair the hoster with host (if the host is not already assiged to the hoster) + // let mut updated_assigned_hosts = hoster.assigned_hosts; + // if !updated_assigned_hosts.contains(&host_pubkey.to_string()) { + // let hoster_query: bson::Document = + // doc! { "_id": hoster._id.clone() }; + // updated_assigned_hosts.push(host_pubkey.to_string()); + // let updated_hoster_doc = to_document(&Hoster { + // assigned_hosts: updated_assigned_hosts, + // ..hoster + // }) + // .map_err(|e| ServiceError::Internal(e.to_string()))?; + + // self.host_collection + // .update_one_within( + // hoster_query, + // UpdateModifications::Document(updated_hoster_doc), + // ) + // .await?; + // } + // } + // None => { + // log::error!( + // "Error: Failed to locate Hoster record. Subject='{}'.", + // msg.subject + // ); + // is_valid = false; + // } + // } + // is_valid + // } + // None => { + // log::error!( + // "Error: Failed to find User Collection with Hoster pubkey. Subject='{}'.", + // msg.subject + // ); + // false + // } + // }; + // is_valid + } else { + false + }; + + // 4. Assign permissions based on whether the hoster was successfully validated + let permissions = if is_hoster_valid { + // If successful, assign personalized inbox and auth permissions + let user_unique_auth_subject = &format!("AUTH.{}.>", host_pubkey); + println!(">>> user_unique_auth_subject : {user_unique_auth_subject}"); + + let user_unique_inbox = &format!("_INBOX.{}.>", host_pubkey); + println!(">>> user_unique_inbox : {user_unique_inbox}"); + + let authenticated_user_diagnostics_subject = + &format!("DIAGNOSTICS.{}.>", host_pubkey); + println!(">>> authenticated_user_diagnostics_subject : {authenticated_user_diagnostics_subject}"); + + types::Permissions { + publish: types::PermissionLimits { + allow: Some(vec![ + "AUTH.validate".to_string(), + user_unique_auth_subject.to_string(), + user_unique_inbox.to_string(), + authenticated_user_diagnostics_subject.to_string(), + ]), + deny: None, + }, + subscribe: types::PermissionLimits { + allow: Some(vec![ + user_unique_auth_subject.to_string(), + user_unique_inbox.to_string(), + authenticated_user_diagnostics_subject.to_string(), + ]), + deny: None, + }, + } + } else { + // Otherwise, exclusively grant publication permissions for the unauthenticated diagnostics subj + // ...to allow the host device to still send diganostic reports + let unauthenticated_user_diagnostics_subject = + format!("DIAGNOSTICS.{}.unauthenticated.>", host_pubkey); + types::Permissions { + publish: types::PermissionLimits { + allow: Some(vec![unauthenticated_user_diagnostics_subject]), + deny: None, + }, + subscribe: types::PermissionLimits { + allow: None, + deny: Some(vec![">".to_string()]), + }, + } + }; + + let auth_response_claim = utils::generate_auth_response_claim( + auth_signing_account_keypair, + auth_signing_account_pubkey, + auth_root_account_pubkey, + permissions, + auth_request_claim, + ) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + let claim_str = serde_json::to_string(&auth_response_claim) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + let token = utils::encode_jwt(&claim_str, &auth_root_account_keypair) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + println!("\n\n\n\nencoded_jwt: {:#?}", token); + + // DONE BY JS HANDLER + // let res = token.into_bytes(); + // if let Some(reply) = msg.reply { + // client.publish(reply, res.into()).await?; + // } + + Ok(types::AuthApiResult { + result: types::AuthResult::Callout(token), + maybe_response_tags: None, + }) + } + + pub async fn handle_handshake_request( + &self, + msg: Arc, + ) -> Result { + log::warn!("INCOMING Message for 'AUTH.validate' : {:?}", msg); + + // 1. Verify expected data was received + let signature: &[u8] = match &msg.headers { + Some(h) => HeaderValue::as_ref(h.get("X-Signature").ok_or_else(|| { + log::error!("Error: Missing x-signature header. Subject='AUTH.authorize'"); + ServiceError::Request(format!("{:?}", ErrorCode::BAD_REQUEST)) + })?), + None => { + log::error!("Error: Missing message headers. Subject='AUTH.authorize'"); + return Err(ServiceError::Request(format!( + "{:?}", + ErrorCode::BAD_REQUEST + ))); + } + }; + + let types::AuthJWTPayload { + host_pubkey, + maybe_sys_pubkey, + nonce: _, + } = Self::convert_msg_to_type::(msg.clone())?; + + // 2. Validate signature + let user_verifying_keypair = KeyPair::from_public_key(&host_pubkey) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + if let Err(e) = user_verifying_keypair.verify(msg.payload.as_ref(), signature) { + log::error!( + "Error: Failed to validate Signature. Subject='{}'. Err={}", + msg.subject, + e + ); + return Err(ServiceError::Request(format!( + "{:?}", + ErrorCode::BAD_REQUEST + ))); + }; + + // 4. Add User keys to nsc resolver (and automatically create account-signed refernce to user key) + if let Some(sys_pubkey) = maybe_sys_pubkey { + Command::new("nsc") + .arg(format!( + "add user -a SYS -n user_sys_host_{} -k {}", + host_pubkey, sys_pubkey + )) + .output() + .context("Failed to add host sys user with provided keys") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + Command::new("nsc") + .arg(format!( + "add user -a WORKLOAD -n user_host_{} -k {} -K {} --tag pubkey:{}", + host_pubkey, host_pubkey, WORKLOAD_SK_ROLE, host_pubkey + )) + .output() + .context("Failed to add host user with provided keys") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + } + + // ..and push auth updates to hub server + Command::new("nsc") + .arg("push -A") + .output() + .context("Failed to update resolver config file") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + // 3. Create User JWT files (automatically signed with respective account key) + let sys_jwt_output = Command::new("nsc") + .arg(format!( + "describe user -n user_sys_host_{} -a SYS --raw", + host_pubkey + )) + .output() + .context("Failed to generate host sys user jwt file") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + let sys_jwt = String::from_utf8(sys_jwt_output.stdout) + .context("Command returned invalid UTF-8 output") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + let host_jwt_output = Command::new("nsc") + .arg(format!( + "describe user -n user_host_{} -a WORKLOAD --raw", + host_pubkey + )) + .output() + .context("Failed to generate host user jwt file") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + let host_jwt = String::from_utf8(host_jwt_output.stdout) + .context("Command returned invalid UTF-8 output") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + let mut tag_map: HashMap = HashMap::new(); + tag_map.insert("host_pubkey".to_string(), host_pubkey.clone()); + + Ok(AuthApiResult { + result: types::AuthResult::Authorization(types::AuthJWTResult { + host_pubkey: host_pubkey.clone(), + status: types::AuthState::Authorized, + host_jwt, + sys_jwt, + }), + maybe_response_tags: Some(tag_map), + }) + } + + // Helper function to initialize mongodb collections + async fn init_collection( + client: &MongoDBClient, + collection_name: &str, + ) -> Result> + where + T: Serialize + for<'de> Deserialize<'de> + Unpin + Send + Sync + Default + IntoIndexes, + { + Ok(MongoCollection::::new(client, schemas::DATABASE_NAME, collection_name).await?) + } + + pub fn call(&self, handler: F) -> AsyncEndpointHandler + where + F: Fn(Self, Arc) -> Fut + Send + Sync + 'static, + Fut: Future> + Send + 'static, + Self: Send + Sync, + { + let api = self.to_owned(); + Arc::new( + move |msg: Arc| -> JsServiceResponse { + let api_clone = api.clone(); + Box::pin(handler(api_clone, msg)) + }, + ) + } + + fn convert_msg_to_type(msg: Arc) -> Result + where + T: for<'de> Deserialize<'de> + Send + Sync, + { + let payload_buf = msg.payload.to_vec(); + serde_json::from_slice::(&payload_buf).map_err(|e| { + let err_msg = format!( + "Error: Failed to deserialize payload. Subject='{}' Err={}", + msg.subject.clone().into_string(), + e + ); + log::error!("{}", err_msg); + ServiceError::Request(format!("{} Code={:?}", err_msg, ErrorCode::BAD_REQUEST)) + }) + } +} + + +// example: +// [1] Subject: AUTH.UDS2A7I4BCECURHE64C52ORK6IDSOSE4ILZ7RJM4IO4EAYF33B67EWEF.> Received: 2025-02-05T21:19:52-06:00 +// X-Signature: [80, 71, 109, 80, 76, 99, 48, 122, 56, 113, 112, 48, 101, 95, 57, 107, 45, 105, 78, 75, 72, 67, 66, 97, 120, 117, 102, 110, 100, 72, 110, 53, 101, 74, 82, 77, 52, 121, 65, 66, 85, 53, 48, 109, 101, 51, 107, 54, 50, 65, 89, 81, 85, 51, 52, 50, 80, 81, 74, 49, 119, 90, 118, 104, 112, 100, 68, 109, 99, 105, 49, 69, 101, 85, 116, 67, 48, 118, 68, 89, 74, 86, 56, 86, 65, 103] +// {"host_pubkey":"UDS2A7I4BCECURHE64C52ORK6IDSOSE4ILZ7RJM4IO4EAYF33B67EWEF","maybe_sys_pubkey":"UACJZQOQK2Y2JFQVNV4CJORAEZGV3GYTCK7UOSCLNEZJRKMOW4ATUZZG","nonce":"zq7bDlgqpGcAAAAA3ItWHoUsldKNZg7/"} + +// - subject: _INBOX.Uwce1Uabie65ojhlucmyhB.vy24bmby +// - subject: _INBOX.5RgE68PiQieODvqbf4Yn1s.6f14XeRJ diff --git a/rust/services/authentication/src/types.rs b/rust/services/authentication/src/types.rs new file mode 100644 index 0000000..2cfaa3a --- /dev/null +++ b/rust/services/authentication/src/types.rs @@ -0,0 +1,246 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use util_libs::js_stream_service::{CreateResponse, CreateTag, EndpointTraits}; + +pub const AUTH_CALLOUT_SUBJECT: &str = "$SYS.REQ.USER.AUTH"; +pub const AUTHORIZE_SUBJECT: &str = "validate"; + +// The workload_sk_role is assigned when the host agent is created during the auth flow. +// NB: This role name *must* match the `ROLE_NAME_WORKLOAD` in the `orchestrator_setup.sh` script file. +pub const WORKLOAD_SK_ROLE: &str = "workload-role"; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum AuthState { + Unauthenticated, // step 0 + Authenticated, // step 1 + Authorized, // step 2 + Forbidden, // failure to auth + Error(String), // internal error +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct AuthJWTPayload { + pub host_pubkey: String, // nkey + pub maybe_sys_pubkey: Option, // nkey + pub nonce: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AuthJWTResult { + pub status: AuthState, + pub host_pubkey: String, + pub host_jwt: String, + pub sys_jwt: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum AuthResult { + Callout(String), // stringifiedAuthResponseClaim + Authorization(AuthJWTResult), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AuthApiResult { + pub result: AuthResult, + // NB: `maybe_response_tags` optionally return endpoint scoped vars to be available for use as a response subject in JS Service Endpoint handler + pub maybe_response_tags: Option>, +} +// NB: The following Traits make API Service compatible as a JS Service Endpoint +impl EndpointTraits for AuthApiResult {} +impl CreateTag for AuthApiResult { + fn get_tags(&self) -> HashMap { + self.maybe_response_tags.clone().unwrap_or_default() + } +} +impl CreateResponse for AuthApiResult { + fn get_response(&self) -> bytes::Bytes { + match self.clone().result { + AuthResult::Authorization(r) => match serde_json::to_vec(&r) { + Ok(r) => r.into(), + Err(e) => e.to_string().into(), + }, + AuthResult::Callout(token) => token.clone().into_bytes().into(), + } + } +} + +////////////////////////// +// Auth Callout Types +////////////////////////// +// Callout Request Types: +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct AuthGuardPayload { + pub host_pubkey: String, // nkey pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub hoster_hc_pubkey: Option, // holochain encoded hoster pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + pub nonce: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + host_signature: Vec, // used to verify the host keypair +} +// NB: Currently there is no way to pass headers in the auth callout. +// Therefore the host_signature is passed within the b64 encoded `AuthGuardPayload` token +impl AuthGuardPayload { + pub fn try_add_signature(mut self, sign_handler: T) -> Result + where + T: Fn(&[u8]) -> Result, + { + let payload_bytes = serde_json::to_vec(&self)?; + let signature = sign_handler(&payload_bytes)?; + self.host_signature = signature.as_bytes().to_vec(); + Ok(self) + } + + pub fn without_signature(mut self) -> Self { + self.host_signature = vec![]; + self + } + + pub fn get_host_signature(&self) -> Vec { + self.host_signature.clone() + } +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct NatsAuthorizationRequestClaim { + #[serde(flatten)] + pub generic_claim_data: ClaimData, + #[serde(rename = "nats")] + pub auth_request: NatsAuthorizationRequest, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct NatsAuthorizationRequest { + pub server_id: NatsServerId, + pub user_nkey: String, + pub client_info: NatsClientInfo, + pub connect_opts: ConnectOptions, + pub r#type: String, // should be authorization_request + pub version: u8, // should be 2 +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct NatsServerId { + pub name: String, // Server name + pub host: String, // Server host address + pub id: String, // Server connection ID + pub version: String, // Version of server (current stable = 2.10.22) + pub cluster: String, // Server cluster name +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct NatsClientInfo { + pub host: String, // client host address + pub id: u64, // client connection ID (I think...) + pub user: String, // the user pubkey (the passed-in key) + pub name_tag: String, // The user pubkey name + pub kind: String, // should be "Client" + pub nonce: String, + pub r#type: String, // should be "nats" + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct ConnectOptions { + #[serde(rename = "auth_token")] + pub user_auth_token: String, // This is the b64 encoding of the `AuthGuardPayload` -- used to validate user + #[serde(rename = "jwt")] + pub user_jwt: String, // This is the jwt string of the `UserClaim` + #[serde(skip_serializing_if = "Option::is_none")] + pub sig: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub lang: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol: Option, +} + +// Callout Response Types: +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthResponseClaim { + #[serde(flatten)] + pub generic_claim_data: ClaimData, + #[serde(rename = "nats")] + pub auth_response: AuthGuardResponse, +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct ClaimData { + #[serde(rename = "iat")] + pub issued_at: i64, // Issued At (Unix timestamp) + #[serde(rename = "iss")] + pub issuer: String, // Issuer -- head account (from which any signing keys were created) + #[serde(default, rename = "aud", skip_serializing_if = "Option::is_none")] + pub audience: Option, // Audience for whom the token is intended + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default, rename = "exp", skip_serializing_if = "Option::is_none")] + pub expires_at: Option, // Expiry (Optional, Unix timestamp) + #[serde(default, rename = "jti", skip_serializing_if = "Option::is_none")] + pub jwt_id: Option, // Base32 hash of the claims + #[serde(default, rename = "nbf", skip_serializing_if = "Option::is_none")] + pub not_before: Option, // Issued At (Unix timestamp) + #[serde(default, rename = "sub")] + pub subcriber: String, // Public key of the account or user to which the JWT is being issued +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct NatsGenericData { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tags: Vec, + #[serde(rename = "type")] + pub claim_type: String, // should be "user" + pub version: u8, // should be 2 +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct AuthGuardResponse { + #[serde(flatten)] + pub generic_data: NatsGenericData, + #[serde(default, rename = "jwt", skip_serializing_if = "Option::is_none")] + pub user_jwt: Option, // This is the jwt string of the `UserClaim` + #[serde(default, skip_serializing_if = "Option::is_none")] + pub issuer_account: Option, // Issuer Account === the signing nkey. Should set when the claim is issued by a signing key. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct UserClaim { + #[serde(flatten)] + pub generic_claim_data: ClaimData, + #[serde(rename = "nats")] + pub user_claim_data: UserClaimData, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct UserClaimData { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub issuer_account: Option, + #[serde(flatten)] + pub permissions: Permissions, + #[serde(flatten)] + pub generic_data: NatsGenericData, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Permissions { + #[serde(rename = "pub")] + pub publish: PermissionLimits, + #[serde(rename = "sub")] + pub subscribe: PermissionLimits, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct PermissionLimits { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub allow: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub deny: Option>, +} diff --git a/rust/services/authentication/src/utils.rs b/rust/services/authentication/src/utils.rs new file mode 100644 index 0000000..e3001ea --- /dev/null +++ b/rust/services/authentication/src/utils.rs @@ -0,0 +1,223 @@ +use super::types; +use anyhow::{anyhow, Result}; +use data_encoding::{BASE32HEX_NOPAD, BASE64URL_NOPAD}; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use nkeys::KeyPair; +use serde::Deserialize; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::io::Write; +use std::sync::Arc; +use std::time::SystemTime; +use util_libs::nats_js_client::ServiceError; + +pub fn handle_internal_err(err_msg: &str) -> ServiceError { + log::error!("{}", err_msg); + ServiceError::Internal(err_msg.to_string()) +} + +pub async fn write_file(data: Vec, output_dir: &str, file_name: &str) -> Result { + let output_path = format!("{}/{}", output_dir, file_name); + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&output_path)?; + + file.write_all(&data)?; + file.flush()?; + Ok(output_path) +} + +/// Decode a Base64-encoded string back into a JSON string +pub fn base64_to_data(base64_data: &str) -> Result +where + T: for<'de> Deserialize<'de>, +{ + let decoded_bytes = BASE64URL_NOPAD.decode(base64_data.as_bytes())?; + let json_string = String::from_utf8(decoded_bytes)?; + let parsed_json: T = serde_json::from_str(&json_string)?; + Ok(parsed_json) +} + +pub fn hash_claim(claims_str: &str) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(claims_str); + let claims_hash = hasher.finalize(); + claims_hash.as_slice().into() +} + +// Convert claims to JWT/Token +pub fn encode_jwt(claims_str: &str, signing_kp: &Arc) -> Result { + const JWT_HEADER: &str = r#"{"typ":"JWT","alg":"ed25519-nkey"}"#; + let b64_header: String = BASE64URL_NOPAD.encode(JWT_HEADER.as_bytes()); + println!("encoded b64 header: {:?}", b64_header); + let b64_body = BASE64URL_NOPAD.encode(claims_str.as_bytes()); + println!("encoded header: {:?}", b64_body); + + let jwt_half = format!("{b64_header}.{b64_body}"); + let sig = signing_kp.sign(jwt_half.as_bytes())?; + let b64_sig = BASE64URL_NOPAD.encode(&sig); + + let token = format!("{jwt_half}.{b64_sig}"); + Ok(token) +} + +/// Convert token into the +pub fn decode_jwt(token: &str) -> Result +where + T: for<'de> Deserialize<'de> + std::fmt::Debug, +{ + // Decode and replace custom `ed25519-nkey` to `EdDSA` + let parts: Vec<&str> = token.split('.').collect(); + println!("parts: {:?}", parts); + println!("parts.len() : {:?}", parts.len()); + + if parts.len() != 3 { + return Err(anyhow!("Invalid JWT format")); + } + + // Decode base64 JWT header and fix the algorithm field + let header_json = BASE64URL_NOPAD.decode(parts[0].as_bytes())?; + let mut header: Value = serde_json::from_slice(&header_json).expect("failed to create header"); + println!("header: {:?}", header); + + // Manually fix the algorithm name + if let Some(alg) = header.get_mut("alg") { + if alg == "ed25519-nkey" { + *alg = serde_json::Value::String("EdDSA".to_string()); + } + } + println!("after header: {:?}", header); + + let modified_header = BASE64URL_NOPAD.encode(&serde_json::to_vec(&header)?); + println!("modified_header: {:?}", modified_header); + + let part_1_json = BASE64URL_NOPAD.decode(parts[1].as_bytes())?; + let mut part_1: Value = serde_json::from_slice(&part_1_json)?; + if part_1.get("exp").is_none() { + let one_week = std::time::Duration::from_secs(7 * 24 * 60 * 60); + let one_week_from_now = SystemTime::now() + one_week; + let expires_at: i64 = one_week_from_now + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs() + .try_into()?; + + let mut b: types::UserClaim = serde_json::from_value(part_1)?; + b.generic_claim_data.expires_at = Some(expires_at); + part_1 = serde_json::to_value(b)?; + } + let modified_part_1 = BASE64URL_NOPAD.encode(&serde_json::to_vec(&part_1)?); + + let modified_token = format!("{}.{}.{}", modified_header, modified_part_1, parts[2]); + println!("modified_token: {:?}", modified_token); + + let account_kp = + KeyPair::from_public_key("ABYGJO6B2OJTXL7DLL7EGR45RQ4I2CKM4D5XYYUSUBZJ7HJJF67E54VC")?; + + let public_key_b32 = account_kp.public_key(); + println!("Public Key (Base32): {}", public_key_b32); + + // Decode from Base32 to raw bytes using Rfc4648 (compatible with NATS keys) + let public_key_bytes = Some(BASE32HEX_NOPAD.decode(public_key_b32.as_bytes())) + .ok_or(anyhow!("Failed to convert public key to bytes"))??; + println!("Decoded Public Key Bytes: {:?}", public_key_bytes); + + // Use the decoded key to create a DecodingKey + let decoding_key = DecodingKey::from_ed_der(&public_key_bytes); + println!(">>>>>>> decoded key"); + + // Validate the token with the correct algorithm + let mut validation = Validation::new(Algorithm::EdDSA); + validation.insecure_disable_signature_validation(); + validation.validate_aud = false; // Disable audience validation + println!("passed validation"); + + let token_data = decode::(&modified_token, &decoding_key, &validation)?; + // println!("token_data: {:#?}", token_data); + + Ok(token_data.claims) +} + +pub fn generate_auth_response_claim( + auth_signing_account_keypair: Arc, + auth_signing_account_pubkey: String, + auth_root_account_pubkey: String, + permissions: types::Permissions, + auth_request_claim: types::NatsAuthorizationRequestClaim, +) -> Result { + let now = SystemTime::now(); + let issued_at = now + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs() + .try_into()?; + let one_week = std::time::Duration::from_secs(7 * 24 * 60 * 60); + let one_week_from_now = now + one_week; + let expires_at: i64 = one_week_from_now + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs() + .try_into()?; + let inner_generic_data = types::NatsGenericData { + claim_type: "user".to_string(), + tags: vec![], + version: 2, + }; + let user_claim_data = types::UserClaimData { + permissions, + generic_data: inner_generic_data, + issuer_account: Some(auth_root_account_pubkey.clone()), // must be the root account pubkey or the issuer account that signs the claim AND must be listed "allowed-account" + }; + let inner_nats_claim = types::ClaimData { + issuer: auth_signing_account_pubkey.clone(), // Must be the pubkey of the keypair that signs the claim + subcriber: auth_request_claim.auth_request.user_nkey.clone(), + issued_at, + audience: None, // Inner claim should have no `audience` when using the operator-auth mode + expires_at: Some(expires_at), + not_before: None, + name: Some("allowed_auth_user".to_string()), + jwt_id: None, + }; + let mut user_claim = types::UserClaim { + generic_claim_data: inner_nats_claim, + user_claim_data, + }; + + let mut user_claim_str = serde_json::to_string(&user_claim)?; + let hashed_user_claim_bytes = hash_claim(&user_claim_str); + user_claim.generic_claim_data.jwt_id = Some(BASE32HEX_NOPAD.encode(&hashed_user_claim_bytes)); + user_claim_str = serde_json::to_string(&user_claim)?; + let user_token = encode_jwt(&user_claim_str, &auth_signing_account_keypair)?; + println!("user_token: {:#?}", user_token); + + let outer_nats_claim = types::ClaimData { + issuer: auth_root_account_pubkey.clone(), // Must be the pubkey of the keypair that signs the claim + subcriber: auth_request_claim.auth_request.user_nkey.clone(), + issued_at, + audience: Some(auth_request_claim.auth_request.server_id.id), + expires_at: None, // Some(expires_at), + not_before: None, + name: None, + jwt_id: None, + }; + let outer_generic_data = types::NatsGenericData { + claim_type: "authorization_response".to_string(), + tags: vec![], + version: 2, + }; + let auth_response = types::AuthGuardResponse { + generic_data: outer_generic_data, + user_jwt: Some(user_token), + issuer_account: None, + error: None, + }; + let mut auth_response_claim = types::AuthResponseClaim { + generic_claim_data: outer_nats_claim, + auth_response, + }; + + let claim_str = serde_json::to_string(&auth_response_claim)?; + let hashed_claim_bytes = hash_claim(&claim_str); + auth_response_claim.generic_claim_data.jwt_id = + Some(BASE32HEX_NOPAD.encode(&hashed_claim_bytes)); + + Ok(auth_response_claim) +} diff --git a/rust/services/workload/src/host_api.rs b/rust/services/workload/src/host_api.rs index 028b581..f44b338 100644 --- a/rust/services/workload/src/host_api.rs +++ b/rust/services/workload/src/host_api.rs @@ -10,12 +10,12 @@ use crate::types::WorkloadResult; use super::{types::WorkloadApiResult, WorkloadServiceApi}; use anyhow::Result; +use async_nats::Message; use core::option::Option::None; use std::{fmt::Debug, sync::Arc}; -use async_nats::Message; use util_libs::{ + db::schemas::{WorkloadState, WorkloadStatus}, nats_js_client::ServiceError, - db::schemas::{WorkloadState, WorkloadStatus} }; #[derive(Debug, Clone, Default)] @@ -24,7 +24,10 @@ pub struct HostWorkloadApi {} impl WorkloadServiceApi for HostWorkloadApi {} impl HostWorkloadApi { - pub async fn start_workload(&self, msg: Arc) -> Result { + pub async fn start_workload( + &self, + msg: Arc, + ) -> Result { let msg_subject = msg.subject.clone().into_string(); log::trace!("Incoming message for '{}'", msg_subject); @@ -52,16 +55,19 @@ impl HostWorkloadApi { } }; - Ok(WorkloadApiResult { + Ok(WorkloadApiResult { result: WorkloadResult { status, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) } - pub async fn update_workload(&self, msg: Arc) -> Result { + pub async fn update_workload( + &self, + msg: Arc, + ) -> Result { let msg_subject = msg.subject.clone().into_string(); log::trace!("Incoming message for '{}'", msg_subject); @@ -69,7 +75,6 @@ impl HostWorkloadApi { log::debug!("Message payload '{}' : {:?}", msg_subject, message_payload); let status = if let Some(workload) = message_payload.workload { - // TODO: Talk through with Stefan // 1. Connect to interface for Nix and instruct systemd to install workload... // eg: nix_install_with(workload) @@ -89,17 +94,20 @@ impl HostWorkloadApi { actual: WorkloadState::Error(err_msg), } }; - - Ok(WorkloadApiResult { + + Ok(WorkloadApiResult { result: WorkloadResult { status, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) } - pub async fn uninstall_workload(&self, msg: Arc) -> Result { + pub async fn uninstall_workload( + &self, + msg: Arc, + ) -> Result { let msg_subject = msg.subject.clone().into_string(); log::trace!("Incoming message for '{}'", msg_subject); @@ -110,7 +118,7 @@ impl HostWorkloadApi { // TODO: Talk through with Stefan // 1. Connect to interface for Nix and instruct systemd to UNinstall workload... // nix_uninstall_with(workload_id) - + // 2. Respond to endpoint request WorkloadStatus { id: workload._id, @@ -127,18 +135,21 @@ impl HostWorkloadApi { } }; - Ok(WorkloadApiResult { + Ok(WorkloadApiResult { result: WorkloadResult { status, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) } // For host agent ? or elsewhere ? // TODO: Talk through with Stefan - pub async fn send_workload_status(&self, msg: Arc) -> Result { + pub async fn send_workload_status( + &self, + msg: Arc, + ) -> Result { let msg_subject = msg.subject.clone().into_string(); log::trace!("Incoming message for '{}'", msg_subject); @@ -150,9 +161,9 @@ impl HostWorkloadApi { Ok(WorkloadApiResult { result: WorkloadResult { status: workload_status, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) } -} \ No newline at end of file +} diff --git a/rust/services/workload/src/lib.rs b/rust/services/workload/src/lib.rs index 9d764b0..51c1807 100644 --- a/rust/services/workload/src/lib.rs +++ b/rust/services/workload/src/lib.rs @@ -5,22 +5,22 @@ Provisioning Account: WORKLOAD Users: orchestrator & host */ -pub mod orchestrator_api; pub mod host_api; +pub mod orchestrator_api; pub mod types; use anyhow::Result; -use core::option::Option::None; use async_nats::jetstream::ErrorCode; -use async_trait::async_trait; -use std::{fmt::Debug, sync::Arc}; use async_nats::Message; -use std::future::Future; +use async_trait::async_trait; +use core::option::Option::None; use serde::Deserialize; +use std::future::Future; +use std::{fmt::Debug, sync::Arc}; use types::{WorkloadApiResult, WorkloadResult}; use util_libs::{ - nats_js_client::{ServiceError, AsyncEndpointHandler, JsServiceResponse}, - db::schemas::{WorkloadState, WorkloadStatus} + db::schemas::{WorkloadState, WorkloadStatus}, + nats_js_client::{AsyncEndpointHandler, JsServiceResponse, ServiceError}, }; pub const WORKLOAD_SRV_NAME: &str = "WORKLOAD"; @@ -28,26 +28,24 @@ pub const WORKLOAD_SRV_SUBJ: &str = "WORKLOAD"; pub const WORKLOAD_SRV_VERSION: &str = "0.0.1"; pub const WORKLOAD_SRV_DESC: &str = "This service handles the flow of Workload requests between the Developer and the Orchestrator, and between the Orchestrator and Host."; - #[async_trait] pub trait WorkloadServiceApi where Self: std::fmt::Debug + Clone + 'static, { - fn call( - &self, - handler: F, - ) -> AsyncEndpointHandler + fn call(&self, handler: F) -> AsyncEndpointHandler where F: Fn(Self, Arc) -> Fut + Send + Sync + 'static, Fut: Future> + Send + 'static, - Self: Send + Sync + Self: Send + Sync, { - let api = self.to_owned(); - Arc::new(move |msg: Arc| -> JsServiceResponse { - let api_clone = api.clone(); - Box::pin(handler(api_clone, msg)) - }) + let api = self.to_owned(); + Arc::new( + move |msg: Arc| -> JsServiceResponse { + let api_clone = api.clone(); + Box::pin(handler(api_clone, msg)) + }, + ) } fn convert_msg_to_type(msg: Arc) -> Result @@ -56,11 +54,14 @@ where { let payload_buf = msg.payload.to_vec(); serde_json::from_slice::(&payload_buf).map_err(|e| { - let err_msg = format!("Error: Failed to deserialize payload. Subject='{}' Err={}", msg.subject.clone().into_string(), e); + let err_msg = format!( + "Error: Failed to deserialize payload. Subject='{}' Err={}", + msg.subject.clone().into_string(), + e + ); log::error!("{}", err_msg); ServiceError::Request(format!("{} Code={:?}", err_msg, ErrorCode::BAD_REQUEST)) }) - } // Helper function to streamline the processing of incoming workload messages @@ -95,11 +96,11 @@ where WorkloadApiResult { result: WorkloadResult { status, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, } } }) } -} \ No newline at end of file +} diff --git a/rust/services/workload/src/orchestrator_api.rs b/rust/services/workload/src/orchestrator_api.rs index a49db80..6bb0e54 100644 --- a/rust/services/workload/src/orchestrator_api.rs +++ b/rust/services/workload/src/orchestrator_api.rs @@ -12,19 +12,19 @@ use crate::types::WorkloadResult; use super::{types::WorkloadApiResult, WorkloadServiceApi}; use anyhow::Result; -use core::option::Option::None; -use std::{collections::HashMap, fmt::Debug, sync::Arc}; use async_nats::Message; +use bson::{self, doc, to_document}; +use core::option::Option::None; use mongodb::{options::UpdateModifications, Client as MongoDBClient}; use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; -use bson::{self, doc, to_document}; +use std::{collections::HashMap, fmt::Debug, sync::Arc}; use util_libs::{ - nats_js_client::ServiceError, db::{ mongodb::{IntoIndexes, MongoCollection, MongoDbAPI}, - schemas::{self, Host, Workload, WorkloadState, WorkloadStatus} - } + schemas::{self, Host, Workload, WorkloadState, WorkloadStatus}, + }, + nats_js_client::ServiceError, }; #[derive(Debug, Clone)] @@ -39,7 +39,8 @@ impl WorkloadServiceApi for OrchestratorWorkloadApi {} impl OrchestratorWorkloadApi { pub async fn new(client: &MongoDBClient) -> Result { Ok(Self { - workload_collection: Self::init_collection(client, schemas::WORKLOAD_COLLECTION_NAME).await?, + workload_collection: Self::init_collection(client, schemas::WORKLOAD_COLLECTION_NAME) + .await?, host_collection: Self::init_collection(client, schemas::HOST_COLLECTION_NAME).await?, user_collection: Self::init_collection(client, schemas::USER_COLLECTION_NAME).await?, }) @@ -51,8 +52,14 @@ impl OrchestratorWorkloadApi { msg, WorkloadState::Reported, |workload: schemas::Workload| async move { - let workload_id = self.workload_collection.insert_one_into(workload.clone()).await?; - log::info!("Successfully added workload. MongodDB Workload ID={:?}", workload_id); + let workload_id = self + .workload_collection + .insert_one_into(workload.clone()) + .await?; + log::info!( + "Successfully added workload. MongodDB Workload ID={:?}", + workload_id + ); let new_workload = schemas::Workload { _id: Some(workload_id), ..workload @@ -64,9 +71,9 @@ impl OrchestratorWorkloadApi { desired: WorkloadState::Reported, actual: WorkloadState::Reported, }, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) }, WorkloadState::Error, @@ -74,16 +81,28 @@ impl OrchestratorWorkloadApi { .await } - pub async fn update_workload(&self, msg: Arc) -> Result { + pub async fn update_workload( + &self, + msg: Arc, + ) -> Result { log::debug!("Incoming message for 'WORKLOAD.update'"); self.process_request( msg, WorkloadState::Running, |workload: schemas::Workload| async move { let workload_query = doc! { "_id": workload._id.clone() }; - let updated_workload_doc = to_document(&workload).map_err(|e| ServiceError::Internal(e.to_string()))?; - self.workload_collection.update_one_within(workload_query, UpdateModifications::Document(updated_workload_doc)).await?; - log::info!("Successfully updated workload. MongodDB Workload ID={:?}", workload._id); + let updated_workload_doc = + to_document(&workload).map_err(|e| ServiceError::Internal(e.to_string()))?; + self.workload_collection + .update_one_within( + workload_query, + UpdateModifications::Document(updated_workload_doc), + ) + .await?; + log::info!( + "Successfully updated workload. MongodDB Workload ID={:?}", + workload._id + ); Ok(WorkloadApiResult { result: WorkloadResult { status: WorkloadStatus { @@ -91,18 +110,20 @@ impl OrchestratorWorkloadApi { desired: WorkloadState::Reported, actual: WorkloadState::Reported, }, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) }, WorkloadState::Error, ) .await - } - pub async fn remove_workload(&self, msg: Arc) -> Result { + pub async fn remove_workload( + &self, + msg: Arc, + ) -> Result { log::debug!("Incoming message for 'WORKLOAD.remove'"); self.process_request( msg, @@ -132,7 +153,10 @@ impl OrchestratorWorkloadApi { } // NB: Automatically published by the nats-db-connector - pub async fn handle_db_insertion(&self, msg: Arc) -> Result { + pub async fn handle_db_insertion( + &self, + msg: Arc, + ) -> Result { log::debug!("Incoming message for 'WORKLOAD.insert'"); self.process_request( msg, @@ -189,10 +213,11 @@ impl OrchestratorWorkloadApi { }; // Note: The `_id` is an option because it is only generated upon the intial insertion of a record in - // a mongodb collection. This also means that whenever a record is fetched from mongodb, it must have the `_id` feild. - // Using `unwrap` is therefore safe. - let host_id = host._id.to_owned().unwrap(); - + // a mongodb collection. This also means that whenever a record is fetched from mongodb, it must have the `_id` field. + let host_id = host._id + .to_owned() + .ok_or_else(|| ServiceError::Internal("Failed to read ._id from record".to_string()))?; + // 4. Update the Workload Collection with the assigned Host ID let workload_query = doc! { "_id": workload_id.clone() }; let updated_workload = &Workload { @@ -205,7 +230,7 @@ impl OrchestratorWorkloadApi { "Successfully added new workload into the Workload Collection. MongodDB Workload ID={:?}", updated_workload_result ); - + // 5. Update the Host Collection with the assigned Workload ID let host_query = doc! { "_id": host.clone()._id }; let updated_host_doc = to_document(&Host { @@ -240,13 +265,16 @@ impl OrchestratorWorkloadApi { // Zeeshan to take a look: // NB: Automatically published by the nats-db-connector - pub async fn handle_db_modification(&self, msg: Arc) -> Result { + pub async fn handle_db_modification( + &self, + msg: Arc, + ) -> Result { log::debug!("Incoming message for 'WORKLOAD.modify'"); - + let workload = Self::convert_msg_to_type::(msg)?; log::trace!("New workload to assign. Workload={:#?}", workload); - - // TODO: ...handle the use case for the update entry change stream + + // TODO: ...handle the use case for the update entry change stream // let workload_request_bytes = serde_json::to_vec(&workload).map_err(|e| ServiceError::Internal(e.to_string()))?; @@ -259,14 +287,17 @@ impl OrchestratorWorkloadApi { Ok(WorkloadApiResult { result: WorkloadResult { status: success_status, - workload: Some(workload) + workload: Some(workload), }, - maybe_response_tags: None + maybe_response_tags: None, }) } // NB: Published by the Hosting Agent whenever the status of a workload changes - pub async fn handle_status_update(&self, msg: Arc) -> Result { + pub async fn handle_status_update( + &self, + msg: Arc, + ) -> Result { log::debug!("Incoming message for 'WORKLOAD.handle_status_update'"); let workload_status = Self::convert_msg_to_type::(msg)?.status; @@ -277,9 +308,9 @@ impl OrchestratorWorkloadApi { Ok(WorkloadApiResult { result: WorkloadResult { status: workload_status, - workload: None + workload: None, }, - maybe_response_tags: None + maybe_response_tags: None, }) } @@ -293,5 +324,4 @@ impl OrchestratorWorkloadApi { { Ok(MongoCollection::::new(client, schemas::DATABASE_NAME, collection_name).await?) } - -} \ No newline at end of file +} diff --git a/rust/services/workload/src/types.rs b/rust/services/workload/src/types.rs index f68d56e..c608bd1 100644 --- a/rust/services/workload/src/types.rs +++ b/rust/services/workload/src/types.rs @@ -1,6 +1,9 @@ -use std::collections::HashMap; -use util_libs::{db::schemas::{self, WorkloadStatus}, js_stream_service::{CreateResponse, CreateTag, EndpointTraits}}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use util_libs::{ + db::schemas::{self, WorkloadStatus}, + js_stream_service::{CreateResponse, CreateTag, EndpointTraits}, +}; #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] @@ -14,7 +17,7 @@ pub enum WorkloadServiceSubjects { SendStatus, Start, Uninstall, - UpdateInstalled + UpdateInstalled, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -26,7 +29,7 @@ pub struct WorkloadResult { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkloadApiResult { pub result: WorkloadResult, - pub maybe_response_tags: Option> + pub maybe_response_tags: Option>, } impl EndpointTraits for WorkloadApiResult {} impl CreateTag for WorkloadApiResult { @@ -42,4 +45,4 @@ impl CreateResponse for WorkloadApiResult { Err(e) => e.to_string().into(), } } -} \ No newline at end of file +} diff --git a/rust/util_libs/src/db/mongodb.rs b/rust/util_libs/src/db/mongodb.rs index 1204912..0219876 100644 --- a/rust/util_libs/src/db/mongodb.rs +++ b/rust/util_libs/src/db/mongodb.rs @@ -1,3 +1,4 @@ +use crate::nats_js_client::ServiceError; use anyhow::Result; use async_trait::async_trait; use bson::{self, doc, Document}; @@ -7,7 +8,6 @@ use mongodb::results::{DeleteResult, UpdateResult}; use mongodb::{options::IndexOptions, Client, Collection, IndexModel}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; -use crate::nats_js_client::ServiceError; #[async_trait] pub trait MongoDbAPI @@ -23,7 +23,7 @@ where async fn update_one_within( &self, query: Document, - updated_doc: UpdateModifications + updated_doc: UpdateModifications, ) -> Result; async fn delete_one_from(&self, query: Document) -> Result; async fn delete_all_from(&self) -> Result; @@ -138,7 +138,7 @@ where async fn update_one_within( &self, query: Document, - updated_doc: UpdateModifications + updated_doc: UpdateModifications, ) -> Result { self.collection .update_one(query, updated_doc) diff --git a/rust/util_libs/src/db/schemas.rs b/rust/util_libs/src/db/schemas.rs index 1239448..76f2141 100644 --- a/rust/util_libs/src/db/schemas.rs +++ b/rust/util_libs/src/db/schemas.rs @@ -31,7 +31,7 @@ pub use String as MongoDbId; #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub enum Role { Developer(DeveloperJWT), // jwt string - Hoster(HosterPubKey), // host pubkey + Hoster(HosterPubKey), // host pubkey } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/rust/util_libs/src/js_stream_service.rs b/rust/util_libs/src/js_stream_service.rs index 94383ab..f044dbe 100644 --- a/rust/util_libs/src/js_stream_service.rs +++ b/rust/util_libs/src/js_stream_service.rs @@ -1,19 +1,20 @@ use super::nats_js_client::EndpointType; use anyhow::{anyhow, Result}; -use std::any::Any; use async_nats::jetstream::consumer::{self, AckPolicy, PullConsumer}; use async_nats::jetstream::stream::{self, Info, Stream}; use async_nats::jetstream::Context; use async_trait::async_trait; use futures::StreamExt; use serde::{Deserialize, Serialize}; +use std::any::Any; use std::collections::HashMap; use std::fmt::Debug; use std::sync::Arc; use tokio::sync::RwLock; -pub type ResponseSubjectsGenerator = Arc) -> Vec + Send + Sync>; +pub type ResponseSubjectsGenerator = + Arc) -> Vec + Send + Sync>; pub trait CreateTag: Send + Sync { fn get_tags(&self) -> HashMap; @@ -24,8 +25,17 @@ pub trait CreateResponse: Send + Sync { } pub trait EndpointTraits: - Serialize + for<'de> Deserialize<'de> + Send + Sync + Clone + Debug + CreateTag + CreateResponse + 'static -{} + Serialize + + for<'de> Deserialize<'de> + + Send + + Sync + + Clone + + Debug + + CreateTag + + CreateResponse + + 'static +{ +} #[async_trait] pub trait ConsumerExtTrait: Send + Sync + Debug + 'static { @@ -215,13 +225,18 @@ impl JsStreamService { where T: EndpointTraits, { - let full_subject = format!("{}.{}", self.service_subject, endpoint_subject); + // Avoid adding the Service Subject prefix if the Endpoint Subject name starts with global keywords $SYS or $JS + let consumer_subject = if endpoint_subject.starts_with("$SYS") || endpoint_subject.starts_with("$JS") { + endpoint_subject.to_string() + } else { + format!("{}.{}", self.service_subject, endpoint_subject) + }; // Register JS Subject Consumer let consumer_config = consumer::pull::Config { durable_name: Some(consumer_name.to_string()), ack_policy: AckPolicy::Explicit, - filter_subject: full_subject, + filter_subject: consumer_subject, ..Default::default() }; @@ -278,6 +293,8 @@ impl JsStreamService { let messages = consumer .stream() .heartbeat(std::time::Duration::from_secs(10)) + .max_messages_per_batch(100) + .expires(std::time::Duration::from_secs(30)) .messages() .await?; @@ -323,7 +340,10 @@ impl JsStreamService { ) where T: EndpointTraits, { + println!("WAITING TO PROCESS MESSAGE..."); while let Some(Ok(js_msg)) = messages.next().await { + println!("MESSAGES : js_msg={:?}", js_msg); + log::trace!( "{}Consumer received message: subj='{}.{}', endpoint={}, service={}", log_info.prefix, @@ -343,8 +363,8 @@ impl JsStreamService { let bytes = r.get_response(); let maybe_subject_tags = r.get_tags(); (bytes, maybe_subject_tags) - }, - Err(err) => (err.to_string().into(), HashMap::new()) + } + Err(err) => (err.to_string().into(), HashMap::new()), }; // Returns a response if a reply address exists. @@ -544,4 +564,4 @@ mod tests { let result = service.spawn_consumer_handler(consumer_name).await; assert!(result.is_ok(), "Failed to spawn consumer handler"); } -} \ No newline at end of file +} diff --git a/rust/util_libs/src/nats_js_client.rs b/rust/util_libs/src/nats_js_client.rs index ce2a0ee..0820a77 100644 --- a/rust/util_libs/src/nats_js_client.rs +++ b/rust/util_libs/src/nats_js_client.rs @@ -2,13 +2,14 @@ use super::js_stream_service::{CreateTag, JsServiceParamsPartial, JsStreamServic use crate::nats_server::LEAF_SERVER_DEFAULT_LISTEN_PORT; use anyhow::Result; -use core::option::Option::None; -use async_nats::{jetstream, HeaderMap, Message, ServerInfo}; +use async_nats::{jetstream, AuthError, HeaderMap, Message, ServerInfo}; +use core::marker::Sync; use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt; use std::fmt::Debug; use std::future::Future; +use std::path::PathBuf; use std::pin::Pin; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -19,6 +20,8 @@ pub enum ServiceError { Request(String), #[error(transparent)] Database(#[from] mongodb::error::Error), + #[error(transparent)] + Authentication(#[from] AuthError), #[error("Nats Error: {0}")] NATS(String), #[error("Internal Error: {0}")] @@ -63,7 +66,7 @@ pub struct PublishInfo { pub subject: String, pub msg_id: String, pub data: Vec, - pub headers: Option + pub headers: Option, } #[derive(Debug)] @@ -99,21 +102,24 @@ pub struct JsClient { service_log_prefix: String, } +#[derive(Clone)] +pub enum Credentials { + Path(std::path::PathBuf), // String = pathbuf as string + Password(String, String), +} + #[derive(Deserialize, Default)] pub struct NewJsClientParams { pub nats_url: String, pub name: String, pub inbox_prefix: String, - #[serde(default)] pub service_params: Vec, #[serde(skip_deserializing)] - pub opts: Vec, // NB: These opts should not be required for client instantiation - #[serde(default)] - pub credentials_path: Option, - #[serde(default)] + pub credentials: Option, pub ping_interval: Option, - #[serde(default)] pub request_timeout: Option, // Defaults to 5s + #[serde(skip_deserializing)] + pub listeners: Vec, } impl JsClient { @@ -125,15 +131,23 @@ impl JsClient { .request_timeout(Some(p.request_timeout.unwrap_or(Duration::from_secs(10)))) .custom_inbox_prefix(&p.inbox_prefix); - let client = match p.credentials_path { - Some(cp) => { - let path = std::path::Path::new(&cp); - connect_options - .credentials_file(path) - .await? - .connect(&p.nats_url) - .await? - } + let client = match p.credentials { + Some(c) => match c { + Credentials::Password(user, pw) => { + connect_options + .user_and_password(user, pw) + .connect(&p.nats_url) + .await? + } + Credentials::Path(cp) => { + let path = std::path::Path::new(&cp); + connect_options + .credentials_file(path) + .await? + .connect(&p.nats_url) + .await? + } + }, None => connect_options.connect(&p.nats_url).await?, }; @@ -159,7 +173,7 @@ impl JsClient { let service_log_prefix = format!("NATS-CLIENT-LOG::{}::", p.name); - let mut default_client = JsClient { + let mut js_client = JsClient { url: p.nats_url, name: p.name, on_msg_published_event: None, @@ -170,43 +184,23 @@ impl JsClient { service_log_prefix: service_log_prefix.clone(), }; - for opt in p.opts { - opt(&mut default_client); + for listener in p.listeners { + listener(&mut js_client); } log::info!( "{}Connected to NATS server at {}", service_log_prefix, - default_client.url + js_client.url ); - Ok(default_client) - } - - pub fn name(&self) -> &str { - &self.name + Ok(js_client) } pub fn get_server_info(&self) -> ServerInfo { self.client.server_info() } - pub async fn monitor(&self) -> Result<(), async_nats::Error> { - if let async_nats::connection::State::Disconnected = self.client.connection_state() { - Err(Box::new(ErrClientDisconnected)) - } else { - Ok(()) - } - } - - pub async fn close(&self) -> Result<(), async_nats::Error> { - self.client.drain().await?; - Ok(()) - } - - pub async fn health_check_stream(&self, stream_name: &str) -> Result<(), async_nats::Error> { - if let async_nats::connection::State::Disconnected = self.client.connection_state() { - return Err(Box::new(ErrClientDisconnected)); - } + pub async fn get_stream_info(&self, stream_name: &str) -> Result<(), async_nats::Error> { let stream = &self.js.get_stream(stream_name).await?; let info = stream.get_info().await?; log::debug!( @@ -218,17 +212,30 @@ impl JsClient { Ok(()) } + pub async fn check_connection( + &self, + ) -> Result { + let conn_state = self.client.connection_state(); + if let async_nats::connection::State::Disconnected = conn_state { + Err(Box::new(ErrClientDisconnected)) + } else { + Ok(conn_state) + } + } + pub async fn publish(&self, payload: PublishInfo) -> Result<(), async_nats::Error> { let now = Instant::now(); let result = match payload.headers { - Some(h) => self - .js - .publish_with_headers(payload.subject.clone(), h, payload.data.clone().into()) - .await, - None => self - .js - .publish(payload.subject.clone(), payload.data.clone().into()) - .await + Some(h) => { + self.js + .publish_with_headers(payload.subject.clone(), h, payload.data.clone().into()) + .await + } + None => { + self.js + .publish(payload.subject.clone(), payload.data.clone().into()) + .await + } }; let duration = now.elapsed(); @@ -267,6 +274,11 @@ impl JsClient { } None } + + pub async fn close(&self) -> Result<(), async_nats::Error> { + self.client.drain().await?; + Ok(()) + } } // Client Options: @@ -301,19 +313,29 @@ where // TODO: there's overlap with the NATS_LISTEN_PORT. refactor this to e.g. read NATS_LISTEN_HOST and NATS_LISTEN_PORT pub fn get_nats_url() -> String { std::env::var("NATS_URL").unwrap_or_else(|_| { - let default = format!("127.0.0.1:{}", LEAF_SERVER_DEFAULT_LISTEN_PORT); + let default = format!("127.0.0.1:{}", LEAF_SERVER_DEFAULT_LISTEN_PORT); // Shouldn't this be the 'NATS_LISTEN_PORT'? log::debug!("using default for NATS_URL: {default}"); default }) } -pub fn get_nats_client_creds(operator: &str, account: &str, user: &str) -> String { - std::env::var("HOST_CREDS_FILE_PATH").unwrap_or_else(|_| { - format!( - "/.local/share/nats/nsc/keys/creds/{}/{}/{}.creds", - operator, account, user - ) - }) +pub fn get_nsc_root_path() -> String { + std::env::var("NSC_PATH").unwrap_or_else(|_| "/.local/share/nats/nsc".to_string()) +} + +pub fn get_nats_creds_by_nsc(operator: &str, account: &str, user: &str) -> String { + format!( + "{}/keys/creds/{}/{}/{}.creds", + get_nsc_root_path(), + operator, + account, + user + ) +} + +pub fn get_path_buf_from_current_dir(file_name: &str) -> PathBuf { + let current_dir_path = std::env::current_dir().expect("Failed to locate current directory."); + current_dir_path.join(file_name) } pub fn get_event_listeners() -> Vec { diff --git a/rust/util_libs/src/nats_server.rs b/rust/util_libs/src/nats_server.rs index 602d04f..f884fd6 100644 --- a/rust/util_libs/src/nats_server.rs +++ b/rust/util_libs/src/nats_server.rs @@ -142,7 +142,7 @@ impl LeafServer { .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .spawn() - .expect("Failed to start NATS server"); + .context("Failed to start NATS server")?; // TODO: wait for a readiness indicator std::thread::sleep(std::time::Duration::from_millis(100)); diff --git a/scripts/hosting_agent_setup.sh b/scripts/hosting_agent_setup.sh new file mode 100644 index 0000000..46cc431 --- /dev/null +++ b/scripts/hosting_agent_setup.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2005,SC2086 + +# -------- +# NB: This setup expects the `nats` and the `nsc` binarys to be locally installed and accessible. This script will verify that they both exist locally before running setup commnds. + +# Script Overview: +# This script is responsible for setting up the "Operator Chain of Trust" (eg: O/A/U) authentication pattern that is associated with the Orchestrator Hub on the Hosting Agent. + +# Input Vars: +# - SHARED_CREDS_DIR +# - OPERATOR_JWT_PATH +# - SYS_ACCOUNT_JWT_PATH +# - AUTH_ACCOUNT_JWT_PATH + +# -------- + +set -e # Exit on any error + +# Check for required commands +for cmd in nsc nats; do + echo "Executing command: $cmd --version" + if command -v "$cmd" &>/dev/null; then + $cmd --version + else + echo "Command '$cmd' not found." + fi +done + +# Variables +NSC_PATH=$1 +OPERATOR_NAME="HOLO" +SYS_ACCOUNT_NAME="SYS" +AUTH_ACCOUNT_NAME="AUTH" +SHARED_CREDS_DIR="shared_creds_output" +OPERATOR_JWT_PATH="$SHARED_CREDS_DIR/$OPERATOR_NAME.jwt" +SYS_ACCOUNT_JWT_PATH="$SHARED_CREDS_DIR/$SYS_ACCOUNT_NAME.jwt" +AUTH_GUARD_USER_NAME="auth-guard" +AUTH_GUARD_USER_PATH="$SHARED_CREDS_DIR/$AUTH_GUARD_USER_NAME.creds" + +if [ ! -d "$SHARED_CREDS_DIR" ]; then + echo "Shared output dir not found. Unable to set up local chain of trust." + exit 1 +else + if [ ! -d "$OPERATOR_JWT_PATH" ]; then + echo "Operator JWT not found. Unable to set up local chain of trust." + exit 1 + else + echo "Found the $OPERATOR_JWT_PATH. Adding Operator to local chain reference." + # Add Operator + nsc add operator -u $OPERATOR_JWT_PATH --force + echo "Operator added to local nsc successfully." + + if [ ! -d "$SYS_ACCOUNT_JWT_PATH" ]; then + echo "SYS account JWT not found. Unable to add SYS ACCOUNT to the local chain of trust." + exit 1 + else + echo "Found the $SYS_ACCOUNT_JWT_PATH. Adding SYS Account to local chain reference." + # Add SYS Account + nsc import account --file $SYS_ACCOUNT_JWT_PATH + echo "SYS account added to local nsc successfully." + fi + + if [ ! -d "$AUTH_GUARD_USER_PATH" ]; then + echo "WARNING: AUTH_GUARD user credentials not found. Unable to add the complete Hosting Agent set-up." + else + echo "Found the $AUTH_GUARD_USER_NAME credentials file." + $AUTH_GUARD_CRED_PATH="{$NSC_PATH}/keys/creds/{$OPERATOR_NAME}/{$AUTH_ACCOUNT_NAME}/" + echo "Moving $AUTH_GUARD_USER_NAME creds to the $AUTH_GUARD_CRED_PATH directory." + mv $AUTH_GUARD_USER_PATH $AUTH_GUARD_CRED_PATH + echo "Set-up complete. Credential files are in the $AUTH_GUARD_CRED_PATH/ directory." + fi + fi +fi + diff --git a/scripts/hub_cluster_config_setup.sh b/scripts/hub_cluster_config_setup.sh new file mode 100644 index 0000000..7dc84c4 --- /dev/null +++ b/scripts/hub_cluster_config_setup.sh @@ -0,0 +1,93 @@ +#!/bin/sh + +# Ensure all required environment variables are set +: "${SERVER_NAME:?Environment variable SERVER_NAME is required}" +: "${SERVER_ADDRESS:?Environment variable SERVER_ADDRESS is required}" +: "${HTTP_ADDRESS:?Environment variable HTTP_ADDRESS is required}" +: "${JS_DOMAIN:?Environment variable JS_DOMAIN is required}" +: "${STORE_PATH:?Environment variable STORE_PATH is required}" +: "${CLUSTER_PORT:?Environment variable CLUSTER_PORT is required}" +: "${CLUSTER_SEED_ADDRESSES:?Environment variable CLUSTER_SEED_ADDRESSES is required}" +: "${CLUSTER_USER_NAME:?Environment variable CLUSTER_USER_NAME is required}" +: "${CLUSTER_USER_PW:?Environment variable CLUSTER_USER_PW is required}" +: "${RESOLVER_PATH:?Environment variable RESOLVER_PATH is required}" + +# Define the output config file +CONFIG_FILE="nats-cluster-server.conf" + +# Create the configuration file +cat > "$CONFIG_FILE" < "$CONFIG_FILE" <&1)" | grep -oP "signing key\s*\K\S+")" -ROLE_NAME_ADMIN="admin_role" -nsc edit signing-key --sk $SIGNING_KEY_ADMIN --role $ROLE_NAME_ADMIN --allow-pub "ADMIN_>" --allow-sub "ADMIN_>" --allow-pub-response +nsc edit account --name $ADMIN_ACCOUNT --js-streams -1 --js-consumer -1 --js-mem-storage 1G --js-disk-storage 5G --conns -1 --leaf-conns -1 + +ADMIN_SK="$(echo "$(nsc edit account -n $ADMIN_ACCOUNT --sk generate 2>&1)" | grep -oP "signing key\s*\K\S+")" +ADMIN_ROLE_NAME="admin_role" +nsc edit signing-key --sk $ADMIN_SK --role $ADMIN_ROLE_NAME --allow-pub "ADMIN.>","AUTH.>","WORKLOAD.>","\$JS.API.>","\$SYS.>","_INBOX.>","_INBOX_*.>","_workload_inbox_*.>","_auth_inbox_*.>" --allow-sub "ADMIN.>","AUTH.>","WORKLOAD.>","\$JS.API.>","\$SYS.>","_INBOX.>","_INBOX_*.>","_workload_inbox_orchestrator.>","_auth_inbox_orchestrator.>" --allow-pub-response + +# Step 3: Create AUTH with JetStream with non-scoped signing key +nsc add account --name $AUTH_ACCOUNT +nsc edit account --name $AUTH_ACCOUNT --sk generate --js-streams -1 --js-consumer -1 --js-mem-storage 1G --js-disk-storage 5G --conns -1 --leaf-conns -1 +AUTH_ACCOUNT_PUBKEY=$(nsc describe account $AUTH_ACCOUNT --field sub | jq -r) +AUTH_SK_ACCOUNT_PUBKEY=$(nsc describe account $AUTH_ACCOUNT --field 'nats.signing_keys[0]' | tr -d '"') + +# Step 4: Create "Sentinel" User in AUTH Account +nsc add user --name $AUTH_GUARD_USER --account $AUTH_ACCOUNT --deny-pubsub ">" -# Step 3: Create WORKLOAD Account with JetStream and scoped signing key +# Step 5: Create WORKLOAD Account with JetStream and scoped signing keys nsc add account --name $WORKLOAD_ACCOUNT -nsc edit account --name $WORKLOAD_ACCOUNT --js-streams -1 --js-consumer -1 --js-mem-storage 1G --js-disk-storage 5G -SIGNING_KEY_WORKLOAD="$(echo "$(nsc edit account -n $WORKLOAD_ACCOUNT --sk generate 2>&1)" | grep -oP "signing key\s*\K\S+")" -ROLE_NAME_WORKLOAD="workload-role" -nsc edit signing-key --sk $SIGNING_KEY_WORKLOAD --role $ROLE_NAME_WORKLOAD --allow-pub "WORKLOAD.>" --allow-sub "WORKLOAD.>" --allow-pub-response +nsc edit account --name $WORKLOAD_ACCOUNT --js-streams -1 --js-consumer -1 --js-mem-storage 1G --js-disk-storage 5G --conns -1 --leaf-conns -1 +WORKLOAD_SK="$(echo "$(nsc edit account -n $WORKLOAD_ACCOUNT --sk generate 2>&1)" | grep -oP "signing key\s*\K\S+")" +WORKLOAD_ROLE_NAME="workload_role" +nsc edit signing-key --sk $WORKLOAD_SK --role $WORKLOAD_ROLE_NAME --allow-pub "WORKLOAD.>","_INBOX_{{tag(pubkey)}}.>","_workload_inbox_{{tag(pubkey)}}.>" --allow-sub "WORKLOAD.{{tag(pubkey)}}.*","_INBOX_{{tag(pubkey)}}.>","_workload_inbox_{{tag(pubkey)}}.>" --allow-pub-response -# Step 4: Create User "orchestrator" in ADMIN Account // noauth -nsc add user --name admin --account $ADMIN_ACCOUNT +# Step 6: Create Operator User in ADMIN Account (for use in Orchestrator) +nsc add user --name $ADMIN_USER --account $ADMIN_ACCOUNT -K $ADMIN_ROLE_NAME -# Step 5: Create User "orchestrator" in WORKLOAD Account -nsc add user --name orchestrator --account $WORKLOAD_ACCOUNT +# Step 7: Create Operator User in AUTH Account (used in auth service) +nsc add user --name $ORCHESTRATOR_AUTH_USER --account $AUTH_ACCOUNT --allow-pubsub ">" +AUTH_USER_PUBKEY=$(nsc describe user --name $ORCHESTRATOR_AUTH_USER --account $AUTH_ACCOUNT --field sub | jq -r) +echo "assigned auth user pubkey: $AUTH_USER_PUBKEY" -# Step 6: Generate JWT files -nsc describe operator --raw --output-file $JWT_OUTPUT_DIR/holo_operator.jwt -nsc describe account --name SYS --raw --output-file $JWT_OUTPUT_DIR/sys_account.jwt -nsc describe account --name $WORKLOAD_ACCOUNT --raw --output-file $JWT_OUTPUT_DIR/workload_account.jwt -nsc describe account --name $ADMIN_ACCOUNT --raw --output-file $JWT_OUTPUT_DIR/admin_account.jwt +# Step 8: Configure Auth Callout +echo $AUTH_ACCOUNT_PUBKEY +echo $AUTH_SK_ACCOUNT_PUBKEY +nsc edit authcallout --account $AUTH_ACCOUNT --allowed-account "\"$AUTH_ACCOUNT_PUBKEY\",\"$AUTH_SK_ACCOUNT_PUBKEY\"" --auth-user $AUTH_USER_PUBKEY -# Step 7: Generate Resolver Config -nsc generate config --nats-resolver --sys-account $SYS_ACCOUNT --force --config-file $RESOLVER_FILE +# Step 9: Generate JWT files +nsc generate creds --name $ORCHESTRATOR_AUTH_USER --account $AUTH_ACCOUNT > $LOCAL_CREDS_DIR/$ORCHESTRATOR_AUTH_USER.creds # --> local to hub exclusively +nsc describe operator --raw --output-file $SHARED_CREDS_DIR/$OPERATOR.jwt +nsc describe account --name SYS --raw --output-file $SHARED_CREDS_DIR/$SYS_ACCOUNT.jwt +nsc generate creds --name $AUTH_GUARD_USER --account $AUTH_ACCOUNT --output-file $SHARED_CREDS_DIR/$AUTH_GUARD_USER.creds + +# ADMIN_SK=$(nsc describe account ADMIN --field 'nats.signing_keys[0].key' | tr -d '"') +extract_signing_key ADMIN $ADMIN_SK +echo "extracted ADMIN signing key" + +extract_signing_key AUTH $AUTH_SK_ACCOUNT_PUBKEY +echo "extracted AUTH signing key" -# Step 8: Push credentials to NATS server -nsc push -A +extract_signing_key AUTH_ROOT $AUTH_ACCOUNT_PUBKEY +echo "extracted AUTH root key" + +# Step 10: Generate Resolver Config +nsc generate config --nats-resolver --sys-account $SYS_ACCOUNT --force --config-file $RESOLVER_FILE -echo "Setup complete. JWTs and resolver file are in the $JWT_OUTPUT_DIR/ directory." +echo "Setup complete. Shared JWTs and resolver file are in the $SHARED_CREDS_DIR/ directory. Private creds are in the $LOCAL_CREDS_DIR/ directory." +echo "!! Don't forget to start the NATS server and push the credentials to the server with 'nsc push -A' !!"