diff --git a/.env.example b/.env.example index 3ca30fd..604585a 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,12 @@ LOCAL_CREDS_PATH="" # ORCHESTRATOR MONGO_URI="mongodb://:" +ORCHESTRATOR_SIGNING_AUTH_NKEY_PATH="/.local/share/nats/nsc/local_creds/AUTH_ROOT_SK.nk" +ORCHESTRATOR_ROOT_AUTH_NKEY_PATH="/.local/share/nats/nsc/local_creds/AUTH_SK.nk" # HOSTING AGENT -HOST_CREDS_FILE_PATH = "ops/admin.creds" LEAF_SERVER_DEFAULT_LISTEN_PORT="4111" -LEAF_SERVER_USER = "test-user" -LEAF_SERVER_PW = "pw-123456789" \ No newline at end of file +HOSTING_AGENT_HOST_NKEY_PATH="/host.nk" +HOSTING_AGENT_SYS_NKEY_PATH="/sys.nk" +HPOS_CONFIG_PATH="path/to/file.config"; +DEVICE_SEED_DEFAULT_PASSWORD="device_pw_1234" diff --git a/Cargo.lock b/Cargo.lock index d5c73ae..f7f752c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,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" @@ -237,6 +243,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[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" @@ -317,12 +329,41 @@ 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" @@ -386,6 +427,44 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "authentication" +version = "0.0.1" +dependencies = [ + "anyhow", + "async-nats", + "async-trait", + "base32", + "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" @@ -432,6 +511,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 = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + +[[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" @@ -533,6 +634,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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.3.1", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -724,6 +846,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 = "cmake" version = "0.1.54" @@ -765,6 +896,18 @@ dependencies = [ "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" @@ -808,6 +951,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" @@ -929,6 +1081,12 @@ dependencies = [ "syn 2.0.98", ] +[[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.8.0" @@ -1087,6 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "serde", "signature", ] @@ -1171,6 +1330,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" @@ -1183,6 +1364,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.1.0" @@ -1214,6 +1407,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[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" @@ -1392,12 +1591,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" @@ -1419,6 +1643,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" @@ -1494,6 +1724,7 @@ version = "0.0.1" dependencies = [ "anyhow", "async-nats", + "authentication", "bson", "bytes", "chrono", @@ -1503,6 +1734,8 @@ dependencies = [ "ed25519-dalek", "env_logger", "futures", + "hpos-config-core", + "hpos-config-seed-bundle-explorer", "hpos-hal", "jsonwebtoken", "log", @@ -1534,6 +1767,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" @@ -1835,7 +2103,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", ] @@ -1948,6 +2216,30 @@ version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +[[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 = "libloading" version = "0.8.6" @@ -1958,6 +2250,34 @@ dependencies = [ "windows-targets 0.52.6", ] +[[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" @@ -1999,7 +2319,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ - "autocfg", + "autocfg 1.4.0", "scopeguard", ] @@ -2142,6 +2462,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[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.5" @@ -2269,6 +2595,12 @@ dependencies = [ "signatory", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -2339,7 +2671,17 @@ 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]] @@ -2357,6 +2699,18 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[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" @@ -2370,6 +2724,7 @@ dependencies = [ "actix-web", "anyhow", "async-nats", + "authentication", "bson", "bytes", "chrono", @@ -2688,6 +3043,25 @@ 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" @@ -2698,7 +3072,7 @@ dependencies = [ "libc", "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc", + "rand_hc 0.2.0", ] [[package]] @@ -2712,6 +3086,16 @@ dependencies = [ "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" @@ -2732,6 +3116,21 @@ dependencies = [ "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" @@ -2750,6 +3149,15 @@ dependencies = [ "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" @@ -2759,6 +3167,59 @@ 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 = "rayon" version = "1.10.0" @@ -2779,6 +3240,15 @@ dependencies = [ "crossbeam-utils", ] +[[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]] name = "redox_syscall" version = "0.5.9" @@ -2862,6 +3332,46 @@ dependencies = [ "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 = "rust-embed" version = "8.6.0" @@ -3408,7 +3918,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ - "autocfg", + "autocfg 1.4.0", ] [[package]] @@ -3427,6 +3937,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.5.2" @@ -3538,6 +4063,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" @@ -3576,6 +4113,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.17.1" @@ -3984,6 +4532,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" @@ -4103,6 +4663,12 @@ dependencies = [ "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" @@ -4615,6 +5181,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" @@ -4636,7 +5213,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.98", - "synstructure", + "synstructure 0.13.1", ] [[package]] @@ -4678,7 +5255,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.98", - "synstructure", + "synstructure 0.13.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index faf6e0b..d6a8cdc 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", "rust/netdiag", diff --git a/nix/modules/nixos/holo-host-agent.nix b/nix/modules/nixos/holo-host-agent.nix index 236ecfa..03e44cb 100644 --- a/nix/modules/nixos/holo-host-agent.nix +++ b/nix/modules/nixos/holo-host-agent.nix @@ -64,6 +64,41 @@ in default = "${cfg.nats.listenHost}:${builtins.toString cfg.nats.listenPort}"; }; + nscPath = lib.mkOption { + type = lib.types.path; + default = "/var/lib/.local/share/nats/nsc"; + }; + + sharedCredsPath = lib.mkOption { + type = lib.types.path; + default = "${cfg.nats.nscPath}/shared_creds"; + }; + + localCredsPath = lib.mkOption { + type = lib.types.path; + default = "${cfg.nats.nscPath}/local_creds"; + }; + + hostNkeyPath = lib.mkOption { + type = lib.types.path; + default = "${cfg.nats.localCredsPath}/host.nk"; + }; + + sysNkeyPath = lib.mkOption { + type = lib.types.path; + default = "${cfg.nats.localCredsPath}/sys.nk"; + }; + + hposCredsPath = lib.mkOption { + type = lib.types.path; + default = "/var/lib/holo-host-agent/server-key-config.json"; + }; + + hposCredsPw = lib.mkOption { + type = lib.types.str; + default = "pass"; + }; + hub = { url = lib.mkOption { type = lib.types.str; @@ -103,6 +138,12 @@ in { RUST_LOG = cfg.rust.log; RUST_BACKTRACE = cfg.rust.backtrace; + NSC_PATH = cfg.nats.nscPath; + LOCAL_CREDS_PATH = cfg.nats.localCredsPath; + HOSTING_AGENT_HOST_NKEY_PATH = cfg.nats.hostNkeyPath; + HOSTING_AGENT_SYS_NKEY_PATH = cfg.nats.sysNkeyPath; + HPOS_CONFIG_PATH = cfg.nats.hposCredsPath; + DEVICE_SEED_DEFAULT_PASSWORD = builtins.toString cfg.nats.hposCredsPw; NATS_LISTEN_PORT = builtins.toString cfg.nats.listenPort; } // lib.attrsets.optionalAttrs (cfg.nats.url != null) { @@ -113,6 +154,14 @@ in pkgs.nats-server ]; + preStart = '' + echo "Start Host Auth Setup" + mkdir -p ${cfg.nats.hostNkeyPath} + mkdir -p ${cfg.nats.sysNkeyPath} + mkdir -p ${cfg.nats.hposCredsPath} + echo "Finshed Host Auth Setup" + ''; + script = let extraDaemonizeArgsList = lib.attrsets.mapAttrsToList ( diff --git a/rust/clients/host_agent/Cargo.toml b/rust/clients/host_agent/Cargo.toml index 71d144a..c10d89c 100644 --- a/rust/clients/host_agent/Cargo.toml +++ b/rust/clients/host_agent/Cargo.toml @@ -29,7 +29,10 @@ bytes = "1.8.0" rand = "0.8.5" tempfile = "3.15.0" machineid-rs = "1.2.4" +netdiag = { path = "../../netdiag" } +hpos-hal = { path = "../../hpos-hal" } util_libs = { path = "../../util_libs" } workload = { path = "../../services/workload" } -hpos-hal = { path = "../../hpos-hal" } -netdiag = { path = "../../netdiag" } +authentication = { path = "../../services/authentication" } +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 e31eb14..ec106e2 100644 --- a/rust/clients/host_agent/src/agent_cli.rs +++ b/rust/clients/host_agent/src/agent_cli.rs @@ -38,6 +38,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..2f1c58e --- /dev/null +++ b/rust/clients/host_agent/src/auth/config.rs @@ -0,0 +1,58 @@ +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..372a9a3 --- /dev/null +++ b/rust/clients/host_agent/src/auth/init.rs @@ -0,0 +1,176 @@ +/* +This client is associated with the: + - AUTH account + - auth guard user + +Nb: Once the host and hoster are validated, and the host creds file is created, +...this client should safely close and then 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 + - calling the host auth service to: + - validate hoster hc pubkey and email + - send the host pubkey to the orchestrator to register with the orchestrator key resovler + - get user jwt from orchestrator and create user creds file with provided file path + - 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, RequestErrorKind}; +use authentication::{ + types::{AuthGuardPayload, AuthJWTPayload, AuthJWTResult, AuthState}, + AUTH_SRV_SUBJ, VALIDATE_AUTH_SUBJECT, +}; +use hpos_hal::inventory::HoloInventory; +use std::str::FromStr; +use std::time::Duration; +use textnonce::TextNonce; +use util_libs::nats::jetstream_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..."); + log::trace!( + "Host Agent Keys before authentication request: {:#?}", + host_agent_keys + ); + + // ==================== Fetch Config File & Call NATS AuthCallout Service to Authenticate Host & Hoster ============================================= + let nonce = TextNonce::new().to_string(); + + // 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_path = if let AuthCredType::Guard(creds) = host_agent_keys.creds.clone() { + creds + } else { + return Err(async_nats::Error::from( + "Failed to locate Auth Guard credentials", + )); + }; + let user_unique_inbox = &format!( + "{}.{}", + HOST_AUTH_CLIENT_INBOX_PREFIX, + host_agent_keys.host_pubkey.to_lowercase() + ); + + // Connect to Nats server as auth guard and call NATS AuthCallout + let nats_url = jetstream_client::get_nats_url(); + let auth_guard_client = async_nats::ConnectOptions::new() + .name(HOST_AUTH_CLIENT_NAME.to_string()) + .custom_inbox_prefix(user_unique_inbox.to_string()) + .ping_interval(Duration::from_secs(10)) + .request_timeout(Some(Duration::from_secs(30))) + .token(user_auth_token) + .credentials_file(&user_creds_path) + .await? + .connect(nats_url) + .await?; + + let server_info = auth_guard_client.server_info(); + println!( + "User connected to server on port {}. Connection State: {:#?}", + server_info.port, + auth_guard_client.connection_state() + ); + + let server_node_id = server_info.server_id; + log::trace!("Host Auth Client: Retrieved Node ID: {}", server_node_id); + + // ==================== Handle Host User and SYS Authoriation ============================================================ + 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(&signature)?, + ); + + println!( + "About to send out the {}.{} message", + AUTH_SRV_SUBJ, VALIDATE_AUTH_SUBJECT + ); + let response_msg = match auth_guard_client + .request_with_headers( + format!("{}.{}", AUTH_SRV_SUBJ, VALIDATE_AUTH_SUBJECT), + headers, + payload_bytes.into(), + ) + .await + { + Ok(msg) => msg, + Err(e) => { + log::error!("{:#?}", e); + if let RequestErrorKind::TimedOut = e.kind() { + 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)?; + if (auth_guard_client + .publish( + unauthenticated_user_diagnostics_subject.to_string(), + payload_bytes.into(), + ) + .await) + .is_ok() + { + return Ok((host_agent_keys, auth_guard_client)); + } + } + return Err(async_nats::Error::from(e)); + } + }; + + println!( + "Received AUTH response: {:#?}", + serde_json::from_slice::(&response_msg.payload) + .expect("failed to serde_json deserialize msg response") + ); + + if let Ok(auth_response) = serde_json::from_slice::(&response_msg.payload) { + match auth_response.status { + AuthState::Authorized => { + host_agent_keys = host_agent_keys + .save_host_creds(auth_response.host_jwt, auth_response.sys_jwt) + .await?; + } + _ => { + log::error!("got unexpected AUTH State : {:?}", auth_response); + } + } + }; + + 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..89e0178 --- /dev/null +++ b/rust/clients/host_agent/src/auth/utils.rs @@ -0,0 +1,58 @@ +use crate::{auth, keys}; +use anyhow::Result; +use data_encoding::BASE64URL_NOPAD; +use hpos_hal::inventory::HoloInventory; + +/// 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) +} + +pub 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 and wait 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::hours(24); + + while max_time_interval > now.signed_duration_since(start) { + let pubkey_lowercase = keys.host_pubkey.to_string().to_lowercase(); + let unauthenticated_user_inventory_subject = + format!("INVENTORY.update.{}.unauthenticated", pubkey_lowercase); + let inventory = HoloInventory::from_host(); + let payload_bytes = serde_json::to_vec(&inventory)?; + + if let Err(e) = auth_guard_client + .publish(unauthenticated_user_inventory_subject, payload_bytes.into()) + .await + { + log::error!( + "Encountered error when sending inventory as unauthenticated user. Err={:#?}", + e + ); + }; + tokio::time::sleep(chrono::TimeDelta::hours(24).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/host_agent/src/hostd/gen_leaf_server.rs b/rust/clients/host_agent/src/hostd/gen_leaf_server.rs index 38f36da..b3a4cb2 100644 --- a/rust/clients/host_agent/src/hostd/gen_leaf_server.rs +++ b/rust/clients/host_agent/src/hostd/gen_leaf_server.rs @@ -107,7 +107,7 @@ pub async fn run( nats_url:nats_url.clone(), name:HOST_AGENT_CLIENT_NAME.to_string(), inbox_prefix: Default::default(), - credentials_path: Default::default(), + credentials: Default::default(), ping_interval:Some(Duration::from_secs(10)), request_timeout:Some(Duration::from_secs(29)), listeners: Default::default(), diff --git a/rust/clients/host_agent/src/hostd/workload.rs b/rust/clients/host_agent/src/hostd/workload.rs index 5d23d49..3b787f5 100644 --- a/rust/clients/host_agent/src/hostd/workload.rs +++ b/rust/clients/host_agent/src/hostd/workload.rs @@ -15,17 +15,15 @@ use async_nats::Message; use std::{path::PathBuf, sync::Arc, time::Duration}; use util_libs::nats::{ jetstream_client, - types::{ConsumerBuilder, EndpointType, JsClientBuilder, JsServiceBuilder}, + types::{ConsumerBuilder, Credentials, EndpointType, JsClientBuilder, JsServiceBuilder}, }; use workload::{ host_api::HostWorkloadApi, types::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 = "_WORKLOAD_INBOX"; +const HOST_AGENT_INBOX_PREFIX: &str = "_HPOS_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, @@ -41,20 +39,21 @@ pub async fn run( let nats_url = jetstream_client::get_nats_url(); log::info!("nats_url : {}", nats_url); - let event_listeners = jetstream_client::get_event_listeners(); + let host_creds = host_creds_path + .to_owned() + .map(Credentials::Path) + .ok_or_else(|| async_nats::Error::from("error"))?; - // Spin up Nats Client and loaded in the Js Stream Service + // Spin up Nats Client and load the Js Stream Service let mut host_workload_client = jetstream_client::JsClient::new(JsClientBuilder { nats_url: nats_url.clone(), name: HOST_AGENT_CLIENT_NAME.to_string(), inbox_prefix: format!("{}.{}", HOST_AGENT_INBOX_PREFIX, pubkey_lowercase), - credentials_path: host_creds_path - .as_ref() - .map(|path| path.to_string_lossy().to_string()), + credentials: Some(vec![host_creds.clone()]), ping_interval: Some(Duration::from_secs(10)), request_timeout: Some(Duration::from_secs(29)), listeners: vec![jetstream_client::with_event_listeners( - event_listeners.clone(), + jetstream_client::get_event_listeners(), )], }) .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..255abea --- /dev/null +++ b/rust/clients/host_agent/src/keys.rs @@ -0,0 +1,357 @@ +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::jetstream_client::{get_local_creds_path, 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 assigned 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("HOSTING_AGENT_HOST_NKEY_PATH") + .context("Cannot read HOSTING_AGENT_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("HOSTING_AGENT_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 user (with or without a sys user) + pub fn try_from_storage( + maybe_host_creds_path: &Option, + maybe_sys_creds_path: &Option, + ) -> Result { + let host_key_path: String = std::env::var("HOSTING_AGENT_HOST_NKEY_PATH") + .context("Cannot read HOSTING_AGENT_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 auth_guard_creds = + PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "AUTH", "auth_guard"))?; + + let host_user_name = format!("host_user_{}", host_pk); + let host_creds_path = maybe_host_creds_path.to_owned().map_or_else( + || PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "WORKLOAD", &host_user_name)), + Ok, + )?; + + let sys_user_name = format!("sys_user_{}", host_pk); + let sys_creds_path = maybe_sys_creds_path.to_owned().map_or_else( + || PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "SYS", &sys_user_name)), + Ok, + )?; + + let mut default_keys = Self { + host_keypair, + host_pubkey: host_pk, + local_sys_keypair: None, + local_sys_pubkey: None, + creds: AuthCredType::Guard(auth_guard_creds), // Set auth_guard_creds as default user cred + }; + + let sys_key_path = std::env::var("HOSTING_AGENT_SYS_NKEY_PATH") + .context("Cannot read HOSTING_AGENT_SYS_NKEY_PATH from env var")?; + let keys = match try_read_keypair_from_file(PathBuf::from_str(&sys_key_path)?)? { + Some(kp) => { + let local_sys_pk = kp.public_key(); + default_keys.local_sys_keypair = Some(kp); + default_keys.local_sys_pubkey = Some(local_sys_pk); + default_keys + } + None => default_keys, + }; + + 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, + maybe_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 maybe_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!("{}/{}", get_local_creds_path(), "host.jwt"))?; + log::trace!("host_path={:?}", host_path); + write_to_file(host_path.clone(), host_user_jwt.as_bytes())?; + log::trace!("Wrote JWT to host file"); + + let sys_path = PathBuf::from_str(&format!("{}/{}", get_local_creds_path(), "sys.jwt"))?; + log::trace!("sys_path={:?}", sys_path); + write_to_file(sys_path.clone(), host_sys_user_jwt.as_bytes())?; + log::trace!("Wrote JWT to sys file"); + + // Import host user jwt to local nsc resolver + // TODO: Determine why the following works in cmd line, but doesn't seem to work when run in current program / run + Command::new("nsc") + .arg("import") + .arg("user") + .arg("--file") + .arg(format!("{:?}", host_path)) + .output() + .context("Failed to add import new host user on hosting agent.")?; + log::trace!("Imported host user successfully"); + + // Import sys user jwt to local nsc resolver + Command::new("nsc") + .arg("import") + .arg("user") + .arg("--file") + .arg(format!("{:?}", sys_path)) + .output() + .context("Failed to add import new sys user on hosting agent.")?; + log::trace!("Imported sys user successfully"); + + // Save user creds and sys creds local to hosting agent + let host_user_name = format!("host_user_{}", self.host_pubkey); + let host_creds_path = + PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "WORKLOAD", &host_user_name))?; + Command::new("nsc") + .args([ + "generate", + "creds", + "--name", + &host_user_name, + "--account", + "WORKLOAD", + "--output-file", + &host_creds_path.to_string_lossy(), + ]) + .output() + .context("Failed to add host user key to hosting agent")?; + log::trace!( + "Generated host user creds. creds_path={:?}", + host_creds_path + ); + + let mut sys_creds_file_name = None; + if self.local_sys_pubkey.as_ref().is_some() { + let sys_user_name = format!("sys_user_{}", self.host_pubkey); + let path = PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "SYS", &sys_user_name))?; + Command::new("nsc") + .args([ + "generate", + "creds", + "--name", + &sys_user_name, + "--account", + "SYS", + "--output-file", + &path.to_string_lossy(), + ]) + .output() + .context("Failed to add sys user key to hosting agent")?; + log::trace!("Generated sys user creds. creds_path={:?}", path); + sys_creds_file_name = Some(path); + } + + self.to_owned() + .add_creds_paths(host_creds_path, 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 6a67832..3f174d3 100644 --- a/rust/clients/host_agent/src/main.rs +++ b/rust/clients/host_agent/src/main.rs @@ -11,8 +11,10 @@ This client is responsible for subscribing the host agent to workload stream end */ 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; @@ -47,10 +49,31 @@ async fn main() -> Result<(), AgentCliError> { } async fn daemonize(args: &DaemonzeArgs) -> Result<(), async_nats::Error> { - // let host_pubkey = auth::init_agent::run().await?; + 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| { + log::error!("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 = auth::utils::run_auth_loop(host_agent_keys).await?; + } + + log::trace!( + "Host Agent Keys after successful authentication: {:#?}", + host_agent_keys + ); + + // Once authenticated, start leaf server and run workload api calls. let bare_client = hostd::gen_leaf_server::run( &args.nats_leafnode_server_name, - &args.nats_leafnode_client_creds_path, + &host_agent_keys.get_host_creds_path(), &args.store_dir, args.hub_url.clone(), args.hub_tls_insecure, @@ -61,8 +84,8 @@ async fn daemonize(args: &DaemonzeArgs) -> Result<(), async_nats::Error> { bare_client.close().await?; let host_workload_client = hostd::workload::run( - "host_id_placeholder>", - &args.nats_leafnode_client_creds_path, + &host_agent_keys.host_pubkey, + &host_agent_keys.get_host_creds_path(), ) .await?; diff --git a/rust/clients/orchestrator/Cargo.toml b/rust/clients/orchestrator/Cargo.toml index c961b0c..3bb256a 100644 --- a/rust/clients/orchestrator/Cargo.toml +++ b/rust/clients/orchestrator/Cargo.toml @@ -30,3 +30,4 @@ utoipa-swagger-ui = { version = "9", features = [ utoipa = { version = "5", features = ["actix_extras"] } 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..5c9ade5 --- /dev/null +++ b/rust/clients/orchestrator/src/auth.rs @@ -0,0 +1,313 @@ +/* +This client is associated with the: + - AUTH account + - (the orchestrator's) auth user + +This client is responsible for: + - initalizing connection and handling interface with db + - registering the `handle_auth_callout` and `handle_auth_validation` fns as core nats service group endpoints: + - NB: These endpoints will consider authentiction successful if: + - user signature is valid + - hoster pubkey is valid + - hoster email is valid + - succesfully paired hoster and host in mongodb + - succesfully added user to resolver on hub (orchestrator side) + - succesfully created signed jwt for user + - succesfully added user jwt file to user collection in mongodb (with ttl) + - keeping service running until explicitly cancelled out +*/ + +use anyhow::{anyhow, Context, Result}; +use async_nats::service::ServiceExt; +use async_nats::Client; +use authentication::{ + types::AuthErrorPayload, AuthServiceApi, AUTH_CALLOUT_SUBJECT, AUTH_SRV_DESC, AUTH_SRV_NAME, + AUTH_SRV_SUBJ, AUTH_SRV_VERSION, VALIDATE_AUTH_SUBJECT, +}; +use futures::StreamExt; +use mongodb::Client as MongoDBClient; +use nkeys::KeyPair; +use std::fs::File; +use std::io::Read; +use std::path::PathBuf; +use std::str::FromStr; +use std::{sync::Arc, time::Duration}; +use util_libs::nats::{ + jetstream_client::{get_nats_creds_by_nsc, get_nats_url}, + types::CreateResponse, +}; + +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(db_client: MongoDBClient) -> Result { + let admin_account_creds_path = + PathBuf::from_str(&get_nats_creds_by_nsc("HOLO", "AUTH", "auth"))?; + + // Root Keypair associated with AUTH account + let root_account_key_path = std::env::var("ORCHESTRATOR_ROOT_AUTH_NKEY_PATH") + .context("Cannot read ORCHESTRATOR_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 + ) + })?, + ); + let root_account_pubkey = root_account_keypair.public_key().clone(); + + // AUTH Account Signing Keypair associated with the `auth` user + let signing_account_key_path = std::env::var("ORCHESTRATOR_SIGNING_AUTH_NKEY_PATH") + .context("Cannot read ORCHESTRATOR_SIGNING_AUTH_NKEY_PATH from env var")?; + 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 + ) + })?, + ); + let signing_account_pubkey = signing_account_keypair.public_key().clone(); + + // ==================== Setup NATS ==================== + let nats_url = get_nats_url(); + let nats_connect_timeout_secs: u64 = 180; + + let orchestrator_auth_client = tokio::select! { + client = async {loop { + let orchestrator_auth_client = async_nats::ConnectOptions::new() + .name(ORCHESTRATOR_AUTH_CLIENT_NAME.to_string()) + .custom_inbox_prefix(ORCHESTRATOR_AUTH_CLIENT_INBOX_PREFIX.to_string()) + .ping_interval(Duration::from_secs(10)) + .request_timeout(Some(Duration::from_secs(30))) + .credentials_file(&admin_account_creds_path).await.map_err(|e| anyhow::anyhow!("Error loading credentials file: {e}"))? + .connect(nats_url.clone()) + .await + .map_err(|e| anyhow::anyhow!("Connecting Orchestrator Auth Client to NATS via {nats_url}: {e}")); + + match orchestrator_auth_client { + Ok(client) => break Ok::(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()); + } + }; + + // ==================== Setup API & Register Endpoints ==================== + // Generate the Auth API with access to db + let auth_api = AuthServiceApi::new(&db_client).await?; + let auth_api_clone = auth_api.clone(); + + // Register Auth Service for Orchestrator and spawn listener for processing + let auth_service = orchestrator_auth_client + .service_builder() + .description(AUTH_SRV_DESC) + .start(AUTH_SRV_NAME, AUTH_SRV_VERSION) + .await?; + + // Auth Callout Service + let sys_user_group = auth_service.group("$SYS").group("REQ").group("USER"); + let mut auth_callout = sys_user_group.endpoint(AUTH_CALLOUT_SUBJECT).await?; + let auth_service_info = auth_service.info().await; + let orchestrator_auth_client_clone = orchestrator_auth_client.clone(); + + tokio::spawn(async move { + while let Some(request) = auth_callout.next().await { + let signing_account_kp = Arc::clone(&signing_account_keypair.clone()); + let signing_account_pk = signing_account_pubkey.clone(); + let root_account_kp = Arc::clone(&root_account_keypair.clone()); + let root_account_pk = root_account_pubkey.clone(); + + let maybe_reply = request.message.reply.clone(); + match auth_api_clone + .handle_auth_callout( + Arc::new(request.message), + signing_account_kp, + signing_account_pk, + root_account_kp, + root_account_pk, + ) + .await + { + Ok(r) => { + let res_bytes = r.get_response(); + if let Some(reply_subject) = maybe_reply { + let _ = orchestrator_auth_client_clone + .publish(reply_subject, res_bytes) + .await + .map_err(|e| { + log::error!( + "{}Failed to send success response. Res={:?} Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + r, + e + ); + }); + } + } + Err(e) => { + let mut err_payload = AuthErrorPayload { + service_info: auth_service_info.clone(), + group: "$SYS.REQ.USER".to_string(), + endpoint: AUTH_CALLOUT_SUBJECT.to_string(), + error: format!("{}", e), + }; + + log::error!( + "{}Failed to handle the endpoint handler. Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + err_payload + ); + + let err_response = serde_json::to_vec(&err_payload).unwrap_or_else(|e| { + err_payload.error = e.to_string(); + log::error!( + "{}Failed to deserialize error response. Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + err_payload + ); + vec![] + }); + + let _ = orchestrator_auth_client_clone + .publish( + format!("{}.ERROR", ORCHESTRATOR_AUTH_CLIENT_INBOX_PREFIX), + err_response.into(), + ) + .await + .map_err(|e| { + err_payload.error = e.to_string(); + log::error!( + "{}Failed to send error response. Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + err_payload + ); + }); + } + } + } + }); + + // Auth Validation Service + let v1_auth_group = auth_service.group(AUTH_SRV_SUBJ); // .group("V1") + let mut auth_validation = v1_auth_group.endpoint(VALIDATE_AUTH_SUBJECT).await?; + let orchestrator_auth_client_clone = orchestrator_auth_client.clone(); + + tokio::spawn(async move { + while let Some(request) = auth_validation.next().await { + let maybe_reply = request.message.reply.clone(); + match auth_api + .handle_auth_validation(Arc::new(request.message)) + .await + { + Ok(r) => { + let res_bytes = r.get_response(); + if let Some(reply_subject) = maybe_reply { + let _ = orchestrator_auth_client_clone + .publish(reply_subject, res_bytes) + .await + .map_err(|e| { + log::error!( + "{}Failed to send success response. Res={:?} Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + r, + e + ); + }); + } + } + Err(e) => { + let auth_service_info = auth_service.info().await; + let mut err_payload = AuthErrorPayload { + service_info: auth_service_info, + group: AUTH_SRV_SUBJ.to_string(), + endpoint: VALIDATE_AUTH_SUBJECT.to_string(), + error: format!("{}", e), + }; + log::error!( + "{}Failed to handle the endpoint handler. Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + err_payload + ); + let err_response = serde_json::to_vec(&err_payload).unwrap_or_else(|e| { + err_payload.error = e.to_string(); + log::error!( + "{}Failed to deserialize error response. Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + err_payload + ); + vec![] + }); + let _ = orchestrator_auth_client_clone + .publish("AUTH.ERROR", err_response.into()) + .await + .map_err(|e| { + err_payload.error = e.to_string(); + log::error!( + "{}Failed to send error response. Err={:?}", + "NATS-SERVICE-LOG::AUTH::", + err_payload + ); + }); + } + } + } + }); + + log::debug!("Orchestrator Auth Service is running. Waiting for requests..."); + + // ==================== Close and Clean Client ==================== + // Only exit program when explicitly requested + tokio::signal::ctrl_c().await?; + + log::debug!("Closing orchestrator auth service..."); + + // Close client and drain internal buffer before exiting to make sure all messages are sent + orchestrator_auth_client.drain().await?; + log::debug!("Closed orchestrator auth service"); + + Ok(orchestrator_auth_client) +} + +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 19aa705..176e208 100644 --- a/rust/clients/orchestrator/src/main.rs +++ b/rust/clients/orchestrator/src/main.rs @@ -1,18 +1,54 @@ +mod auth; mod extern_api; mod utils; mod workloads; use anyhow::Result; +use async_nats::Client; use dotenv::dotenv; +use mongodb::{options::ClientOptions, Client as MongoDBClient}; +use tokio::task::spawn; +use util_libs::db::mongodb::get_mongodb_url; #[tokio::main] async fn main() -> Result<(), async_nats::Error> { dotenv().ok(); env_logger::init(); - // Run auth service - // TODO: invoke auth service (once ready) - // Run workload service - workloads::run().await?; + // Setup MongoDB Client + let mongo_uri: String = get_mongodb_url(); + let db_client_options = ClientOptions::parse(mongo_uri).await?; + let db_client = MongoDBClient::with_options(db_client_options)?; + let thread_db_client = db_client.clone(); + + // Start Nats Auth Service + println!("starting auth..."); + let auth_client: Client = auth::run(db_client).await?; + println!("finished setting up auth..."); + + // Start Nats Admin Services + spawn(async move { + println!("spawning workload client..."); + let default_nats_connect_timeout_secs = 30; + let admin_creds_path = None; + if let Err(e) = workloads::run( + &admin_creds_path, + default_nats_connect_timeout_secs, + thread_db_client, + ) + .await + { + log::error!("Error running workload service. Err={:?}", e) + } + }); + + // Only exit program when explicitly requested + tokio::signal::ctrl_c().await?; + + log::debug!("Closing orchestrator auth service..."); + + // Close auth client and drain internal buffer before exiting to make sure all messages are sent + auth_client.drain().await?; + log::debug!("Closed orchestrator auth service"); Ok(()) } diff --git a/rust/clients/orchestrator/src/workloads.rs b/rust/clients/orchestrator/src/workloads.rs index d9a42df..33d05b7 100644 --- a/rust/clients/orchestrator/src/workloads.rs +++ b/rust/clients/orchestrator/src/workloads.rs @@ -17,50 +17,73 @@ This client is responsible for: use super::utils; use anyhow::{anyhow, Result}; use async_nats::Message; -use mongodb::{options::ClientOptions, Client as MongoDBClient}; -use std::{sync::Arc, time::Duration}; -use util_libs::{ - db::mongodb::get_mongodb_url, - nats::{ - jetstream_client::{self, JsClient}, - types::{ConsumerBuilder, EndpointType, JsClientBuilder, JsServiceBuilder}, - }, +use mongodb::Client as MongoDBClient; +use std::str::FromStr; +use std::{path::PathBuf, sync::Arc, time::Duration}; +use util_libs::nats::{ + jetstream_client::{self, JsClient}, + types::{ConsumerBuilder, Credentials, EndpointType, JsClientBuilder, JsServiceBuilder}, }; use workload::{ orchestrator_api::OrchestratorWorkloadApi, types::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_ADMIN_CLIENT_NAME: &str = "Orchestrator Admin Client"; +const ORCHESTRATOR_ADMIN_CLIENT_INBOX_PREFIX: &str = "_ADMIN_INBOX.orchestrator"; -pub async fn run() -> Result<(), async_nats::Error> { +pub async fn run( + admin_creds_path: &Option, + nats_connect_timeout_secs: u64, + db_client: MongoDBClient, +) -> Result<(), async_nats::Error> { // ==================== Setup NATS ==================== let nats_url = jetstream_client::get_nats_url(); - let creds_path = jetstream_client::get_nats_client_creds("HOLO", "WORKLOAD", "orchestrator"); - let event_listeners = jetstream_client::get_event_listeners(); - - let mut orchestrator_workload_client = JsClient::new(JsClientBuilder { - nats_url, - name: ORCHESTRATOR_WORKLOAD_CLIENT_NAME.to_string(), - inbox_prefix: ORCHESTRATOR_WORKLOAD_CLIENT_INBOX_PREFIX.to_string(), - credentials_path: Some(creds_path), - ping_interval: Some(Duration::from_secs(10)), - request_timeout: Some(Duration::from_secs(5)), - listeners: vec![jetstream_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)?; + let creds_path = admin_creds_path + .to_owned() + .ok_or(PathBuf::from_str(&jetstream_client::get_nats_creds_by_nsc( + "HOLO", "ADMIN", "admin", + ))) + .map(Credentials::Path) + .map_err(|e| anyhow!("Failed to locate admin credential path. Err={:?}", e))?; + + let mut orchestrator_workload_client = tokio::select! { + client = async {loop { + let c = JsClient::new(JsClientBuilder { + nats_url: nats_url.clone(), + name: ORCHESTRATOR_ADMIN_CLIENT_NAME.to_string(), + inbox_prefix: ORCHESTRATOR_ADMIN_CLIENT_INBOX_PREFIX.to_string(), + credentials: Some(vec![creds_path.clone()]), + ping_interval: Some(Duration::from_secs(10)), + request_timeout: Some(Duration::from_secs(5)), + listeners: vec![jetstream_client::with_event_listeners(jetstream_client::get_event_listeners())], + }) + .await + .map_err(|e| anyhow::anyhow!("connecting to NATS via {nats_url}: {e}")); + + match c { + 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(async_nats::Error::from(anyhow!("timed out waiting for NATS on {:?}", nats_url))); + } + }; // ==================== Setup JS Stream Service ==================== // Instantiate the Workload API (requires access to db client) - let workload_api = OrchestratorWorkloadApi::new(&client).await?; + let workload_api = OrchestratorWorkloadApi::new(&db_client).await?; + // Register Workload Streams for Orchestrator to consume and proceess + // NB: These subjects are published by external Developer (via external api), the Nats-DB-Connector, or the Hosting Agent let workload_stream_service = JsServiceBuilder { name: WORKLOAD_SRV_NAME.to_string(), description: WORKLOAD_SRV_DESC.to_string(), @@ -71,8 +94,6 @@ pub async fn run() -> Result<(), async_nats::Error> { .add_js_service(workload_stream_service) .await?; - // Register Workload Streams for Orchestrator to consume and proceess - // NB: These subjects are published by external Developer (via external api), the Nats-DB-Connector, or the Hosting Agent let workload_service = orchestrator_workload_client .get_js_service(WORKLOAD_SRV_NAME.to_string()) .await diff --git a/rust/services/authentication/Cargo.toml b/rust/services/authentication/Cargo.toml new file mode 100644 index 0000000..b0a9d0e --- /dev/null +++ b/rust/services/authentication/Cargo.toml @@ -0,0 +1,29 @@ +[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 } +bson = { version = "2.6.1", features = ["chrono-0_4"] } +url = { version = "2", features = ["serde"] } +async-trait = "0.1.83" +mongodb = "3.1" +base32 = "0.5.1" +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..ebabe30 --- /dev/null +++ b/rust/services/authentication/src/lib.rs @@ -0,0 +1,400 @@ +/* +Service Name: AUTH +Subject: "AUTH.>" +Provisioning Account: AUTH Account (ie: This service is exclusively permissioned to the AUTH account.) +Users: orchestrator auth user & auth guard user +Endpoints & Managed Subjects: + - handle_auth_callout: $SYS.REQ.USER.AUTH + - handle_auth_validation: AUTH.validate +*/ + +pub mod types; +pub mod utils; +use anyhow::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 data_encoding::BASE64URL_NOPAD; +use mongodb::{options::UpdateModifications, Client as MongoDBClient}; +use nkeys::KeyPair; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Debug, sync::Arc}; +use types::{AuthApiResult, DbValidationData}; +use util_libs::{ + db::{ + mongodb::{IntoIndexes, MongoCollection, MongoDbAPI}, + schemas::{self, Host, Hoster, User}, + }, + nats::types::ServiceError, +}; + +pub const AUTH_SRV_NAME: &str = "AUTH_SERVICE"; +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."; + +// Service Endpoint Names: +// NB: Do not change this subject name unless NATS.io has changed the naming of their auth permissions subject. +// NB: `AUTH_CALLOUT_SUBJECT` attached to the global subject `$SYS.REQ.USER` +pub const AUTH_CALLOUT_SUBJECT: &str = "AUTH"; +pub const VALIDATE_AUTH_SUBJECT: &str = "validate"; + +#[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 { + log::info!("Incoming message for '$SYS.REQ.USER.AUTH' : {:#?}", msg); + + // 1. Verify expected data was received + let auth_request_token = String::from_utf8_lossy(&msg.payload).to_string(); + let auth_request_claim = utils::decode_jwt::( + &auth_request_token, + &auth_signing_account_pubkey, + ) + .map_err(|e| ServiceError::Authentication(AuthError::new(e)))?; + + let auth_request_user_claim = utils::decode_jwt::( + &auth_request_claim.auth_request.connect_opts.user_jwt, + &auth_signing_account_pubkey, + ) + .map_err(|e| ServiceError::Authentication(AuthError::new(e)))?; + + if auth_request_user_claim.generic_claim_data.issuer != auth_signing_account_pubkey { + let e = "Error: Failed to validate issuer for auth user."; + log::error!("{} Subject='{}'.", e, msg.subject); + return Err(ServiceError::Authentication(AuthError::new(e))); + }; + + // 2. Validate Host signature, returning validation error if not successful + let user_data = utils::base64_to_data::( + &auth_request_claim.auth_request.connect_opts.user_auth_token, + ) + .map_err(|e| ServiceError::Authentication(AuthError::new(e)))?; + let host_pubkey = user_data.host_pubkey.as_ref(); + let host_signature = user_data.get_host_signature(); + let decoded_sig = BASE64URL_NOPAD + .decode(&host_signature) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + let user_verifying_keypair = KeyPair::from_public_key(host_pubkey) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + let payload_no_sig = &(user_data.clone().without_signature()); + let raw_payload = serde_json::to_vec(payload_no_sig) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + if let Err(e) = user_verifying_keypair.verify(raw_payload.as_ref(), &decoded_sig) { + 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 = self + .verify_is_valid_in_db(user_data.clone()) + .await + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + // 4. Assign permissions based on whether the hoster was successfully validated + let pubkey_lowercase = host_pubkey.to_lowercase(); + let permissions = if is_hoster_valid { + // If successful, assign personalized inbox and auth permissions + let user_unique_auth_subject = &format!("AUTH.{}.>", pubkey_lowercase); + let user_unique_inbox = &format!("_AUTH_INBOX.{}.>", pubkey_lowercase); + let authenticated_user_diagnostics_subject = + &format!("DIAGNOSTICS.{}.>", pubkey_lowercase); + + 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.>", pubkey_lowercase); + 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()))?; + + Ok(types::AuthApiResult { + result: types::AuthResult::Callout(token), + maybe_response_tags: None, + }) + } + + pub async fn handle_auth_validation( + &self, + msg: Arc, + ) -> Result { + log::info!("Incoming message for 'AUTH.validate' : {:#?}", msg); + + // 1. Verify expected data was received + let signature: &[u8] = match &msg.headers { + Some(h) => { + let r = HeaderValue::as_str(h.get("X-Signature").ok_or_else(|| { + log::error!("Error: Missing X-Signature header. Subject='AUTH.authorize'"); + ServiceError::Request(format!("{:?}", ErrorCode::BAD_REQUEST)) + })?); + r.as_bytes() + } + 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, + .. + } = Self::convert_msg_to_type::(msg.clone())?; + + // 2. Validate signature + let decoded_signature = BASE64URL_NOPAD + .decode(signature) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + 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(), &decoded_signature) { + log::error!( + "Error: Failed to validate Signature. Subject='{}'. Err={}", + msg.subject, + e + ); + return Err(ServiceError::Authentication(AuthError::new(format!( + "{:?}", + e + )))); + }; + + // 3. Add User keys to nsc resolver (and automatically create account-signed refernce to user key) + utils::add_user_keys_to_resolver(&host_pubkey, &maybe_sys_pubkey)?; + + // 4. Create User JWT files (automatically signed with respective account key) + let (host_jwt, sys_jwt) = utils::create_user_jwt_files(&host_pubkey, &maybe_sys_pubkey) + .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()); + + // 5. Form the result and return + 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 verify_is_valid_in_db( + &self, + user_data: types::AuthGuardPayload, + ) -> Result { + if let (Some(hoster_hc_pubkey), Some(hoster_email)) = + (user_data.hoster_hc_pubkey, user_data.email) + { + let host_pubkey = user_data.host_pubkey; + + let pipeline = vec![ + // Step 1: Find the `user` document with a matching `hoster_hc_pubkey`` + doc! { + "$match": { "hoster.pubkey": hoster_hc_pubkey.clone() } + }, + // Step 2: Look-up the associated `user_info`` document by referencing the `user.user_info_id` field + // NB: The `local_field` references a field local to the `user` document matched in step 1 + doc! { + "$lookup": { + "from": "user_info", + "localField": "user_info_id", + "foreignField": "_id", + "as": "user_info" + } + }, + // Extract the matching `user_info` document from resulting array + doc! { "$unwind": "$user_info" }, + doc! { + "$lookup": { + "from": "hoster", + "localField": "hoster.collection_id", + "foreignField": "_id", + "as": "hoster_record" + } + }, + // Extract the matching `hoster` document from resulting array + // NB: `hoster` is aliased to `hoster_record` to avoid namespace collision with the `user`` document field `hoster` + doc! { "$unwind": "$hoster_record" }, + doc! { + "$project": { + "_id": 0, + "jurisdiction": 1, + "hoster.pubkey": 1, + "hoster_record": 1, + "user_info.email": 1, + } + }, + ]; + + // Run the aggregation pipeline + let result = self + .user_collection + .aggregate::(pipeline) + .await + .unwrap_or(vec![]); + + println!("Aggregate pipeline result: {:#?}", result); + + // If no result is returned or more than 1 item exists, call failed + if result.is_empty() { + println!("Failed update pipeline..."); + log::error!("Failed DB Authorization. REASON=Failed to locate user collection associated with the valid hoster and user_info document."); + return Ok(false); + } else if result.len() > 1 { + log::error!("Failed DB Authorization. REASON=Recieved unexpected volume of results when validating user data."); + return Ok(false); + } + + let DbValidationData { + jurisdiction: _, + user_info, + hoster, + hoster_pubkey, + } = &result[0]; + + if user_info.email != hoster_email { + log::error!("Failed DB Authorization. REASON=Invalid hoster email."); + return Ok(false); + } + + if hoster_pubkey.pubkey != hoster_hc_pubkey { + log::error!("Failed DB Authorization. REASON=Invalid hoster pubkey."); + return Ok(false); + } + + // Now that host & hoster are successfully validated... + // Create a new host document in db and assign the bidirectional references + let mut new_host = Host::default(); + new_host.metadata.created_at = Some(bson::DateTime::now()); + new_host.device_id = host_pubkey; + + // Assign Hoster to Host + new_host.assigned_hoster = hoster._id.ok_or(ServiceError::Internal( + "Passed DB Authorization, but failed to assign hoster to host. REASON=Failed." + .to_string(), + ))?; + let host_id = self.host_collection.insert_one_into(new_host).await?; + + // Assign Host to Hoster + let mut updated_hoster = hoster.to_owned(); + updated_hoster.assigned_hosts.push(host_id); + self.hoster_collection.update_one_within( + doc! { + "_id": hoster._id + }, + UpdateModifications::Document(doc! { + "$set": to_document(&updated_hoster).map_err(|e| ServiceError::Authentication(AuthError::new(e)))? + }), + ).await?; + + Ok(true) + } else { + Ok(false) + } + } + + 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?) + } + + 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)) + }) + } +} diff --git a/rust/services/authentication/src/types.rs b/rust/services/authentication/src/types.rs new file mode 100644 index 0000000..cafff05 --- /dev/null +++ b/rust/services/authentication/src/types.rs @@ -0,0 +1,274 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use util_libs::{ + db::schemas::Hoster, + nats::types::{CreateResponse, CreateTag, EndpointTraits}, +}; + +// 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 `hub_auth_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, Debug)] +pub struct AuthErrorPayload { + pub service_info: async_nats::service::Info, + pub group: String, + pub endpoint: String, + pub error: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct AuthJWTPayload { + pub host_pubkey: String, // nkey + pub maybe_sys_pubkey: Option, // optional 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), // stringified `AuthResponseClaim` + 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(), + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct UserEmail { + pub email: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct HosterPubkey { + pub pubkey: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct DbValidationData { + pub jurisdiction: String, + pub user_info: UserEmail, + #[serde(rename = "hoster")] + pub hoster_pubkey: HosterPubkey, + #[serde(rename = "hoster_record")] + pub hoster: Hoster, +} + +////////////////////////// +// 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..ee2d0a2 --- /dev/null +++ b/rust/services/authentication/src/utils.rs @@ -0,0 +1,301 @@ +use super::types; +use anyhow::{anyhow, Context, Result}; +use base32::decode as base32Decode; +use base32::Alphabet; +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::process::Command; +use std::sync::Arc; +use std::time::SystemTime; +use types::WORKLOAD_SK_ROLE; +use util_libs::nats::{jetstream_client, types::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()); + let b64_body = BASE64URL_NOPAD.encode(claims_str.as_bytes()); + let jwt_half = format!("{b64_header}.{b64_body}"); + let sig = signing_kp.sign(jwt_half.as_bytes())?; + let b64_sig = BASE64URL_NOPAD.encode(&sig); + Ok(format!("{jwt_half}.{b64_sig}")) +} + +/// Convert token into the +pub fn decode_jwt(token: &str, auth_signing_account_pubkey: &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(); + 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"); + + // 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()); + } + } + let modified_header = BASE64URL_NOPAD.encode(&serde_json::to_vec(&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]); + + // Decode from Base32 to raw bytes using Rfc4648 (compatible with NATS keys) + let public_key_bytes = base32Decode( + Alphabet::Rfc4648 { padding: false }, + auth_signing_account_pubkey, + ) + .expect("Failed to convert public key to bytes"); + + // Use the decoded key to create a DecodingKey + let decoding_key = DecodingKey::from_ed_der(&public_key_bytes); + + // 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 + + let token_data = decode::(&modified_token, &decoding_key, &validation)?; + 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)?; + 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) +} + +pub fn add_user_keys_to_resolver( + host_pubkey: &str, + maybe_sys_pubkey: &Option, +) -> Result<(), ServiceError> { + match Command::new("nsc") + .args([ + "add", + "user", + "-a", + "WORKLOAD", + "-n", + &format!("host_user_{}", host_pubkey), + "-k", + host_pubkey, + "-K", + WORKLOAD_SK_ROLE, + "--tag", + &format!("pubkey:{}", host_pubkey), + ]) + .output() + .context("Failed to add host user with provided keys") + .map_err(|e| ServiceError::Internal(e.to_string())) + { + Ok(r) => { + let stderr = String::from_utf8_lossy(&r.stderr); + if !r.stderr.is_empty() && !stderr.contains("already exists") { + return Err(ServiceError::Internal(stderr.to_string())); + } + } + Err(e) => { + return Err(e); + } + }; + + if let Some(sys_pubkey) = maybe_sys_pubkey.clone() { + match Command::new("nsc") + .args([ + "add", + "user", + "-a", + "SYS", + "-n", + &format!("sys_user_{}", host_pubkey), + "-k", + &sys_pubkey, + ]) + .output() + .context("Failed to add host sys user with provided keys") + .map_err(|e| ServiceError::Internal(e.to_string())) + { + Ok(r) => { + let stderr = String::from_utf8_lossy(&r.stderr); + if !r.stderr.is_empty() && !stderr.contains("already exists") { + return Err(ServiceError::Internal(stderr.to_string())); + } + } + Err(e) => { + return Err(e); + } + }; + }; + + Ok(()) +} + +pub fn create_user_jwt_files( + host_pubkey: &str, + maybe_sys_pubkey: &Option, +) -> Result<(String, String)> { + let host_jwt = std::fs::read_to_string(jetstream_client::get_nats_jwt_by_nsc( + "HOLO", + "WORKLOAD", + &format!("host_user_{}.jwt", host_pubkey), + )) + .map_err(|e| ServiceError::Internal(e.to_string()))?; + + let sys_jwt = if maybe_sys_pubkey.is_some() { + std::fs::read_to_string(jetstream_client::get_nats_jwt_by_nsc( + "HOLO", + "SYS", + &format!("sys_user_{}.jwt", host_pubkey), + )) + .map_err(|e| ServiceError::Internal(e.to_string()))? + } else { + String::new() + }; + + // PUSH the auth updates to resolver programmtically by sending jwts to `SYS.REQ.ACCOUNT.PUSH` subject + Command::new("nsc") + .arg("push -A") + .output() + .context("Failed to update resolver config file") + .map_err(|e| ServiceError::Internal(e.to_string()))?; + log::trace!("\nPushed new jwts to resolver server"); + + Ok((host_jwt, sys_jwt)) +} diff --git a/rust/util_libs/src/nats/jetstream_client.rs b/rust/util_libs/src/nats/jetstream_client.rs index 1e6a000..c7d43a7 100644 --- a/rust/util_libs/src/nats/jetstream_client.rs +++ b/rust/util_libs/src/nats/jetstream_client.rs @@ -2,8 +2,8 @@ use super::{ jetstream_service::JsStreamService, leaf_server::LEAF_SERVER_DEFAULT_LISTEN_PORT, types::{ - ErrClientDisconnected, EventHandler, EventListener, JsClientBuilder, JsServiceBuilder, - PublishInfo, + Credentials, ErrClientDisconnected, EventHandler, EventListener, JsClientBuilder, + JsServiceBuilder, PublishInfo, }, }; use anyhow::Result; @@ -25,6 +25,7 @@ impl std::fmt::Debug for JsClient { } } +#[derive(Clone)] pub struct JsClient { url: String, name: String, @@ -38,27 +39,37 @@ pub struct JsClient { impl JsClient { pub async fn new(p: JsClientBuilder) -> Result { - let connect_options = async_nats::ConnectOptions::new() + let mut connect_options = async_nats::ConnectOptions::new() .name(&p.name) .ping_interval(p.ping_interval.unwrap_or(Duration::from_secs(120))) .request_timeout(Some(p.request_timeout.unwrap_or(Duration::from_secs(10)))) .custom_inbox_prefix(&p.inbox_prefix); // .require_tls(true) - 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? + if let Some(credentials_list) = p.credentials { + for credentials in credentials_list { + match credentials { + Credentials::Password(user, pw) => { + connect_options = connect_options.user_and_password(user, pw); + } + Credentials::Path(cp) => { + let path = std::path::Path::new(&cp); + connect_options = connect_options.credentials_file(path).await?; + } + Credentials::Token(t) => { + connect_options = connect_options.token(t); + } + } } - None => connect_options.connect(&p.nats_url).await?, }; - let log_prefix = format!("NATS-CLIENT-LOG::{}::", p.name); - log::info!("{}Connected to NATS server at {}", log_prefix, p.nats_url); + let client = connect_options.connect(&p.nats_url).await?; + let service_log_prefix = format!("NATS-CLIENT-LOG::{}::", p.name); + log::info!( + "{}Connected to NATS server at {}", + service_log_prefix, + p.nats_url + ); let mut js_client = JsClient { url: p.nats_url, @@ -67,7 +78,7 @@ impl JsClient { on_msg_failed_event: None, js_services: None, js_context: jetstream::new(client.clone()), - service_log_prefix: log_prefix, + service_log_prefix, client, }; @@ -217,19 +228,39 @@ 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 - ) - }) +fn get_nsc_root_path() -> String { + std::env::var("NSC_PATH").unwrap_or_else(|_| "/.local/share/nats/nsc".to_string()) +} + +pub fn get_local_creds_path() -> String { + std::env::var("LOCAL_CREDS_PATH") + .unwrap_or_else(|_| format!("{}/local_creds", get_nsc_root_path())) +} + +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_nats_jwt_by_nsc(operator: &str, account: &str, user: &str) -> String { + format!( + "{}/stores/{}/accounts/{}/users/{}.jwt", + get_nsc_root_path(), + operator, + account, + user + ) } pub fn get_event_listeners() -> Vec { @@ -272,7 +303,7 @@ mod tests { } #[tokio::test] - async fn test_nats_js_client_init() { + async fn test_jetstream_client_init() { let params = get_default_params(); let client = JsClient::new(params).await; assert!(client.is_ok(), "Client initialization failed: {:?}", client); @@ -282,7 +313,7 @@ mod tests { } #[tokio::test] - async fn test_nats_js_client_publish() { + async fn test_jetstream_client_publish() { let params = get_default_params(); let client = JsClient::new(params).await.unwrap(); let payload = PublishInfo { diff --git a/rust/util_libs/src/nats/jetstream_service.rs b/rust/util_libs/src/nats/jetstream_service.rs index e622e3a..dc1a492 100644 --- a/rust/util_libs/src/nats/jetstream_service.rs +++ b/rust/util_libs/src/nats/jetstream_service.rs @@ -185,6 +185,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?; diff --git a/rust/util_libs/src/nats/types.rs b/rust/util_libs/src/nats/types.rs index d1b6fe1..109208f 100644 --- a/rust/util_libs/src/nats/types.rs +++ b/rust/util_libs/src/nats/types.rs @@ -1,7 +1,7 @@ use super::jetstream_client::JsClient; use anyhow::Result; use async_nats::jetstream::consumer::PullConsumer; -use async_nats::{HeaderMap, Message}; +use async_nats::{AuthError, HeaderMap, Message}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::any::Any; @@ -147,13 +147,20 @@ where } } +#[derive(Clone)] +pub enum Credentials { + Path(std::path::PathBuf), // String = pathbuf as string + Password(String, String), + Token(String), +} + #[derive(Deserialize, Default)] pub struct JsClientBuilder { pub nats_url: String, pub name: String, pub inbox_prefix: String, - #[serde(default)] - pub credentials_path: Option, + #[serde(default, skip_deserializing)] + pub credentials: Option>, #[serde(default)] pub ping_interval: Option, #[serde(default)] @@ -193,6 +200,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}")] 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+")" -ADMIN_ROLE_NAME="admin-role" -nsc edit signing-key --sk $ADMIN_SIGNING_KEY --role $ADMIN_ROLE_NAME --allow-pub "ADMIN.>","WORKLOAD.>","\$JS.>","\$SYS.>","_INBOX.>","_INBOX_*.>","*._WORKLOAD_INBOX.>" --allow-sub "ADMIN.>""WORKLOAD.>","\$JS.>","\$SYS.>","_INBOX.>","_INBOX_*.>","ORCHESTRATOR._WORKLOAD_INBOX.>" --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 -# Step 3: Create HPOS Account with JetStream and scoped signing key -nsc add account --name $HPOS_ACCOUNT -nsc edit account --name $HPOS_ACCOUNT --js-streams -1 --js-consumer -1 --js-mem-storage 1G --js-disk-storage 5G -HPOS_SIGNING_KEY="$(echo "$(nsc edit account -n $HPOS_ACCOUNT --sk generate 2>&1)" | grep -oP "signing key\s*\K\S+")" -WORKLOAD_ROLE_NAME="workload-role" -nsc edit signing-key --sk $HPOS_SIGNING_KEY --role $WORKLOAD_ROLE_NAME --allow-pub "WORKLOAD.>","{{tag(pubkey)}}._WORKLOAD_INBOX.>","\$JS.API>" --allow-sub "WORKLOAD.{{tag(pubkey)}}.*","{{tag(pubkey)}}._WORKLOAD_INBOX.>","\$JS.API>" --allow-pub-response +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.>","\$SYS.>","\$G.>","_INBOX.>","_ADMIN_INBOX.>","_AUTH_INBOX.>" --allow-sub "ADMIN.>","AUTH.>","WORKLOAD.>","\$JS.>","\$SYS.>","\$G.>","_INBOX.>","_ADMIN_INBOX.orchestrator.>","_AUTH_INBOX.orchestrator.>" --allow-pub-response -# Step 4: Create User "admin" in ADMIN Account -nsc add user --name admin --account $ADMIN_ACCOUNT +# 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 HPOS Account with JetStream and scoped signing keys +nsc add account --name $HPOS_ACCOUNT +nsc edit account --name $HPOS_ACCOUNT --js-streams -1 --js-consumer -1 --js-mem-storage 1G --js-disk-storage 5G --conns -1 --leaf-conns -1 +HPOS_WORKLOAD_SK="$(echo "$(nsc edit account -n $HPOS_ACCOUNT --sk generate 2>&1)" | grep -oP "signing key\s*\K\S+")" +WORKLOAD_ROLE_NAME="workload_role" +nsc edit signing-key --sk $HPOS_WORKLOAD_SK --role $WORKLOAD_ROLE_NAME --allow-pub "_ADMIN_INBOX.orchestrator.>","WORKLOAD.orchestrator.>","\$JS.API.>","WORKLOAD.{{tag(pubkey)}}.>","_HPOS_INBOX.{{tag(pubkey)}}.>" --allow-sub "WORKLOAD.{{tag(pubkey)}}.>","_HPOS_INBOX.{{tag(pubkey)}}.>","\$JS.API.>" --allow-pub-response # Step 5: Export/Import WORKLOAD Service Stream between ADMIN and HPOS accounts # Share orchestrator (as admin user) workload streams with host @@ -94,13 +136,38 @@ nsc add import --src-account ADMIN --name "WORKLOAD_SERVICE" --remote-subject "W nsc add export --name "WORKLOAD_SERVICE" --subject "WORKLOAD.>" --account HPOS nsc add import --src-account HPOS --name "WORKLOAD_SERVICE" --remote-subject "WORKLOAD.>" --local-subject "WORKLOAD.>" --account ADMIN -# 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 $HPOS_ACCOUNT --raw --output-file $JWT_OUTPUT_DIR/hpos_account.jwt -nsc describe account --name $ADMIN_ACCOUNT --raw --output-file $JWT_OUTPUT_DIR/admin_account.jwt +# Step 6: Create Orchestrator User in ADMIN Account +nsc add user --name $ADMIN_USER --account $ADMIN_ACCOUNT -K $ADMIN_ROLE_NAME + +# Step 7: Create Orchestrator User in AUTH Account (used in auth-callout 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 8: Create "Sentinel" User in AUTH Account (used by host agents in auth-callout service) +nsc add user --name $AUTH_GUARD_USER --account $AUTH_ACCOUNT --deny-pubsub ">" + +# Step 9: 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 10: 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 + +extract_signing_key ADMIN $ADMIN_SK +echo "extracted ADMIN signing key" + +extract_signing_key AUTH $AUTH_SK_ACCOUNT_PUBKEY +echo "extracted AUTH signing key" + +extract_signing_key AUTH_ROOT $AUTH_ACCOUNT_PUBKEY +echo "extracted AUTH root key" -# Step 7: Generate Resolver Config +# Step 11: 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."