diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a87446ebc..104858f07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,11 @@ jobs: with: egress-policy: audit + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install Rust @@ -62,7 +67,7 @@ jobs: uses: swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0 - name: Run tests - run: cargo nextest run ${{ matrix.test_args }} + run: cargo nextest run ${{ matrix.test_args }} --all-features - name: Run Doc tests run: cargo test --doc diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index 115ce01b6..656156694 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -35,6 +35,11 @@ jobs: with: egress-policy: audit + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Cache @@ -72,4 +77,4 @@ jobs: - name: Execute functional tests env: OS_CLOUD: devstack - run: cargo nextest run --test functional -- --skip network::v2::auto_allocated_topology::list::list + run: cargo nextest run --test functional diff --git a/Cargo.lock b/Cargo.lock index 46ac6cd75..3e3c06d6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,45 @@ dependencies = [ "term", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -314,6 +353,28 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "async-task" version = "4.7.1" @@ -337,6 +398,33 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "authenticator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d71e457dc518a15eecc90d3b0660dee4b51623b34ac4262c9326e0d7e0f8e2" +dependencies = [ + "base64 0.21.7", + "bitflags 1.3.2", + "cfg-if", + "core-foundation", + "devd-rs", + "libc", + "libudev", + "log", + "memoffset", + "openssl", + "openssl-sys", + "rand 0.8.5", + "runloop", + "serde", + "serde_bytes", + "serde_cbor", + "serde_json", + "sha2", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -370,6 +458,17 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64urlsafedata" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5913e643e4dfb43d5908e9e6f1386f8e0dfde086ecef124a6450c6195d89160" +dependencies = [ + "base64 0.21.7", + "pastey", + "serde", +] + [[package]] name = "basic-cookies" version = "0.1.5" @@ -406,6 +505,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.9.3" @@ -706,7 +811,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.9.3", "crossterm_winapi", "mio", "parking_lot", @@ -722,7 +827,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.9.3", "crossterm_winapi", "derive_more", "document-features", @@ -831,6 +936,35 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +dependencies = [ + "powerfmt", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -894,6 +1028,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "devd-rs" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9313f104b590510b46fc01c0a324fc76505c13871454d3c48490468d04c8d395" +dependencies = [ + "libc", + "nom", +] + [[package]] name = "dialoguer" version = "0.12.0" @@ -1143,6 +1287,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -1337,6 +1496,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "handlebars" version = "6.3.2" @@ -1767,7 +1932,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -2014,8 +2179,28 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags", + "bitflags 2.9.3", + "libc", +] + +[[package]] +name = "libudev" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe" +dependencies = [ "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", ] [[package]] @@ -2118,6 +2303,21 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2145,6 +2345,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "normpath" version = "1.3.0" @@ -2163,6 +2373,42 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-modular" version = "0.6.1" @@ -2196,6 +2442,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2230,11 +2485,50 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.3", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "openstack_cli" version = "0.13.1" dependencies = [ "assert_cmd", + "base64 0.22.1", "bytes", "chrono", "clap", @@ -2253,7 +2547,7 @@ dependencies = [ "openstack_sdk", "openstack_types", "owo-colors", - "rand", + "rand 0.9.2", "regex", "reqwest", "serde", @@ -2267,6 +2561,8 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "webauthn-authenticator-rs", + "webauthn-rs-proto", ] [[package]] @@ -2274,6 +2570,7 @@ name = "openstack_sdk" version = "0.22.1" dependencies = [ "async-trait", + "base64 0.22.1", "bincode", "bytes", "chrono", @@ -2307,6 +2604,8 @@ dependencies = [ "tracing", "tracing-test", "url", + "webauthn-authenticator-rs", + "webauthn-rs-proto", ] [[package]] @@ -2423,6 +2722,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -2527,6 +2832,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "polling" version = "3.10.0" @@ -2565,6 +2876,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2632,7 +2949,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags", + "bitflags 2.9.3", "memchr", "pulldown-cmark-escape", "unicase", @@ -2673,7 +2990,7 @@ dependencies = [ "bytes", "getrandom 0.3.3", "lru-slab", - "rand", + "rand 0.9.2", "ring", "rustc-hash", "rustls", @@ -2714,14 +3031,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -2731,7 +3069,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -2749,7 +3096,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags", + "bitflags 2.9.3", "cassowary", "compact_str", "crossterm 0.28.1", @@ -2771,7 +3118,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags", + "bitflags 2.9.3", ] [[package]] @@ -2890,6 +3237,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "runloop" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d79b4b604167921892e84afbbaad9d5ad74e091bf6c511d9dbfb0593f09fabd" + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -2902,13 +3265,22 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -2921,7 +3293,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", @@ -3026,6 +3398,35 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_cbor_2" +version = "0.12.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -3367,7 +3768,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.3", "core-foundation", "system-configuration-sys", ] @@ -3471,6 +3872,36 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3545,6 +3976,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -3595,7 +4038,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags", + "bitflags 2.9.3", "bytes", "futures-util", "http 1.3.1", @@ -3748,6 +4191,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -3839,6 +4291,7 @@ checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -3854,6 +4307,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +[[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" @@ -4016,6 +4475,95 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webauthn-attestation-ca" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384e43534efe4e8f56c4eb1615a27e24d2ff29281385c843cf9f16ac1077dbdc" +dependencies = [ + "base64urlsafedata", + "openssl", + "openssl-sys", + "serde", + "tracing", + "uuid", +] + +[[package]] +name = "webauthn-authenticator-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "720d11d7d7408e6c7cf65ab4d79b1f96c2a531df4e469e12656d6b814bdcd1b1" +dependencies = [ + "async-stream", + "async-trait", + "authenticator", + "base64 0.21.7", + "base64urlsafedata", + "bitflags 1.3.2", + "futures", + "hex", + "nom", + "num-derive", + "num-traits", + "openssl", + "openssl-sys", + "rpassword", + "serde", + "serde_bytes", + "serde_cbor_2", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "unicode-normalization", + "url", + "uuid", + "webauthn-rs-core", + "webauthn-rs-proto", +] + +[[package]] +name = "webauthn-rs-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "269c210cd5f183aaca860bb5733187d1dd110ebed54640f8fc1aca31a04aa4dc" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "der-parser", + "hex", + "nom", + "openssl", + "openssl-sys", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "serde_cbor_2", + "serde_json", + "thiserror 1.0.69", + "tracing", + "url", + "uuid", + "webauthn-attestation-ca", + "webauthn-rs-proto", + "x509-parser", +] + +[[package]] +name = "webauthn-rs-proto" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144dbee9abb4bfad78fd283a2613f0312a0ed5955051b7864cfc98679112ae60" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "serde", + "serde_json", + "url", +] + [[package]] name = "webpki-roots" version = "1.0.2" @@ -4297,7 +4845,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.9.3", ] [[package]] @@ -4306,6 +4854,23 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xtask" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 87627c365..384729bbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ repository = "https://github.com/gtema/openstack" [workspace.dependencies] async-trait = { version = "^0.1" } #, optional = true } +base64 = "0.22" bytes = "^1.10" chrono = { version = "^0.4", default-features = false, features = ["clock", "serde"] } clap = { version = "^4.5", features = ["cargo", "color", "derive", "env"] } @@ -51,6 +52,8 @@ tracing = "^0.1" tracing-error = { version = "^0.2" } tracing-subscriber = { version = "^0.3", features = ["env-filter", "serde"] } url = { version = "^2.5", features = ["serde"] } +webauthn-authenticator-rs = { version = "0.5", features = ["ctap2", "mozilla", "ui-cli"]} +webauthn-rs-proto = { version = "0.5" } [profile.dev] debug = 0 diff --git a/deny.toml b/deny.toml index 9fc3950c1..33865f252 100644 --- a/deny.toml +++ b/deny.toml @@ -71,6 +71,7 @@ feature-depth = 1 # output a note when they are encountered. ignore = [ #"RUSTSEC-0000-0000", + "RUSTSEC-2021-0127", # serde_cbor as optional transitive dep: https://github.com/mozilla/authenticator-rs/issues/327 "RUSTSEC-2024-0436", "RUSTSEC-2025-0052" # unless low maintained httpmock uses it #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, diff --git a/dist-workspace.toml b/dist-workspace.toml index d14ed6c1d..0c75ed5bb 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -3,6 +3,7 @@ members = ["cargo:."] # Config for 'dist' [dist] +all-features = true # Skip checking whether the specified configuration files are up to date allow-dirty = ["ci"] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) diff --git a/openstack_cli/Cargo.toml b/openstack_cli/Cargo.toml index fb9e81a3b..d3c0dd25a 100644 --- a/openstack_cli/Cargo.toml +++ b/openstack_cli/Cargo.toml @@ -33,7 +33,7 @@ default = [ "load_balancer", "network", "object_store", - "placement" + "placement", ] block_storage = ["openstack_sdk/block_storage"] compute = ["openstack_sdk/compute"] @@ -46,6 +46,8 @@ network = ["openstack_sdk/network"] object_store = ["openstack_sdk/object_store"] placement = ["openstack_sdk/placement"] keystone_ng = ["openstack_sdk/keystone_ng", "openstack_types/keystone_ng"] +passkey = ["keystone_ng", "openstack_sdk/passkey", "dep:webauthn-authenticator-rs", "dep:webauthn-rs-proto"] + _test_admin = [] _test_net_auto-allocated-topology = [] _test_net_dhcp_agent_scheduler = [] @@ -57,20 +59,25 @@ _test_net_network-segment-range = [] _test_net_vpn = [] [dependencies] -bytes = {workspace = true} +base64 = { workspace = true } +bytes = { workspace = true } +config.workspace = true chrono = { workspace= true } clap = { workspace = true, features = ["color", "derive", "env"] } clap_complete = { workspace = true } color-eyre = { workspace = true } comfy-table = { version = "^7.2" } dialoguer = { workspace = true, features=["fuzzy-select"] } +dirs = { workspace = true } eyre = { workspace = true } http = { workspace = true } +itertools = { workspace = true } json-patch = { workspace = true } openstack_sdk = { path="../openstack_sdk", version = "^0.22", default-features = false, features = ["async", "identity"] } openstack_types = { path="../openstack_types", version = "^0.22" } owo-colors = { version = "^4.2", features = ["supports-colors"] } indicatif = "^0.18" +rand = { version = "^0.9" } regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } @@ -83,10 +90,8 @@ thiserror = { workspace = true } tracing = { workspace = true} tracing-subscriber = { workspace = true } url = { workspace = true } -config.workspace = true -dirs.workspace = true -itertools.workspace = true -rand = { version = "^0.9" } +webauthn-authenticator-rs = { workspace = true, optional = true } +webauthn-rs-proto = { workspace = true, optional = true } [dev-dependencies] assert_cmd = "^2.0" diff --git a/openstack_cli/src/cli.rs b/openstack_cli/src/cli.rs index df18b944f..3d798c9ca 100644 --- a/openstack_cli/src/cli.rs +++ b/openstack_cli/src/cli.rs @@ -33,6 +33,8 @@ use crate::config::{Config, ConfigError}; use crate::container_infrastructure_management::v1 as container_infra; use crate::dns::v2 as dns; use crate::identity::v3 as identity; +#[cfg(feature = "keystone_ng")] +use crate::identity::v4 as identity_v4; use crate::image::v2 as image; use crate::load_balancer::v2 as load_balancer; use crate::network::v2 as network; @@ -277,6 +279,8 @@ pub enum TopLevelCommands { ContainerInfrastructure(container_infra::ContainerInfrastructureCommand), Dns(dns::DnsCommand), Identity(identity::IdentityCommand), + #[cfg(feature = "keystone_ng")] + Identity4(identity_v4::IdentityCommand), Image(image::ImageCommand), LoadBalancer(load_balancer::LoadBalancerCommand), Network(network::NetworkCommand), @@ -314,6 +318,8 @@ impl Cli { TopLevelCommands::ContainerInfrastructure(args) => args.take_action(self, client).await, TopLevelCommands::Dns(args) => args.take_action(self, client).await, TopLevelCommands::Identity(args) => args.take_action(self, client).await, + #[cfg(feature = "keystone_ng")] + TopLevelCommands::Identity4(args) => args.take_action(self, client).await, TopLevelCommands::Image(args) => args.take_action(self, client).await, TopLevelCommands::LoadBalancer(args) => args.take_action(self, client).await, TopLevelCommands::Network(args) => args.take_action(self, client).await, diff --git a/openstack_cli/src/error.rs b/openstack_cli/src/error.rs index c81a3cf20..fa96e6717 100644 --- a/openstack_cli/src/error.rs +++ b/openstack_cli/src/error.rs @@ -179,6 +179,10 @@ pub enum OpenStackCliError { #[error("input parameters error: {0}")] InputParameters(String), + /// Base64 decoding error. + #[error(transparent)] + Base64Decode(#[from] base64::DecodeError), + /// Others. #[error(transparent)] Other(#[from] eyre::Report), diff --git a/openstack_cli/src/identity/mod.rs b/openstack_cli/src/identity/mod.rs index c47f77ce3..0b591c4de 100644 --- a/openstack_cli/src/identity/mod.rs +++ b/openstack_cli/src/identity/mod.rs @@ -14,3 +14,5 @@ //! Identity (Keystone) API bindings pub mod v3; +#[cfg(feature = "keystone_ng")] +pub mod v4; diff --git a/openstack_cli/src/identity/v4.rs b/openstack_cli/src/identity/v4.rs new file mode 100644 index 000000000..fd3bcbe3f --- /dev/null +++ b/openstack_cli/src/identity/v4.rs @@ -0,0 +1,63 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Identity v3 API commands +use clap::{Parser, Subcommand}; + +use openstack_sdk::AsyncOpenStack; + +use crate::{Cli, OpenStackCliError}; + +pub mod user; + +/// Identity (Keystone) commands +/// +/// The Identity service generates authentication tokens that permit access to the OpenStack +/// services REST APIs. Clients obtain this token and the URL endpoints for other service APIs by +/// supplying their valid credentials to the authentication service. +/// +/// Each time you make a REST API request to an OpenStack service, you supply your authentication +/// token in the X-Auth-Token request header. +/// +/// Like most OpenStack projects, OpenStack Identity protects its APIs by defining policy rules +/// based on a role-based access control (RBAC) approach. +/// +/// The Identity service configuration file sets the name and location of a JSON policy file that +/// stores these rules. +#[derive(Parser)] +pub struct IdentityCommand { + /// subcommand + #[command(subcommand)] + command: IdentityCommands, +} + +/// Supported subcommands +#[allow(missing_docs)] +#[derive(Subcommand)] +pub enum IdentityCommands { + User(user::UserCommand), +} + +impl IdentityCommand { + /// Perform command action + pub async fn take_action( + &self, + parsed_args: &Cli, + session: &mut AsyncOpenStack, + ) -> Result<(), OpenStackCliError> { + match &self.command { + IdentityCommands::User(cmd) => cmd.take_action(parsed_args, session).await, + } + } +} diff --git a/openstack_cli/src/identity/v4/user/mod.rs b/openstack_cli/src/identity/v4/user/mod.rs new file mode 100644 index 000000000..b2bae7dec --- /dev/null +++ b/openstack_cli/src/identity/v4/user/mod.rs @@ -0,0 +1,54 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Identity User commands + +use clap::{Parser, Subcommand}; + +use openstack_sdk::AsyncOpenStack; + +use crate::{Cli, OpenStackCliError}; + +#[cfg(feature = "passkey")] +pub mod passkey; + +/// User commands +/// +#[derive(Parser)] +pub struct UserCommand { + #[command(subcommand)] + command: UserCommands, +} + +/// Supported subcommands +#[allow(missing_docs)] +#[derive(Subcommand)] +pub enum UserCommands { + #[cfg(feature = "passkey")] + Passkey(passkey::PasskeyCommand), +} + +impl UserCommand { + /// Perform command action + pub async fn take_action( + &self, + parsed_args: &Cli, + session: &mut AsyncOpenStack, + ) -> Result<(), OpenStackCliError> { + match &self.command { + #[cfg(feature = "passkey")] + UserCommands::Passkey(cmd) => cmd.take_action(parsed_args, session).await, + } + } +} diff --git a/openstack_cli/src/identity/v4/user/passkey.rs b/openstack_cli/src/identity/v4/user/passkey.rs new file mode 100644 index 000000000..0a59138c6 --- /dev/null +++ b/openstack_cli/src/identity/v4/user/passkey.rs @@ -0,0 +1,51 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Identity Passkey passkey commands + +use clap::{Parser, Subcommand}; + +use openstack_sdk::AsyncOpenStack; + +use crate::{Cli, OpenStackCliError}; + +pub mod register; + +/// Passkey passkey commands +/// +#[derive(Parser)] +pub struct PasskeyCommand { + #[command(subcommand)] + command: PasskeyCommands, +} + +/// Supported subcommands +#[allow(missing_docs)] +#[derive(Subcommand)] +pub enum PasskeyCommands { + Register(register::PasskeyCommand), +} + +impl PasskeyCommand { + /// Perform command action + pub async fn take_action( + &self, + parsed_args: &Cli, + session: &mut AsyncOpenStack, + ) -> Result<(), OpenStackCliError> { + match &self.command { + PasskeyCommands::Register(cmd) => cmd.take_action(parsed_args, session).await, + } + } +} diff --git a/openstack_cli/src/identity/v4/user/passkey/register.rs b/openstack_cli/src/identity/v4/user/passkey/register.rs new file mode 100644 index 000000000..035869236 --- /dev/null +++ b/openstack_cli/src/identity/v4/user/passkey/register.rs @@ -0,0 +1,547 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: This file is automatically generated from OpenAPI schema using +// `openstack-codegenerator`. + +//! Create Passkey command +//! +//! + +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use clap::Args; +use eyre::{OptionExt, WrapErr, eyre}; +use tracing::{error, info, warn}; + +use webauthn_authenticator_rs::WebauthnAuthenticator; +use webauthn_authenticator_rs::prelude::Url; + +use openstack_sdk::AsyncOpenStack; + +use crate::Cli; +use crate::OpenStackCliError; +use crate::output::OutputProcessor; + +use openstack_sdk::api::QueryAsync; +use openstack_sdk::api::find_by_name; +use openstack_sdk::api::identity::v3::user::find as find_user; +use openstack_sdk::api::identity::v4::user::passkey::{register_finish, register_start}; +use openstack_types::identity::v4::user::passkey::response::{ + register_finish as passkey, register_start as register, +}; + +/// Register new passkey for the user +#[derive(Args)] +#[command(about = "Create the identity provider.")] +pub struct PasskeyCommand { + /// Path parameters + #[command(flatten)] + path: PathParameters, + + /// Passkey information + #[command(flatten)] + passkey: Passkey, +} + +/// Path parameters +#[derive(Args)] +struct PathParameters { + /// User resource for which the operation should be performed. + #[command(flatten)] + user: UserInput, +} + +/// User input select group +#[derive(Args)] +#[group(required = false, multiple = false)] +struct UserInput { + /// User Name. + #[arg(long, help_heading = "Path parameters", value_name = "USER_NAME")] + user_name: Option, + /// User ID. + #[arg(long, help_heading = "Path parameters", value_name = "USER_ID")] + user_id: Option, + /// Current authenticated user. + #[arg(long, help_heading = "Path parameters", action = clap::ArgAction::SetTrue)] + current_user: bool, +} + +/// Passkey data +#[derive(Args, Clone)] +struct Passkey { + /// Passkey description + #[arg(help_heading = "Body parameters", long)] + description: Option, +} + +#[inline(always)] +fn convert_api_parameters_to_webauthn( + val: register::RegisterStartResponse, +) -> Result { + Ok( + webauthn_authenticator_rs::prelude::CreationChallengeResponse { + public_key: convert_api_response_to_public_key_credential_options(val)?, + }, + ) +} + +#[inline(always)] +fn convert_api_response_to_public_key_credential_options( + val: register::RegisterStartResponse, +) -> Result { + Ok( + webauthn_rs_proto::attest::PublicKeyCredentialCreationOptions { + attestation: val.attestation.map(|att| convert_attestation(att)), + attestation_formats: val.attestation_formats.map(|ats| { + ats.into_iter() + .map(|at| convert_attestation_format(at)) + .collect::>() + }), + authenticator_selection: val + .authenticator_selection + .map(|authr| convert_authenticator_selection(authr)), + challenge: URL_SAFE.decode(val.challenge)?.into(), + exclude_credentials: val + .exclude_credentials + .map(|ecs| { + ecs.into_iter() + .map(|cred| convert_exclude_credential(cred)) + //.transpose()? + .collect::, _>>() + }) + .transpose()?, //.collect::>() + extensions: val.extensions.map(|ext| convert_extension(ext)), + hints: val.hints.map(|hints| { + hints + .into_iter() + .map(|hint| convert_hint(hint)) + .collect::>() + }), + pub_key_cred_params: val + .pub_key_cred_params + .into_iter() + .map(|cp| convert_pub_key_cred_params(cp)) + .collect::>(), + rp: convert_rp(val.rp), + timeout: val.timeout, + user: convert_user(val.user)?, + }, + ) +} + +#[inline(always)] +fn convert_attestation( + val: register::Attestation, +) -> webauthn_rs_proto::options::AttestationConveyancePreference { + match val { + register::Attestation::Direct => { + webauthn_rs_proto::options::AttestationConveyancePreference::Direct + } + register::Attestation::Indirect => { + webauthn_rs_proto::options::AttestationConveyancePreference::Indirect + } + register::Attestation::None => { + webauthn_rs_proto::options::AttestationConveyancePreference::None + } + } +} + +#[inline(always)] +fn convert_attestation_format( + val: register::AttestationFormats, +) -> webauthn_rs_proto::options::AttestationFormat { + match val { + register::AttestationFormats::Androidkey => { + webauthn_rs_proto::options::AttestationFormat::AndroidKey + } + register::AttestationFormats::Androidsafetynet => { + webauthn_rs_proto::options::AttestationFormat::AndroidSafetyNet + } + register::AttestationFormats::Appleanonymous => { + webauthn_rs_proto::options::AttestationFormat::AppleAnonymous + } + register::AttestationFormats::Fidou2f => { + webauthn_rs_proto::options::AttestationFormat::FIDOU2F + } + register::AttestationFormats::None => webauthn_rs_proto::options::AttestationFormat::None, + register::AttestationFormats::Packed => { + webauthn_rs_proto::options::AttestationFormat::Packed + } + register::AttestationFormats::Tpm => webauthn_rs_proto::options::AttestationFormat::Tpm, + } +} + +#[inline(always)] +fn convert_authenticator_selection( + val: register::AuthenticatorSelection, +) -> webauthn_rs_proto::options::AuthenticatorSelectionCriteria { + webauthn_rs_proto::options::AuthenticatorSelectionCriteria { + authenticator_attachment: val + .authenticator_attachment + .map(|authra| convert_authenticator_attachment(authra)), + resident_key: val.resident_key.map(|key| convert_resident_key(key)), + require_resident_key: val.require_resident_key, + user_verification: convert_user_verification(val.user_verification), + } +} + +#[inline(always)] +fn convert_authenticator_attachment( + val: register::AuthenticatorAttachment, +) -> webauthn_rs_proto::options::AuthenticatorAttachment { + match val { + register::AuthenticatorAttachment::Crossplatform => { + webauthn_rs_proto::options::AuthenticatorAttachment::CrossPlatform + } + register::AuthenticatorAttachment::Platform => { + webauthn_rs_proto::options::AuthenticatorAttachment::Platform + } + } +} + +#[inline(always)] +fn convert_resident_key( + val: register::ResidentKey, +) -> webauthn_rs_proto::options::ResidentKeyRequirement { + match val { + register::ResidentKey::Discouraged => { + webauthn_rs_proto::options::ResidentKeyRequirement::Discouraged + } + register::ResidentKey::Preferred => { + webauthn_rs_proto::options::ResidentKeyRequirement::Preferred + } + register::ResidentKey::Required => { + webauthn_rs_proto::options::ResidentKeyRequirement::Required + } + } +} + +#[inline(always)] +fn convert_user_verification( + val: register::UserVerification, +) -> webauthn_rs_proto::options::UserVerificationPolicy { + match val { + register::UserVerification::Discourageddonotuse => { + webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE + } + register::UserVerification::Preferred => { + webauthn_rs_proto::options::UserVerificationPolicy::Preferred + } + register::UserVerification::Required => { + webauthn_rs_proto::options::UserVerificationPolicy::Required + } + } +} + +#[inline(always)] +fn convert_exclude_credential( + val: register::ExcludeCredentials, +) -> Result { + Ok(webauthn_rs_proto::options::PublicKeyCredentialDescriptor { + id: URL_SAFE.decode(val.id)?.into(), + type_: val.type_, + transports: val.transports.map(|trs| { + trs.into_iter() + .map(|tr| convert_transport(tr)) + .collect::>() + }), + }) +} + +#[inline(always)] +fn convert_transport( + val: register::Transports, +) -> webauthn_rs_proto::options::AuthenticatorTransport { + match val { + register::Transports::Ble => webauthn_rs_proto::options::AuthenticatorTransport::Ble, + register::Transports::Hybrid => webauthn_rs_proto::options::AuthenticatorTransport::Hybrid, + register::Transports::Internal => { + webauthn_rs_proto::options::AuthenticatorTransport::Internal + } + register::Transports::Nfc => webauthn_rs_proto::options::AuthenticatorTransport::Nfc, + register::Transports::Test => webauthn_rs_proto::options::AuthenticatorTransport::Test, + register::Transports::Unknown => { + webauthn_rs_proto::options::AuthenticatorTransport::Unknown + } + register::Transports::Usb => webauthn_rs_proto::options::AuthenticatorTransport::Usb, + } +} + +#[inline(always)] +fn convert_transport_webauthn_to_keystone( + val: webauthn_rs_proto::options::AuthenticatorTransport, +) -> register_finish::Transports { + match val { + webauthn_rs_proto::options::AuthenticatorTransport::Ble => register_finish::Transports::Ble, + webauthn_rs_proto::options::AuthenticatorTransport::Hybrid => { + register_finish::Transports::Hybrid + } + webauthn_rs_proto::options::AuthenticatorTransport::Internal => { + register_finish::Transports::Internal + } + webauthn_rs_proto::options::AuthenticatorTransport::Nfc => register_finish::Transports::Nfc, + webauthn_rs_proto::options::AuthenticatorTransport::Test => { + register_finish::Transports::Test + } + webauthn_rs_proto::options::AuthenticatorTransport::Unknown => { + register_finish::Transports::Unknown + } + webauthn_rs_proto::options::AuthenticatorTransport::Usb => register_finish::Transports::Usb, + } +} + +#[inline(always)] +fn convert_hint(val: register::Hints) -> webauthn_rs_proto::options::PublicKeyCredentialHints { + match val { + register::Hints::Clientdevice => { + webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice + } + register::Hints::Hybrid => webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid, + register::Hints::Securitykey => { + webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey + } + } +} + +#[inline(always)] +fn convert_extension( + val: register::Extensions, +) -> webauthn_rs_proto::extensions::RequestRegistrationExtensions { + webauthn_rs_proto::extensions::RequestRegistrationExtensions { + cred_props: val.cred_props, + cred_protect: val.cred_protect.map(|cp| convert_cred_protect(cp)), + hmac_create_secret: val.hmac_create_secret, + min_pin_length: val.min_pin_length, + uvm: val.uvm, + } +} + +#[inline(always)] +fn convert_cred_protect(val: register::CredProtect) -> webauthn_rs_proto::extensions::CredProtect { + webauthn_rs_proto::extensions::CredProtect { + credential_protection_policy: convert_credential_protection_policy( + val.credential_protection_policy, + ), + enforce_credential_protection_policy: val.enforce_credential_protection_policy, + } +} + +#[inline(always)] +fn convert_credential_protection_policy( + val: register::CredentialProtectionPolicy, +) -> webauthn_rs_proto::extensions::CredentialProtectionPolicy { + match val { + register::CredentialProtectionPolicy::Userverificationoptional => { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional + } + register::CredentialProtectionPolicy::Userverificationoptionalwithcredentialidlist => { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList + } + register::CredentialProtectionPolicy::Userverificationrequired => { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired + } + } +} + +#[inline(always)] +fn convert_pub_key_cred_params( + val: register::PubKeyCredParams, +) -> webauthn_rs_proto::options::PubKeyCredParams { + webauthn_rs_proto::options::PubKeyCredParams { + alg: val.alg, + type_: val.type_, + } +} + +#[inline(always)] +fn convert_rp(val: register::Rp) -> webauthn_rs_proto::options::RelyingParty { + webauthn_rs_proto::options::RelyingParty { + id: val.id, + name: val.name, + } +} + +#[inline(always)] +fn convert_user( + val: register::User, +) -> Result { + Ok(webauthn_rs_proto::options::User { + id: URL_SAFE.decode(val.id)?.into(), + name: val.name, + display_name: val.display_name, + }) +} + +fn get_finish_registration_endpoint( + user_id: String, + register_cred: webauthn_authenticator_rs::prelude::RegisterPublicKeyCredential, +) -> Result, OpenStackCliError> { + let mut builder = register_finish::Request::builder(); + builder.id(register_cred.id); + builder.raw_id(URL_SAFE.encode(register_cred.raw_id)); + builder.type_(register_cred.type_); + builder.user_id(user_id); + + let mut rsp = register_finish::ResponseBuilder::default(); + + rsp.attestation_object(URL_SAFE.encode(register_cred.response.attestation_object)); + rsp.client_data_json(URL_SAFE.encode(register_cred.response.client_data_json)); + if let Some(transports) = register_cred.response.transports { + rsp.transports( + transports + .into_iter() + .map(|tr| convert_transport_webauthn_to_keystone(tr)) + .collect::>(), + ); + } + + builder.response( + rsp.build() + .wrap_err_with(|| eyre!("cannot build passkey `response` structure"))?, + ); + + let mut extensions = register_finish::ExtensionsBuilder::default(); + if let Some(val) = register_cred.extensions.appid { + extensions.appid(val); + } + if let Some(val) = register_cred.extensions.cred_props { + extensions.cred_props( + register_finish::CredPropsBuilder::default() + .rk(val.rk) + .build() + .wrap_err_with(|| eyre!("cannot build passkey `cred_props` structure"))?, + ); + } + if let Some(val) = register_cred.extensions.cred_protect { + extensions.cred_protect(match val { + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptional => register_finish::CredProtect::Userverificationoptional, + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIDList => register_finish::CredProtect::Userverificationoptionalwithcredentialidlist, + webauthn_rs_proto::extensions::CredentialProtectionPolicy::UserVerificationRequired => register_finish::CredProtect::Userverificationrequired, + }); + } + if let Some(val) = register_cred.extensions.hmac_secret { + extensions.hmac_secret(val); + } + if let Some(val) = register_cred.extensions.min_pin_length { + extensions.min_pin_length(val); + } + builder.extensions( + extensions + .build() + .wrap_err_with(|| eyre!("cannot build passkey `extensions` structure"))?, + ); + + Ok(builder + .build() + .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?) +} + +impl PasskeyCommand { + /// Perform command action + pub async fn take_action( + &self, + parsed_args: &Cli, + client: &mut AsyncOpenStack, + ) -> Result<(), OpenStackCliError> { + info!("Create Passkey"); + + let op = OutputProcessor::from_args( + parsed_args, + Some("identity.user/passkey"), + Some("register"), + ); + op.validate_args(parsed_args)?; + + let mut ep_builder = register_start::Request::builder(); + + let user_id = if let Some(id) = &self.path.user.user_id { + // user_id is passed. No need to lookup + id.clone() + } else if let Some(name) = &self.path.user.user_name { + // user_name is passed. Need to lookup resource + let mut sub_find_builder = find_user::Request::builder(); + warn!( + "Querying user by name (because of `--user-name` parameter passed) may not be definite. This may fail in which case parameter `--user-id` should be used instead." + ); + + sub_find_builder.id(name); + let find_ep = sub_find_builder + .build() + .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; + let find_data: serde_json::Value = find_by_name(find_ep).query_async(client).await?; + // Try to extract resource id + match find_data.get("id") { + Some(val) => match val.as_str() { + Some(id_str) => id_str.to_owned(), + None => { + return Err(OpenStackCliError::ResourceAttributeNotString( + serde_json::to_string(&val)?, + )); + } + }, + None => { + return Err(OpenStackCliError::ResourceAttributeMissing( + "id".to_string(), + )); + } + } + } else if self.path.user.current_user { + client + .get_auth_info() + .ok_or_eyre("Cannot determine current authentication information")? + .token + .user + .id + } else { + return Err(eyre!("cannot determine the user").into()); + }; + ep_builder.user_id(user_id.clone()); + let mut passkey = register_start::PasskeyBuilder::default(); + if let Some(description) = &self.passkey.description { + passkey.description(description); + } + + ep_builder.passkey( + passkey + .build() + .wrap_err_with(|| eyre!("cannot build `passkey` structure"))?, + ); + let ep = ep_builder + .build() + .map_err(|x| OpenStackCliError::EndpointBuild(x.to_string()))?; + let pk_request: register::RegisterStartResponse = ep.query_async(client).await?; + + let mut auth = WebauthnAuthenticator::new( + webauthn_authenticator_rs::mozilla::MozillaAuthenticator::new(), + ); + match auth.do_registration( + Url::parse("http://localhost:8080").unwrap(), + convert_api_parameters_to_webauthn(pk_request)?, + ) { + Ok(rsp) => { + let ep = get_finish_registration_endpoint(user_id, rsp)?; + let data = ep.query_async(client).await?; + op.output_single::(data)?; + } + Err(e) => { + error!("Error -> {:x?}", e); + return Err(eyre!("Registration failed").into()); + } + } + + // Show command specific hints + op.show_command_hint()?; + Ok(()) + } +} diff --git a/openstack_sdk/Cargo.toml b/openstack_sdk/Cargo.toml index 7c9234160..3332036ba 100644 --- a/openstack_sdk/Cargo.toml +++ b/openstack_sdk/Cargo.toml @@ -41,9 +41,11 @@ async = [] client_der = [] client_pem = [] keystone_ng = [] +passkey = ["keystone_ng", "dep:webauthn-authenticator-rs", "dep:webauthn-rs-proto"] [dependencies] async-trait = {workspace = true} +base64 = { workspace = true } bincode = { version = "^2.0", default-features = false, features = ["serde", "std"] } bytes = {workspace = true} chrono = { workspace= true } @@ -73,6 +75,8 @@ tokio = { workspace = true } tokio-util = {workspace = true} tracing = { workspace = true} url = { workspace = true } +webauthn-authenticator-rs = { workspace = true, optional = true } +webauthn-rs-proto = { workspace = true, optional = true } [dev-dependencies] httpmock = "^0.7" diff --git a/openstack_sdk/src/api/identity/v4.rs b/openstack_sdk/src/api/identity/v4.rs index 5e292b27c..4184f371c 100644 --- a/openstack_sdk/src/api/identity/v4.rs +++ b/openstack_sdk/src/api/identity/v4.rs @@ -15,6 +15,6 @@ // WARNING: This file is automatically generated from OpenAPI schema using // `openstack-codegenerator`. -//! `Identity` v4 Service bindings +//! `Identity` Service bindings pub mod federation; pub mod user; diff --git a/openstack_sdk/src/api/identity/v4/user/passkey/register_finish.rs b/openstack_sdk/src/api/identity/v4/user/passkey/register_finish.rs index ebd2f374e..82131bbd6 100644 --- a/openstack_sdk/src/api/identity/v4/user/passkey/register_finish.rs +++ b/openstack_sdk/src/api/identity/v4/user/passkey/register_finish.rs @@ -224,7 +224,7 @@ impl RestEndpoint for Request<'_> { } fn response_key(&self) -> Option> { - None + Some("passkey".into()) } /// Returns headers to be set into the request @@ -273,22 +273,25 @@ mod tests { #[test] fn test_response_key() { - assert!(Request::builder() - .extensions(ExtensionsBuilder::default().build().unwrap()) - .id("foo") - .raw_id("foo") - .response( - ResponseBuilder::default() - .attestation_object("foo") - .client_data_json("foo") - .build() - .unwrap() - ) - .type_("foo") - .build() - .unwrap() - .response_key() - .is_none()) + assert_eq!( + Request::builder() + .extensions(ExtensionsBuilder::default().build().unwrap()) + .id("foo") + .raw_id("foo") + .response( + ResponseBuilder::default() + .attestation_object("foo") + .client_data_json("foo") + .build() + .unwrap() + ) + .type_("foo") + .build() + .unwrap() + .response_key() + .unwrap(), + "passkey" + ); } #[cfg(feature = "sync")] @@ -304,7 +307,7 @@ mod tests { then.status(200) .header("content-type", "application/json") - .json_body(json!({ "dummy": {} })); + .json_body(json!({ "passkey": {} })); }); let endpoint = Request::builder() @@ -341,7 +344,7 @@ mod tests { .header("not_foo", "not_bar"); then.status(200) .header("content-type", "application/json") - .json_body(json!({ "dummy": {} })); + .json_body(json!({ "passkey": {} })); }); let endpoint = Request::builder() diff --git a/openstack_sdk/src/api/identity/v4/user/passkey/register_start.rs b/openstack_sdk/src/api/identity/v4/user/passkey/register_start.rs index 81ecb8b08..608290c45 100644 --- a/openstack_sdk/src/api/identity/v4/user/passkey/register_start.rs +++ b/openstack_sdk/src/api/identity/v4/user/passkey/register_start.rs @@ -24,14 +24,26 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use crate::api::rest_endpoint_prelude::*; +use serde::Deserialize; +use serde::Serialize; use std::borrow::Cow; +/// Passkey information. +#[derive(Builder, Debug, Deserialize, Clone, Serialize)] +#[builder(setter(strip_option))] +pub struct Passkey<'a> { + /// Passkey description + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default, setter(into))] + pub(crate) description: Option>, +} + #[derive(Builder, Debug, Clone)] #[builder(setter(strip_option))] pub struct Request<'a> { - /// The description for the passkey (name). + /// Passkey information. #[builder(setter(into))] - pub(crate) description: Option>, + pub(crate) passkey: Passkey<'a>, /// The ID of the user. #[builder(default, setter(into))] @@ -95,7 +107,7 @@ impl RestEndpoint for Request<'_> { fn body(&self) -> Result)>, BodyError> { let mut params = JsonBodyParams::default(); - params.push("description", serde_json::to_value(&self.description)?); + params.push("passkey", serde_json::to_value(&self.passkey)?); params.into_body() } @@ -134,7 +146,7 @@ mod tests { fn test_service_type() { assert_eq!( Request::builder() - .description("foo") + .passkey(PasskeyBuilder::default().build().unwrap()) .build() .unwrap() .service_type(), @@ -146,7 +158,7 @@ mod tests { fn test_response_key() { assert_eq!( Request::builder() - .description("foo") + .passkey(PasskeyBuilder::default().build().unwrap()) .build() .unwrap() .response_key() @@ -173,7 +185,7 @@ mod tests { let endpoint = Request::builder() .user_id("user_id") - .description("foo") + .passkey(PasskeyBuilder::default().build().unwrap()) .build() .unwrap(); let _: serde_json::Value = endpoint.query(&client).unwrap(); @@ -200,7 +212,7 @@ mod tests { let endpoint = Request::builder() .user_id("user_id") - .description("foo") + .passkey(PasskeyBuilder::default().build().unwrap()) .headers( [( Some(HeaderName::from_static("foo")), diff --git a/openstack_sdk/src/auth.rs b/openstack_sdk/src/auth.rs index 53b1fc2bc..0bca910c7 100644 --- a/openstack_sdk/src/auth.rs +++ b/openstack_sdk/src/auth.rs @@ -38,6 +38,8 @@ pub mod v3totp; pub mod v3websso; #[cfg(feature = "keystone_ng")] pub mod v4federation; +#[cfg(feature = "passkey")] +pub mod v4passkey; use authtoken::{AuthToken, AuthTokenError}; use authtoken_scope::AuthTokenScopeError; @@ -103,6 +105,15 @@ impl From for AuthError { } } +#[cfg(feature = "passkey")] +impl From for AuthError { + fn from(source: v4passkey::PasskeyError) -> Self { + Self::AuthToken { + source: source.into(), + } + } +} + /// Authentication state enum #[derive(Debug, Eq, PartialEq)] pub enum AuthState { diff --git a/openstack_sdk/src/auth/authtoken.rs b/openstack_sdk/src/auth/authtoken.rs index 6fa24a38f..3ac393339 100644 --- a/openstack_sdk/src/auth/authtoken.rs +++ b/openstack_sdk/src/auth/authtoken.rs @@ -28,6 +28,8 @@ use crate::api::RestEndpoint; use crate::auth::auth_token_endpoint as token_v3; #[cfg(feature = "keystone_ng")] use crate::auth::v4federation; +#[cfg(feature = "passkey")] +use crate::auth::v4passkey; use crate::auth::{ auth_helper::AuthHelper, authtoken_scope, v3applicationcredential, v3oidcaccesstoken, v3password, v3token, v3totp, v3websso, AuthState, @@ -166,6 +168,15 @@ pub enum AuthTokenError { #[from] source: v4federation::FederationError, }, + + /// Passkey error + #[cfg(feature = "passkey")] + #[error("Passkey based authentication error: {}", source)] + Passkey { + /// The error source + #[from] + source: v4passkey::PasskeyError, + }, } type AuthResult = Result; @@ -261,9 +272,12 @@ pub enum AuthType { V3Multifactor, /// WebSSO V3WebSso, - /// Federation #[cfg(feature = "keystone_ng")] + /// Federation. V4Federation, + #[cfg(feature = "passkey")] + /// Passkey. + V4Passkey, } impl FromStr for AuthType { @@ -282,6 +296,8 @@ impl FromStr for AuthType { "v3websso" => Ok(Self::V3WebSso), #[cfg(feature = "keystone_ng")] "v4federation" | "federation" => Ok(Self::V4Federation), + #[cfg(feature = "passkey")] + "v4passkey" | "passkey" => Ok(Self::V4Passkey), other => Err(Self::Err::IdentityMethod { auth_type: other.into(), }), @@ -311,6 +327,8 @@ impl AuthType { Self::V3WebSso => "v3websso", #[cfg(feature = "keystone_ng")] Self::V4Federation => "v4federation", + #[cfg(feature = "passkey")] + Self::V4Passkey => "v4passkey", } } } diff --git a/openstack_sdk/src/auth/v4passkey.rs b/openstack_sdk/src/auth/v4passkey.rs new file mode 100644 index 000000000..af88af299 --- /dev/null +++ b/openstack_sdk/src/auth/v4passkey.rs @@ -0,0 +1,24 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Passkey (security device) based login. +//! + +mod error; +mod finish; +mod start; + +pub use error::PasskeyError; +pub use finish::get_finish_auth_ep; +pub use start::{get_init_auth_ep, PasskeyAuthenticationStartResponse}; diff --git a/openstack_sdk/src/auth/v4passkey/error.rs b/openstack_sdk/src/auth/v4passkey/error.rs new file mode 100644 index 000000000..e77310468 --- /dev/null +++ b/openstack_sdk/src/auth/v4passkey/error.rs @@ -0,0 +1,86 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Passkey (security device) based login. +//! + +use thiserror::Error; +use tracing::error; + +use super::finish; +use super::start; + +/// Passkey auth related errors. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum PasskeyError { + /// Auth data is missing + #[error("auth data is missing")] + MissingAuthData, + + /// Start authentication request builder. + #[error("error preparing auth request: {}", source)] + InitAuthBuilder { + #[from] + source: start::AuthStartRequestBuilderError, + }, + + /// Start authentication passkey request builder. + #[error("error preparing auth request: {}", source)] + InitPasskeyBuilder { + #[from] + source: start::PasskeyBuilderError, + }, + + /// Finish authentication request builder. + #[error(transparent)] + FinishPasskeyBuilder { + #[from] + source: finish::AuthFinishRequestBuilderError, + }, + + /// HmacGetSecretOutput. + #[error(transparent)] + HmacGetSecretOutput { + #[from] + source: finish::HmacGetSecretOutputBuilderError, + }, + + /// Passkey auth extensions. + #[error(transparent)] + AuthExtensions { + #[from] + source: finish::AuthenticationExtensionsClientOutputsBuilderError, + }, + + /// Passkey auth response. + #[error(transparent)] + AuthResponse { + #[from] + source: finish::AuthenticatorAssertionResponseRawBuilderError, + }, + /// Base64 decode. + #[error(transparent)] + Base64 { + #[from] + source: base64::DecodeError, + }, + + /// WebAuthn. + #[error(transparent)] + WebAuthn { + #[from] + source: webauthn_authenticator_rs::error::WebauthnCError, + }, +} diff --git a/openstack_sdk/src/auth/v4passkey/finish.rs b/openstack_sdk/src/auth/v4passkey/finish.rs new file mode 100644 index 000000000..217836420 --- /dev/null +++ b/openstack_sdk/src/auth/v4passkey/finish.rs @@ -0,0 +1,181 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Finish passkey (security device) auth: exchange signed challenge with Keystone token. + +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; + +use crate::api::rest_endpoint_prelude::*; +use crate::api::RestEndpoint; +use crate::auth::auth_helper::AuthHelper; +use crate::config; +use crate::types::{ApiVersion, ServiceType}; + +use super::error::PasskeyError; + +/// A client response to an authentication challenge. This contains all required information to +/// asses and assert trust in a credentials legitimacy, followed by authentication to a user. +/// +/// You should not need to handle the inner content of this structure - you should provide this to +/// the correctly handling function of Webauthn only. +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize)] +#[builder(setter(into, strip_option))] +pub struct AuthFinishRequest<'a> { + /// The credential Id, likely base64. + id: Cow<'a, str>, + /// Unsigned Client processed extensions. + extensions: AuthenticationExtensionsClientOutputs<'a>, + /// The binary of the credential id. + raw_id: Cow<'a, str>, + /// The authenticator response. + response: AuthenticatorAssertionResponseRaw<'a>, + /// The authenticator type. + type_: Cow<'a, str>, + /// The ID of the user. + user_id: Cow<'a, str>, +} + +impl RestEndpoint for AuthFinishRequest<'_> { + fn method(&self) -> http::Method { + http::Method::POST + } + + fn endpoint(&self) -> Cow<'static, str> { + "auth/passkey/finish".to_string().into() + } + + fn body(&self) -> Result)>, BodyError> { + let mut params = JsonBodyParams::default(); + + params.push("id", &self.id); + params.push("extensions", serde_json::to_value(&self.extensions)?); + params.push("raw_id", &self.raw_id); + params.push("response", serde_json::to_value(&self.response)?); + params.push("type_", &self.type_); + params.push("user_id", &self.user_id); + + params.into_body() + } + + fn service_type(&self) -> ServiceType { + ServiceType::Identity + } + + /// Returns required API version + fn api_version(&self) -> Option { + Some(ApiVersion::new(4, 0)) + } +} + +/// [AuthenticatorAssertionResponseRaw](https://w3c.github.io/webauthn/#authenticatorassertionresponse) +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize)] +#[builder(setter(into, strip_option))] +pub struct AuthenticatorAssertionResponseRaw<'a> { + /// Raw authenticator data. + pub authenticator_data: Cow<'a, str>, + /// Signed client data. + pub client_data_json: Cow<'a, str>, + /// Signature. + pub signature: Cow<'a, str>, + /// Optional userhandle. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub user_handle: Option>, +} + +/// [AuthenticationExtensionsClientOutputs](https://w3c.github.io/webauthn/#dictdef-authenticationextensionsclientoutputs) +/// +/// The default option here for Options are None, so it can be derived +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize)] +#[builder(setter(into, strip_option))] +pub struct AuthenticationExtensionsClientOutputs<'a> { + /// Indicates whether the client used the provided appid extension. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub appid: Option, + /// The response to a hmac get secret request. + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub hmac_get_secret: Option>, +} + +/// The response to a hmac get secret request. +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize)] +#[builder(setter(into, strip_option))] +pub struct HmacGetSecretOutput<'a> { + /// Output of HMAC(Salt 1 || Client Secret). + pub output1: Cow<'a, str>, + /// Output of HMAC(Salt 2 || Client Secret). + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub output2: Option>, +} + +impl TryFrom + for AuthFinishRequestBuilder<'_> +{ + type Error = PasskeyError; + fn try_from( + value: webauthn_authenticator_rs::prelude::PublicKeyCredential, + ) -> Result { + let mut req = AuthFinishRequestBuilder::default(); + req.id(value.id); + req.raw_id(URL_SAFE.encode(value.raw_id)); + req.type_(value.type_); + let mut ext_builder = AuthenticationExtensionsClientOutputsBuilder::default(); + if let Some(appid) = value.extensions.appid { + ext_builder.appid(appid); + } + if let Some(ext) = &value.extensions.hmac_get_secret { + let mut hmac_out = HmacGetSecretOutputBuilder::default(); + hmac_out.output1(URL_SAFE.encode(ext.output1.clone())); + if let Some(out2) = &ext.output2 { + hmac_out.output2(URL_SAFE.encode(out2)); + } + ext_builder.hmac_get_secret(hmac_out.build()?); + } + req.extensions(ext_builder.build()?); + let mut rsp_builder = AuthenticatorAssertionResponseRawBuilder::default(); + rsp_builder.authenticator_data(URL_SAFE.encode(value.response.authenticator_data)); + rsp_builder.client_data_json(URL_SAFE.encode(value.response.client_data_json)); + rsp_builder.signature(URL_SAFE.encode(value.response.signature)); + if let Some(uh) = &value.response.user_handle { + rsp_builder.user_handle(URL_SAFE.encode(uh)); + } + req.response(rsp_builder.build()?); + Ok(req) + } +} + +/// Get [`RestEndpoint`] for finishing the passkey authentication. +pub async fn get_finish_auth_ep<'a, A: AuthHelper>( + config: &config::CloudConfig, + passkey_auth: webauthn_authenticator_rs::prelude::PublicKeyCredential, + _auth_helper: &mut A, +) -> Result, PasskeyError> { + if let Some(auth) = &config.auth { + let mut ep_builder: AuthFinishRequestBuilder = passkey_auth.try_into()?; + if let Some(val) = &auth.user_id { + ep_builder.user_id(val.clone()); + } else { + return Err(PasskeyError::MissingAuthData); + } + return Ok(ep_builder.build()?); + } + + Err(PasskeyError::MissingAuthData) +} diff --git a/openstack_sdk/src/auth/v4passkey/start.rs b/openstack_sdk/src/auth/v4passkey/start.rs new file mode 100644 index 000000000..90cd3e5dd --- /dev/null +++ b/openstack_sdk/src/auth/v4passkey/start.rs @@ -0,0 +1,363 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//! Initialize passkey (security device) based login. +//! + +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; + +use crate::api::rest_endpoint_prelude::*; +use crate::api::RestEndpoint; +use crate::auth::auth_helper::AuthHelper; +use crate::config; +use crate::types::{ApiVersion, ServiceType}; + +use super::PasskeyError; + +/// Endpoint for initializing passkey authorization. +#[derive(Builder, Debug, Clone, Serialize)] +#[builder(setter(strip_option))] +pub struct Passkey<'a> { + /// User ID. + #[builder(setter(into))] + user_id: Cow<'a, str>, +} + +/// Endpoint for initializing passkey authorization. +#[derive(Builder, Debug, Clone)] +#[builder(setter(strip_option))] +pub struct AuthStartRequest<'a> { + /// passkey auth request. + #[builder(setter(into))] + passkey: Passkey<'a>, +} + +impl<'a> AuthStartRequest<'a> { + /// Create a builder for the endpoint. + pub fn builder() -> AuthStartRequestBuilder<'a> { + AuthStartRequestBuilder::default() + } +} + +impl RestEndpoint for AuthStartRequest<'_> { + fn method(&self) -> http::Method { + http::Method::POST + } + + fn endpoint(&self) -> Cow<'static, str> { + "auth/passkey/start".to_string().into() + } + + fn body(&self) -> Result)>, BodyError> { + let mut params = JsonBodyParams::default(); + + params.push("passkey", serde_json::to_value(&self.passkey)?); + + params.into_body() + } + + fn service_type(&self) -> ServiceType { + ServiceType::Identity + } + + /// Returns required API version + fn api_version(&self) -> Option { + Some(ApiVersion::new(4, 0)) + } +} + +/// Get [`RestEndpoint`] for initializing the passkey authentication +pub async fn get_init_auth_ep( + config: &config::CloudConfig, + _auth_helper: &mut A, +) -> Result { + if let Some(auth) = &config.auth { + let mut ep = AuthStartRequest::builder(); + let mut passkey = PasskeyBuilder::default(); + + if let Some(val) = &auth.user_id { + passkey.user_id(val.clone()); + } else { + return Err(PasskeyError::MissingAuthData); + } + ep.passkey(passkey.build()?); + return Ok(ep.build()?); + } + + Err(PasskeyError::MissingAuthData) +} + +/// Passkey Authorization challenge. +/// +/// A JSON serializable challenge which is issued to the user’s webbrowser for handling. This is +/// meant to be opaque, that is, you should not need to inspect or alter the content of the struct +/// - you should serialise it and transmit it to the client only. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct PasskeyAuthenticationStartResponse { + /// The options. + pub public_key: PublicKeyCredentialRequestOptions, + /// The mediation requested. + pub mediation: Option, +} + +/// The requested options for the authentication. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct PublicKeyCredentialRequestOptions { + /// The set of credentials that are allowed to sign this challenge. + pub allow_credentials: Vec, + /// The challenge that should be signed by the authenticator. + pub challenge: String, + /// extensions. + pub extensions: Option, + /// Hints defining which types credentials may be used in this operation. + pub hints: Option>, + /// The relying party ID. + pub rp_id: String, + /// The timeout for the authenticator in case of no interaction. + pub timeout: Option, + /// The verification policy the browser will request. + pub user_verification: UserVerificationPolicy, +} + +/// Request in residentkey workflows that conditional mediation should be used in the UI, or not. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum Mediation { + /// Discovered credentials are presented to the user in a dialog. Conditional UI is used. See + /// https://github.com/w3c/webauthn/wiki/Explainer:-WebAuthn-Conditional-UI + /// https://w3c.github.io/webappsec-credential-management/#enumdef-credentialmediationrequirement + Conditional, +} + +/// A descriptor of a credential that can be used. +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct AllowCredentials { + /// The id of the credential. + pub id: String, + /// https://www.w3.org/TR/webauthn/#transport may be usb, nfc, ble, internal + pub transports: Option>, + /// The type of credential. + pub type_: String, +} + +/// https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum AuthenticatorTransport { + /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-ble + Ble, + /// Hybrid transport, formerly caBLE. Part of the level 3 draft specification. https://w3c.github.io/webauthn/#dom-authenticatortransport-hybrid + Hybrid, + /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-internal + Internal, + /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-nfc + Nfc, + /// Test transport; used for Windows 10. + Test, + /// An unknown transport was provided - it will be ignored. + Unknown, + /// https://www.w3.org/TR/webauthn/#dom-authenticatortransport-usb + Usb, +} +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum UserVerificationPolicy { + /// Require user verification bit to be set, and fail the registration or authentication if + /// false. If the authenticator is not able to perform verification, it will not be usable with + /// this policy. + /// + /// This policy is the default as it is the only secure and consistent user verification + /// option. + Required, + /// Prefer UV if possible, but ignore if not present. In other webauthn deployments this is + /// bypassable as it implies the library will not check UV is set correctly for this + /// credential. Webauthn-RS is not vulnerable to this as we check the UV state always based on + /// it’s presence at registration. + /// + /// However, in some cases use of this policy can lead to some credentials failing to verify + /// correctly due to browser peripheral exchange bypasses. + Preferred, + /// Discourage - but do not prevent - user verification from being supplied. Many CTAP devices + /// will attempt UV during registration but not authentication leading to user confusion. + DiscouragedDoNotUse, +} + +/// A hint as to the class of device that is expected to fufil this operation. +/// +/// https://www.w3.org/TR/webauthn-3/#enumdef-publickeycredentialhints +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum PublicKeyCredentialHint { + /// The credential is a platform authenticator. + ClientDevice, + /// The credential will come from an external device. + Hybrid, + /// The credential is a removable security key. + SecurityKey, +} + +/// Extension option inputs for PublicKeyCredentialRequestOptions +/// +/// Implements [AuthenticatorExtensionsClientInputs] from the spec +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct RequestAuthenticationExtensions { + /// The appid extension options. + pub appid: Option, + /// ⚠️ - Browsers do not support this! + /// https://bugs.chromium.org/p/chromium/issues/detail?id=1023225 Hmac get secret. + pub hmac_get_secret: Option, + /// ⚠️ - Browsers do not support this! Uvm. + pub uvm: Option, +} + +/// The inputs to the hmac secret if it was created during registration. +/// +/// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-hmac-secret-extension +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct HmacGetSecretInput { + /// Retrieve a symmetric secrets from the authenticator with this input. + pub output1: String, + /// Rotate the secret in the same operation. + pub output2: Option, +} + +impl TryFrom for webauthn_rs_proto::extensions::HmacGetSecretInput { + type Error = PasskeyError; + fn try_from(val: HmacGetSecretInput) -> Result { + Ok(Self { + output1: URL_SAFE.decode(val.output1)?.into(), + output2: val + .output2 + .map(|s2| URL_SAFE.decode(s2)) + .transpose()? + .map(Into::into), + }) + } +} + +impl TryFrom + for webauthn_rs_proto::extensions::RequestAuthenticationExtensions +{ + type Error = PasskeyError; + fn try_from(val: RequestAuthenticationExtensions) -> Result { + Ok(Self { + appid: val.appid, + hmac_get_secret: val.hmac_get_secret.map(TryInto::try_into).transpose()?, + uvm: val.uvm, + }) + } +} + +impl From for webauthn_rs_proto::options::AuthenticatorTransport { + fn from(val: AuthenticatorTransport) -> Self { + match val { + AuthenticatorTransport::Ble => webauthn_rs_proto::options::AuthenticatorTransport::Ble, + AuthenticatorTransport::Hybrid => { + webauthn_rs_proto::options::AuthenticatorTransport::Hybrid + } + AuthenticatorTransport::Internal => { + webauthn_rs_proto::options::AuthenticatorTransport::Internal + } + AuthenticatorTransport::Nfc => webauthn_rs_proto::options::AuthenticatorTransport::Nfc, + AuthenticatorTransport::Test => { + webauthn_rs_proto::options::AuthenticatorTransport::Test + } + AuthenticatorTransport::Unknown => { + webauthn_rs_proto::options::AuthenticatorTransport::Unknown + } + AuthenticatorTransport::Usb => webauthn_rs_proto::options::AuthenticatorTransport::Usb, + } + } +} + +impl From for webauthn_rs_proto::options::UserVerificationPolicy { + fn from(val: UserVerificationPolicy) -> Self { + match val { + UserVerificationPolicy::Required => { + webauthn_rs_proto::options::UserVerificationPolicy::Required + } + UserVerificationPolicy::Preferred => { + webauthn_rs_proto::options::UserVerificationPolicy::Preferred + } + UserVerificationPolicy::DiscouragedDoNotUse => { + webauthn_rs_proto::options::UserVerificationPolicy::Discouraged_DO_NOT_USE + } + } + } +} + +impl From for webauthn_rs_proto::options::PublicKeyCredentialHints { + fn from(val: PublicKeyCredentialHint) -> Self { + match val { + PublicKeyCredentialHint::ClientDevice => { + webauthn_rs_proto::options::PublicKeyCredentialHints::ClientDevice + } + PublicKeyCredentialHint::Hybrid => { + webauthn_rs_proto::options::PublicKeyCredentialHints::Hybrid + } + PublicKeyCredentialHint::SecurityKey => { + webauthn_rs_proto::options::PublicKeyCredentialHints::SecurityKey + } + } + } +} + +impl TryFrom for webauthn_rs_proto::options::AllowCredentials { + type Error = PasskeyError; + fn try_from(val: AllowCredentials) -> Result { + Ok(Self { + id: URL_SAFE.decode(val.id)?.into(), + transports: val + .transports + .map(|tr| tr.into_iter().map(Into::into).collect::>()), + type_: val.type_, + }) + } +} + +impl TryFrom + for webauthn_rs_proto::auth::PublicKeyCredentialRequestOptions +{ + type Error = PasskeyError; + fn try_from(val: PublicKeyCredentialRequestOptions) -> Result { + Ok(Self { + allow_credentials: val + .allow_credentials + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + challenge: URL_SAFE.decode(val.challenge)?.into(), + extensions: val.extensions.map(TryInto::try_into).transpose()?, + hints: val + .hints + .map(|hints| hints.into_iter().map(Into::into).collect::>()), + rp_id: val.rp_id, + timeout: val.timeout, + user_verification: val.user_verification.into(), + }) + } +} + +impl TryFrom + for webauthn_authenticator_rs::prelude::RequestChallengeResponse +{ + type Error = PasskeyError; + fn try_from(val: PasskeyAuthenticationStartResponse) -> Result { + Ok(Self { + public_key: val.public_key.try_into()?, + mediation: val.mediation.map(|med| match med { + Mediation::Conditional => webauthn_rs_proto::auth::Mediation::Conditional, + }), + }) + } +} diff --git a/openstack_sdk/src/error.rs b/openstack_sdk/src/error.rs index 4dee8aa85..17b0a1f56 100644 --- a/openstack_sdk/src/error.rs +++ b/openstack_sdk/src/error.rs @@ -20,6 +20,8 @@ use thiserror::Error; use crate::api; #[cfg(feature = "keystone_ng")] use crate::auth::v4federation::FederationError; +#[cfg(feature = "passkey")] +use crate::auth::v4passkey::PasskeyError; use crate::auth::{ authtoken::AuthTokenError, authtoken_scope::AuthTokenScopeError, v3oidcaccesstoken::OidcAccessTokenError, v3websso::WebSsoError, AuthError, @@ -241,4 +243,13 @@ impl From for OpenStackError { } } +#[cfg(feature = "passkey")] +impl From for OpenStackError { + fn from(source: PasskeyError) -> Self { + Self::AuthError { + source: source.into(), + } + } +} + pub type OpenStackResult = Result; diff --git a/openstack_sdk/src/openstack_async.rs b/openstack_sdk/src/openstack_async.rs index 7594dc092..071fd33c1 100644 --- a/openstack_sdk/src/openstack_async.rs +++ b/openstack_sdk/src/openstack_async.rs @@ -494,6 +494,52 @@ impl AsyncOpenStack { return Err(OpenStackError::NoAuth); } } + + #[cfg(feature = "passkey")] + AuthType::V4Passkey => { + let auth_ep = + auth::v4passkey::get_init_auth_ep(&self.config, auth_helper).await?; + let req: auth::v4passkey::PasskeyAuthenticationStartResponse = + auth_ep.query_async(self).await?; + use webauthn_authenticator_rs::prelude::Url; + use webauthn_authenticator_rs::WebauthnAuthenticator; + let mut auth = WebauthnAuthenticator::new( + webauthn_authenticator_rs::mozilla::MozillaAuthenticator::new(), + ); + let passkey_auth = auth + .do_authentication( + Url::parse("http://localhost:8080").unwrap(), + req.try_into()?, + ) + .map_err(auth::v4passkey::PasskeyError::from)?; + let finish_ep = auth::v4passkey::get_finish_auth_ep( + &self.config, + passkey_auth, + auth_helper, + ) + .await?; + rsp = finish_ep.raw_query_async(self).await?; + + let token = rsp + .headers() + .get("x-subject-token") + .ok_or(AuthError::AuthTokenNotInResponse)? + .to_str() + .expect("x-subject-token is a string"); + + // Set retrieved token as current auth + let token_info: AuthResponse = serde_json::from_slice(rsp.body())?; + let token_auth = authtoken::AuthToken { + token: token.to_string(), + auth_info: Some(token_info), + }; + self.set_auth(Auth::AuthToken(Box::new(token_auth.clone())), false); + + // And now time to rescope the token + let auth_ep = + authtoken::build_reauth_request(&token_auth, &requested_scope)?; + rsp = auth_ep.raw_query_async(self).await?; + } } }; diff --git a/openstack_types/src/identity/v4/user/passkey/response.rs b/openstack_types/src/identity/v4/user/passkey/response.rs index 4c30bf092..b66c73c99 100644 --- a/openstack_types/src/identity/v4/user/passkey/response.rs +++ b/openstack_types/src/identity/v4/user/passkey/response.rs @@ -16,4 +16,5 @@ // `openstack-codegenerator`. //! `response` REST operations of identity +pub mod register_finish; pub mod register_start; diff --git a/openstack_types/src/identity/v4/user/passkey/response/register_finish.rs b/openstack_types/src/identity/v4/user/passkey/response/register_finish.rs new file mode 100644 index 000000000..fa79d9d23 --- /dev/null +++ b/openstack_types/src/identity/v4/user/passkey/response/register_finish.rs @@ -0,0 +1,33 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 +// +// WARNING: This file is automatically generated from OpenAPI schema using +// `openstack-codegenerator`. +//! Response type for the POST `users/{user_id}/passkeys/register_finish` operation + +use serde::{Deserialize, Serialize}; +use structable::{StructTable, StructTableOptions}; + +/// RegisterFinish response representation +#[derive(Clone, Deserialize, Serialize, StructTable)] +pub struct RegisterFinishResponse { + /// Credential ID. + #[structable()] + pub credential_id: String, + + /// Credential description. + #[serde(default)] + #[structable(optional)] + pub description: Option, +} diff --git a/openstack_types/src/identity/v4/user/passkey/response/register_start.rs b/openstack_types/src/identity/v4/user/passkey/response/register_start.rs index 2d47a4d9f..2bce06ac9 100644 --- a/openstack_types/src/identity/v4/user/passkey/response/register_start.rs +++ b/openstack_types/src/identity/v4/user/passkey/response/register_start.rs @@ -71,7 +71,7 @@ pub struct RegisterStartResponse { #[structable(optional)] pub timeout: Option, - /// User information + /// User Entity. #[structable(serialize)] pub user: User, } @@ -201,6 +201,10 @@ impl std::str::FromStr for ResidentKey { #[derive(Debug, Deserialize, Clone, Serialize)] pub enum UserVerification { + // Discourageddonotuse + #[serde(rename = "DiscouragedDoNotUse")] + Discourageddonotuse, + // Preferred #[serde(rename = "Preferred")] Preferred, @@ -214,6 +218,7 @@ impl std::str::FromStr for UserVerification { type Err = (); fn from_str(input: &str) -> Result { match input { + "DiscouragedDoNotUse" => Ok(Self::Discourageddonotuse), "Preferred" => Ok(Self::Preferred), "Required" => Ok(Self::Required), _ => Err(()), @@ -391,24 +396,11 @@ pub struct Rp { pub name: String, } -/// Domain information -/// `Domain` type -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Domain { - #[serde(default)] - pub id: Option, - #[serde(default)] - pub name: Option, -} - -/// User information +/// User Entity. /// `User` type #[derive(Clone, Debug, Deserialize, Serialize)] pub struct User { - pub domain: Domain, + pub display_name: String, pub id: String, - #[serde(default)] - pub name: Option, - #[serde(default)] - pub password_expires_at: Option, + pub name: String, }