diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index a4576120bd..41c3a6eaa3 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -69,9 +69,11 @@ jobs: - name: Install required packages run: zypper --non-interactive install + clang-devel jq libopenssl-3-devel openssl-3 + pam-devel python-langtable-data timezone diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 3e7b177d68..d79e816f4c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -50,13 +50,19 @@ dependencies = [ "anyhow", "async-trait", "axum", + "axum-extra", + "chrono", "cidr", "clap", + "config", "gettext-rs", "http-body-util", + "jsonwebtoken", "log", "macaddr", "once_cell", + "pam", + "rand", "regex", "serde", "serde_json", @@ -152,6 +158,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.11" @@ -421,6 +442,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -448,6 +491,26 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.4.1", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -474,6 +537,9 @@ name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] [[package]] name = "block" @@ -506,6 +572,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "bumpalo" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f" + [[package]] name = "bytecount" version = "0.6.7" @@ -533,6 +605,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -541,11 +622,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "windows-targets 0.52.0", ] [[package]] @@ -579,6 +663,16 @@ dependencies = [ "serde", ] +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", +] + [[package]] name = "clap" version = "4.5.0" @@ -635,6 +729,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "console" version = "0.15.7" @@ -648,6 +762,26 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -657,6 +791,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -684,6 +824,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -761,6 +907,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1009,8 +1170,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1039,6 +1202,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.4.2" @@ -1058,12 +1227,42 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -1172,6 +1371,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1189,7 +1411,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", "serde", ] @@ -1235,12 +1457,41 @@ dependencies = [ "nom", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jsonschema" version = "0.16.1" @@ -1268,12 +1519,33 @@ dependencies = [ "uuid", ] +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.151" @@ -1310,6 +1582,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1643,6 +1921,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "ordered-stream" version = "0.2.0" @@ -1659,6 +1947,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pam" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba" +dependencies = [ + "libc", + "memchr", + "pam-macros", + "pam-sys", + "users", +] + +[[package]] +name = "pam-macros" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pam-sys" +version = "1.0.0-alpha5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b" +dependencies = [ + "bindgen", + "libc", +] + [[package]] name = "parking" version = "2.2.0" @@ -1697,12 +2019,73 @@ dependencies = [ "regex", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.7", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pest_meta" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.2" @@ -1845,7 +2228,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -1968,12 +2351,54 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.37.27" @@ -2080,6 +2505,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2136,6 +2570,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2145,6 +2585,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "simplelog" version = "0.12.1" @@ -2197,6 +2649,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2352,6 +2810,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2434,11 +2901,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.6", +] + [[package]] name = "toml_datetime" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2448,7 +2930,20 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow", + "winnow 0.5.28", +] + +[[package]] +name = "toml_edit" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.1", ] [[package]] @@ -2590,6 +3085,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uds_windows" version = "1.1.0" @@ -2640,6 +3141,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -2651,6 +3158,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "users" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf-8" version = "0.7.6" @@ -2734,6 +3251,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + [[package]] name = "winapi" version = "0.3.9" @@ -2765,6 +3336,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -2972,6 +3552,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d90f4e0f530c4c69f62b80d839e9ef3855edc9cba471a160c4d692deed62b401" +dependencies = [ + "memchr", +] + [[package]] name = "xdg-home" version = "1.0.0" @@ -2982,6 +3571,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zbus" version = "3.14.1" diff --git a/rust/WEB-SERVER.md b/rust/WEB-SERVER.md new file mode 100644 index 0000000000..75534d03ee --- /dev/null +++ b/rust/WEB-SERVER.md @@ -0,0 +1,107 @@ +# Web server development notes + +This document includes some notes about the usage and development of the new Agama web server. + +## Installing Rust and related tools + +It is recommended to use Rustup to install Rust and related tools. In openSUSE distributions, rustup +is available as a package. After rustup is installed, you can proceed to install the toolchain: + +``` +zypper --non-interactive in rustup +rustup install stable +``` + +In addition to the Rust compiler, the previous command would install some additionall components +like `cargo`, `clippy`, `rustfmt`, documentation, etc. + +Another interesting addition might be +[cargo-binstall](https://github.com/cargo-bins/cargo-binstall), which allows to install Rust +binaries. If you are fine with this approach, just run: + +``` +cargo install cargo-binstall +``` + +## Setting up PAM + +The web sever will use [Pluggable Authentication Modules +(PAM)](https://github.com/linux-pam/linux-pam) for authentication. For that +reason, you need to copy the `agama` service definition for PAM to `/usr/lib/pam.d`. Otherwise, PAM +would not know how to authenticate the service: + +``` +cp share/agama.pam /usr/lib/pam.d/agama +``` + +For further information, see [Authenticating with PAM](https://doc.opensuse.org/documentation/leap/security/single-html/book-security/index.html#cha-pam). + +## Running the server + +> [!NOTE] +> The web server needs to connect to Agama's D-Bus daemon. So you can either start the Agama service +> or just start the D-Bus daemon (`sudo bundle exec bin/agamactl -f` from the `service/` directory). + +You need to run the server as `root`, so you cannot use `cargo run` directly. Instead, just do: + +``` +$ cargo build +$ sudo ./target/debug/agama-web-server serve +``` + +If it fails to compile, please check whether `clang-devel` and `pam-devel` are installed. + +You can add a `--listen` flag if you want to use a different port: + +``` +$ sudo ./target/debug/agama-web-server serve --listen 0.0.0.0:5678 +``` + +## Trying the server + +You can check whether the server is up and running by just performing a ping: + +``` +$ curl http://localhost:3000/ping +``` + +### Authentication + +The web server uses a bearer token for HTTP authentication. You can get the token by providing your +password to the `/authenticate` endpoint. + +``` +$ curl http://localhost:3000/authenticate \ + -H "Content-Type: application/json" \ + -d '{"password": "your-password"}' +{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U"}⏎ +``` + +Now you can access protected routes by including the token in the header: + +``` +$ curl -X GET http://localhost:3000/protected \ + -H "Accept: application/json" \ + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" +``` + +### Connecting to the websocket + +You can use `websocat` to connect to the websocket. To install the tool, just run: + +``` +$ cargo binstall websocat +``` + +If you did not install `cargo-binstall`, you can do: + +``` +$ cargo install websocat +``` + +Now, you can use the following command to connect: + +``` +$ websocat ws://localhost:3000/ws + -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDg1MTA5MzB9.3HmKAC5u4H_FigMqEa9e74OFAq40UldjlaExrOGqE0U" +``` diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 326394ffc9..85f8753a9e 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -36,6 +36,17 @@ tracing = "0.1.40" clap = { version = "4.5.0", features = ["derive", "wrap_help"] } tower = "0.4.13" utoipa = { version = "4.2.0", features = ["axum_extras"] } +config = "0.14.0" +rand = "0.8.5" +jsonwebtoken = "9.2.0" +axum-extra = { version = "0.9.2", features = ["typed-header"] } +chrono = { version = "0.4.34", default-features = false, features = [ + "now", + "std", + "alloc", + "clock", +] } +pam = "0.8.0" [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-dbus-server/share/server-example.yaml b/rust/agama-dbus-server/share/server-example.yaml new file mode 100644 index 0000000000..d0d8c4f399 --- /dev/null +++ b/rust/agama-dbus-server/share/server-example.yaml @@ -0,0 +1,2 @@ +--- +jwt_key: "replace-me" diff --git a/rust/agama-dbus-server/src/agama-web-server.rs b/rust/agama-dbus-server/src/agama-web-server.rs index 4e45bfacfd..9df6dfac0c 100644 --- a/rust/agama-dbus-server/src/agama-web-server.rs +++ b/rust/agama-dbus-server/src/agama-web-server.rs @@ -36,7 +36,9 @@ async fn serve_command(address: &str) { .unwrap_or_else(|_| panic!("could not listen on {}", address)); let dbus_connection = connection().await.unwrap(); - axum::serve(listener, web::service(dbus_connection)) + let config = web::ServiceConfig::load().unwrap(); + let service = web::service(config, dbus_connection); + axum::serve(listener, service) .await .expect("could not mount app on listener"); } diff --git a/rust/agama-dbus-server/src/web.rs b/rust/agama-dbus-server/src/web.rs index cfc44ef4d4..b3e1490f9b 100644 --- a/rust/agama-dbus-server/src/web.rs +++ b/rust/agama-dbus-server/src/web.rs @@ -4,10 +4,15 @@ //! * Emit relevant events via websocket. //! * Serve the code for the web user interface (not implemented yet). +mod auth; +mod config; mod docs; mod http; mod service; +mod state; mod ws; +pub use auth::generate_token; +pub use config::ServiceConfig; pub use docs::ApiDoc; pub use service::service; diff --git a/rust/agama-dbus-server/src/web/auth.rs b/rust/agama-dbus-server/src/web/auth.rs new file mode 100644 index 0000000000..5674bdc753 --- /dev/null +++ b/rust/agama-dbus-server/src/web/auth.rs @@ -0,0 +1,93 @@ +//! Contains the code to handle access authorization. + +use super::state::ServiceState; +use async_trait::async_trait; +use axum::{ + extract::FromRequestParts, + http::{request, StatusCode}, + response::{IntoResponse, Response}, + Json, RequestPartsExt, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use chrono::{Duration, Utc}; +use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; +use pam::PamError; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use thiserror::Error; + +/// Represents an authentication error. +#[derive(Error, Debug)] +pub enum AuthError { + /// The authentication error is not included in the headers. + #[error("Missing authentication token")] + MissingToken, + /// The authentication error is invalid. + #[error("Invalid authentication token: {0}")] + InvalidToken(#[from] jsonwebtoken::errors::Error), + /// The authentication failed (most probably the password is wrong) + #[error("Authentication via PAM failed: {0}")] + Failed(#[from] PamError), +} + +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + let body = json!({ + "error": self.to_string() + }); + (StatusCode::BAD_REQUEST, Json(body)).into_response() + } +} + +/// Claims that are included in the token. +/// +/// See https://datatracker.ietf.org/doc/html/rfc7519 for reference. +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenClaims { + exp: i64, +} + +impl Default for TokenClaims { + fn default() -> Self { + let exp = Utc::now() + Duration::days(1); + Self { + exp: exp.timestamp(), + } + } +} + +#[async_trait] +impl FromRequestParts for TokenClaims { + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut request::Parts, + state: &ServiceState, + ) -> Result { + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|_| AuthError::MissingToken)?; + + let decoding = DecodingKey::from_secret(state.config.jwt_secret.as_ref()); + let token_data = jsonwebtoken::decode(bearer.token(), &decoding, &Validation::default())?; + + Ok(token_data.claims) + } +} + +/// Generates a JWT. +/// +/// - `secret`: secret to encrypt/sign the token. +pub fn generate_token(secret: &str) -> String { + let claims = TokenClaims::default(); + jsonwebtoken::encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(secret.as_ref()), + ) + .unwrap() +} diff --git a/rust/agama-dbus-server/src/web/config.rs b/rust/agama-dbus-server/src/web/config.rs new file mode 100644 index 0000000000..0292d6d361 --- /dev/null +++ b/rust/agama-dbus-server/src/web/config.rs @@ -0,0 +1,46 @@ +//! Handles Agama web server configuration. +//! +//! The configuration can be written in YAML or JSON formats, although we plan to choose just one +//! of them in the future. It is read from the following locations: +//! +//! * `/usr/etc/agama.d/server.{json/yaml}` +//! * `/etc/agama.d/server.{json/yaml}` +//! * `./agama-dbus-server/share/server.{json/yaml}` +//! +//! All the settings are merged into a single configuration. The values in the latter locations +//! take precedence. + +use config::{Config, ConfigError, File}; +use rand::distributions::{Alphanumeric, DistString}; +use serde::Deserialize; + +/// Web service configuration. +#[derive(Clone, Debug, Deserialize)] +pub struct ServiceConfig { + /// Key to sign the JSON Web Tokens. + pub jwt_secret: String, +} + +impl ServiceConfig { + pub fn load() -> Result { + const JWT_SECRET_SIZE: usize = 30; + let jwt_secret: String = + Alphanumeric.sample_string(&mut rand::thread_rng(), JWT_SECRET_SIZE); + + let config = Config::builder() + .set_default("jwt_secret", jwt_secret)? + .add_source(File::with_name("/usr/etc/agama.d/server").required(false)) + .add_source(File::with_name("/etc/agama.d/server").required(false)) + .add_source(File::with_name("agama-dbus-server/share/server").required(false)) + .build()?; + config.try_deserialize() + } +} + +impl Default for ServiceConfig { + fn default() -> Self { + Self { + jwt_secret: "".to_string(), + } + } +} diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index 9635638bb9..f6131044db 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -1,7 +1,12 @@ //! Implements the handlers for the HTTP-based API. -use axum::Json; -use serde::Serialize; +use super::{ + auth::{generate_token, AuthError}, + state::ServiceState, +}; +use axum::{extract::State, Json}; +use pam::Client; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, ToSchema)] @@ -18,3 +23,37 @@ pub async fn ping() -> Json { status: "success".to_string(), }) } + +// TODO: remove this route (as it is just for teting) as soon as we have a legit protected one +pub async fn protected() -> String { + "OK".to_string() +} + +#[derive(Serialize)] +pub struct AuthResponse { + /// Bearer token to use on subsequent calls + token: String, +} + +#[derive(Deserialize)] +pub struct LoginRequest { + /// User password + pub password: String, +} + +#[utoipa::path(get, path = "/authenticate", responses( + (status = 200, description = "The user have been successfully authenticated", body = AuthResponse) +))] +pub async fn authenticate( + State(state): State, + Json(login): Json, +) -> Result, AuthError> { + let mut pam_client = Client::with_password("agama")?; + pam_client + .conversation_mut() + .set_credentials("root", login.password); + pam_client.authenticate()?; + + let token = generate_token(&state.config.jwt_secret); + Ok(Json(AuthResponse { token })) +} diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index 01dac81b28..2c30d24212 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -1,17 +1,25 @@ -use axum::{routing::get, Router}; +use super::{auth::TokenClaims, config::ServiceConfig, state::ServiceState}; +use axum::{ + middleware, + routing::{get, post}, + Router, +}; use tower_http::trace::TraceLayer; /// Returns a service that implements the web-based Agama API. -pub fn service(dbus_connection: zbus::Connection) -> Router { - let state = ServiceState { dbus_connection }; +pub fn service(config: ServiceConfig, dbus_connection: zbus::Connection) -> Router { + let state = ServiceState { + config, + dbus_connection, + }; Router::new() - .route("/ping", get(super::http::ping)) + .route("/protected", get(super::http::protected)) .route("/ws", get(super::ws::ws_handler)) + .route_layer(middleware::from_extractor_with_state::( + state.clone(), + )) + .route("/ping", get(super::http::ping)) + .route("/authenticate", post(super::http::authenticate)) .layer(TraceLayer::new_for_http()) .with_state(state) } - -#[derive(Clone)] -pub struct ServiceState { - pub dbus_connection: zbus::Connection, -} diff --git a/rust/agama-dbus-server/src/web/state.rs b/rust/agama-dbus-server/src/web/state.rs new file mode 100644 index 0000000000..49e16c4e22 --- /dev/null +++ b/rust/agama-dbus-server/src/web/state.rs @@ -0,0 +1,12 @@ +//! Implements the web service state. + +use super::config::ServiceConfig; + +/// Web service state. +/// +/// It holds the service configuration and the current D-Bus connection. +#[derive(Clone)] +pub struct ServiceState { + pub config: ServiceConfig, + pub dbus_connection: zbus::Connection, +} diff --git a/rust/agama-dbus-server/src/web/ws.rs b/rust/agama-dbus-server/src/web/ws.rs index c1d3cb2770..3cfd9061c1 100644 --- a/rust/agama-dbus-server/src/web/ws.rs +++ b/rust/agama-dbus-server/src/web/ws.rs @@ -1,6 +1,6 @@ //! Implements the websocket handling. -use super::service::ServiceState; +use super::state::ServiceState; use agama_lib::progress::{Progress, ProgressMonitor, ProgressPresenter}; use async_trait::async_trait; use axum::{ diff --git a/rust/agama-dbus-server/tests/service.rs b/rust/agama-dbus-server/tests/service.rs index 93e60e42fe..c825fbbe43 100644 --- a/rust/agama-dbus-server/tests/service.rs +++ b/rust/agama-dbus-server/tests/service.rs @@ -1,10 +1,11 @@ mod common; use self::common::DBusServer; -use agama_dbus_server::service; +use agama_dbus_server::{service, web::generate_token, web::ServiceConfig}; use axum::{ body::Body, - http::{Request, StatusCode}, + http::{Method, Request, StatusCode}, + response::Response, }; use http_body_util::BodyExt; use std::error::Error; @@ -19,7 +20,7 @@ async fn body_to_string(body: Body) -> String { #[test] async fn test_ping() -> Result<(), Box> { let dbus_server = DBusServer::new().start().await?; - let web_server = service(dbus_server.connection()); + let web_server = service(ServiceConfig::default(), dbus_server.connection()); let request = Request::builder().uri("/ping").body(Body::empty()).unwrap(); let response = web_server.oneshot(request).await.unwrap(); @@ -29,3 +30,44 @@ async fn test_ping() -> Result<(), Box> { assert_eq!(&body, "{\"status\":\"success\"}"); Ok(()) } + +async fn access_protected_route(token: &str, jwt_secret: &str) -> Response { + let dbus_server = DBusServer::new().start().await.unwrap(); + let config = ServiceConfig { + jwt_secret: jwt_secret.to_string(), + }; + let web_server = service(config, dbus_server.connection()); + let request = Request::builder() + .uri("/protected") + .method(Method::GET) + .header("Authorization", format!("Bearer {}", token)) + .body(Body::empty()) + .unwrap(); + + web_server.oneshot(request).await.unwrap() +} + +// TODO: The following test should belong to `auth.rs`. However, we need a working +// D-Bus connection which is not available on containers. Let's keep the test +// here until by now. +#[test] +async fn test_access_protected_route() -> Result<(), Box> { + let token = generate_token("nots3cr3t"); + let response = access_protected_route(&token, "nots3cr3t").await; + assert_eq!(response.status(), StatusCode::OK); + + let body = body_to_string(response.into_body()).await; + assert_eq!(body, "OK"); + Ok(()) +} + +// TODO: The following test should belong to `auth.rs`. However, we need a working +// D-Bus connection which is not available on containers. Let's keep the test +// here until by now. +#[test] +async fn test_access_protected_route_failed() -> Result<(), Box> { + let token = generate_token("nots3cr3t"); + let response = access_protected_route(&token, "wrong").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + Ok(()) +} diff --git a/rust/package/agama-cli.spec b/rust/package/agama-cli.spec index 9a6fdd25ea..b180f26aa7 100644 --- a/rust/package/agama-cli.spec +++ b/rust/package/agama-cli.spec @@ -34,6 +34,8 @@ BuildRequires: timezone BuildRequires: dbus-1-common # required by agama-dbus-server integration tests BuildRequires: dbus-1-daemon +BuildRequires: clang-devel +BuildRequires: pkgconfig(pam) Requires: jsonnet Requires: lshw # required by "agama logs store" @@ -85,6 +87,7 @@ install -D -d -m 0755 %{buildroot}%{_bindir} install -m 0755 %{_builddir}/agama/target/release/agama %{buildroot}%{_bindir}/agama install -m 0755 %{_builddir}/agama/target/release/agama-dbus-server %{buildroot}%{_bindir}/agama-dbus-server install -m 0755 %{_builddir}/agama/target/release/agama-web-server %{buildroot}%{_bindir}/agama-web-server +install -D -p -m 644 %{_builddir}/agama/share/agama.pam $RPM_BUILD_ROOT%{_pam_vendordir}/agama install -D -d -m 0755 %{buildroot}%{_datadir}/agama-cli install -m 0644 %{_builddir}/agama/agama-lib/share/profile.schema.json %{buildroot}%{_datadir}/agama-cli install --directory %{buildroot}%{_datadir}/dbus-1/agama-services @@ -106,6 +109,7 @@ install -m 0644 --target-directory=%{buildroot}%{_datadir}/dbus-1/agama-services %files -n agama-dbus-server %{_bindir}/agama-dbus-server %{_datadir}/dbus-1/agama-services +%{_pam_vendordir}/agama %files -n agama-web-server %{_bindir}/agama-web-server diff --git a/rust/share/agama.pam b/rust/share/agama.pam new file mode 100644 index 0000000000..76d077affe --- /dev/null +++ b/rust/share/agama.pam @@ -0,0 +1,3 @@ +#%PAM-1.0 +auth include common-auth +account include common-account