diff --git a/Cargo.lock b/Cargo.lock index 53ef6489..90d9b4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,19 +4,13 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -29,7 +23,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "version_check", ] @@ -186,7 +180,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -198,14 +192,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "brotli", "flate2", @@ -236,7 +230,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -247,7 +241,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -328,24 +322,30 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -383,7 +383,7 @@ checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f" dependencies = [ "base64 0.22.1", "blowfish", - "getrandom 0.3.2", + "getrandom 0.3.3", "subtle", "zeroize", ] @@ -462,14 +462,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -478,9 +478,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -534,9 +534,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.18" +version = "1.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" dependencies = [ "jobserver", "libc", @@ -638,7 +638,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -649,9 +649,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "color-eyre" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" +checksum = "e6e1761c0e16f8883bbbb8ce5990867f4f06bf11a0253da6495a04ce4b6ef0ec" dependencies = [ "backtrace", "color-spantrace", @@ -664,9 +664,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +checksum = "2ddd8d5bfda1e11a501d0a7303f3bfed9aa632ebdb859be40d0fd70478ed70d5" dependencies = [ "once_cell", "owo-colors", @@ -746,7 +746,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -777,9 +777,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -877,6 +877,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -887,6 +899,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "darling" version = "0.20.11" @@ -908,7 +947,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -919,20 +958,20 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "data-encoding" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -971,7 +1010,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -992,7 +1031,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1002,7 +1041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1025,7 +1064,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1055,6 +1094,44 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -1064,6 +1141,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1135,11 +1233,27 @@ checksum = "c66b725fe9483b9ee72ccaec072b15eb8ad95a3ae63a8c798d5748883b72fd33" dependencies = [ "base64 0.22.1", "byteorder", - "getrandom 0.2.15", + "getrandom 0.2.16", "openssl", "zeroize", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.1.1" @@ -1147,7 +1261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", - "miniz_oxide 0.8.8", + "miniz_oxide", ] [[package]] @@ -1303,36 +1417,52 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] [[package]] name = "half" @@ -1367,9 +1497,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", @@ -1382,7 +1512,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -1399,9 +1529,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hex" @@ -1499,6 +1629,25 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 0.26.11", ] [[package]] @@ -1508,13 +1657,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", + "libc", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1543,21 +1696,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1566,31 +1720,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1598,67 +1732,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1678,9 +1799,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1692,6 +1813,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1699,7 +1831,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "serde", ] @@ -1711,7 +1843,7 @@ checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1723,6 +1855,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is-terminal" version = "0.4.16" @@ -1761,7 +1899,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -1797,15 +1935,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libsqlite3-sys" @@ -1819,15 +1957,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" @@ -1839,18 +1977,18 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "matchers" version = "0.1.0" @@ -1904,15 +2042,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.8" @@ -1956,7 +2085,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -1968,7 +2097,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2013,7 +2142,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2054,11 +2183,31 @@ dependencies = [ "libm", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.21.7", + "chrono", + "getrandom 0.2.16", + "http", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "object" -version = "0.32.2" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2084,6 +2233,37 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openidconnect" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd50d4a5e7730e754f94d977efe61f611aadd3131f6a2b464f6e3a4167e8ef7" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http", + "itertools", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + [[package]] name = "openssl" version = "0.10.72" @@ -2107,14 +2287,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -2143,6 +2323,7 @@ dependencies = [ "http-body-util", "mockall", "mockall_double", + "openidconnect", "regex", "rmp", "sea-orm", @@ -2157,6 +2338,7 @@ dependencies = [ "tracing", "tracing-subscriber", "tracing-test", + "url", "utoipa", "utoipa-axum", "utoipa-swagger-ui", @@ -2164,6 +2346,15 @@ dependencies = [ "webauthn-rs", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "4.6.0" @@ -2204,7 +2395,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2215,9 +2406,33 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "owo-colors" -version = "3.5.0" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] [[package]] name = "parking" @@ -2306,7 +2521,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2396,6 +2611,15 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2437,6 +2661,15 @@ dependencies = [ "termtree", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -2465,14 +2698,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -2485,7 +2718,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "version_check", "yansi", ] @@ -2510,6 +2743,61 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quinn" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2538,8 +2826,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2549,7 +2847,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2558,7 +2866,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] @@ -2583,9 +2900,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags", ] @@ -2643,6 +2960,73 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.11", + "windows-registry", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -2708,7 +3092,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -2717,9 +3101,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.6.0" +version = "8.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +checksum = "60e425e204264b144d4c929d126d0de524b40a961686414bab5040f7465c71be" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -2728,22 +3112,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.100", + "syn 2.0.101", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.6.0" +version = "8.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" dependencies = [ "sha2", "walkdir", @@ -2770,7 +3154,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -2782,6 +3166,21 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -2793,9 +3192,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ "bitflags", "errno", @@ -2804,6 +3203,50 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -2841,14 +3284,14 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] name = "sea-orm" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e61af841881c137d4bc8e0d8411cee9168548b404f9e4788e8af7e8f94bd4e" +checksum = "f7cf58b28bcf1e053539c38afbb60d963ac8e1db87f6109db7b0eff4cbeaefb3" dependencies = [ "async-stream", "async-trait", @@ -2875,28 +3318,28 @@ dependencies = [ [[package]] name = "sea-orm-macros" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b86e3e77b548e6c6c1f612a1ca024d557dffdb81b838bf482ad3222140c77b" +checksum = "cac37512fde1f5b9ef71ec773cfabb90ad3b68c27e53131ff38763c247fcbb2d" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "sea-bae", - "syn 2.0.100", + "syn 2.0.101", "unicode-ident", ] [[package]] name = "sea-query" -version = "0.32.3" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a24d8b9fcd2674a6c878a3d871f4f1380c6c43cc3718728ac96864d888458e" +checksum = "5506de3a33d9ee4ee161c5847acb87fe4f82ced6649afc9eabeb8df6f40ba94a" dependencies = [ "bigdecimal", "chrono", "inherent", - "ordered-float", + "ordered-float 4.6.0", "rust_decimal", "serde_json", "time", @@ -2925,6 +3368,26 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -2934,6 +3397,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.17" @@ -2961,7 +3434,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -2986,6 +3459,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -3007,6 +3489,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3020,9 +3532,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3046,9 +3558,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -3060,7 +3572,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3153,9 +3665,9 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.2", + "hashbrown 0.15.3", "hashlink", - "indexmap", + "indexmap 2.9.0", "log", "memchr", "once_cell", @@ -3184,7 +3696,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3207,7 +3719,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.100", + "syn 2.0.101", "tempfile", "tokio", "url", @@ -3244,7 +3756,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "rust_decimal", "serde", @@ -3288,7 +3800,7 @@ dependencies = [ "memchr", "num-bigint", "once_cell", - "rand", + "rand 0.8.5", "rust_decimal", "serde", "serde_json", @@ -3384,9 +3896,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -3398,16 +3910,19 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3423,7 +3938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3461,7 +3976,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3472,7 +3987,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3527,9 +4042,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -3562,9 +4077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -3585,7 +4100,17 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", ] [[package]] @@ -3601,9 +4126,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -3614,9 +4139,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ "serde", "serde_spanned", @@ -3626,20 +4151,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap", + "indexmap 2.9.0", "serde", "serde_spanned", "toml_datetime", @@ -3716,7 +4241,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3786,7 +4311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3795,6 +4320,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" @@ -3846,6 +4377,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[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.4" @@ -3858,12 +4395,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -3882,7 +4413,7 @@ version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap", + "indexmap 2.9.0", "serde", "serde_json", "utoipa-gen", @@ -3910,7 +4441,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -3943,7 +4474,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "serde", ] @@ -3975,6 +4506,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4018,10 +4558,23 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -4040,7 +4593,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4064,6 +4617,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webauthn-attestation-ca" version = "0.5.1" @@ -4104,8 +4667,8 @@ dependencies = [ "hex", "nom", "openssl", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_cbor_2", "serde_json", @@ -4131,6 +4694,24 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.6.0" @@ -4182,7 +4763,7 @@ dependencies = [ "windows-interface", "windows-link", "windows-result", - "windows-strings", + "windows-strings 0.4.0", ] [[package]] @@ -4193,7 +4774,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4204,7 +4785,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4213,6 +4794,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + [[package]] name = "windows-result" version = "0.3.2" @@ -4222,6 +4814,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.4.0" @@ -4282,13 +4883,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4301,6 +4918,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4313,6 +4936,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4325,12 +4954,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4343,6 +4984,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4355,6 +5002,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4367,6 +5020,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4379,11 +5038,17 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -4397,17 +5062,11 @@ dependencies = [ "bitflags", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" @@ -4454,9 +5113,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4466,34 +5125,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4513,7 +5172,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", "synstructure", ] @@ -4534,14 +5193,25 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", ] [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4550,13 +5220,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.100", + "syn 2.0.101", ] [[package]] @@ -4569,22 +5239,20 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "indexmap", + "indexmap 2.9.0", "memchr", "zopfli", ] [[package]] name = "zopfli" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index 61c44f3a..d88ce2f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,45 +17,47 @@ name = "fernet_token" harness = false [dependencies] -async-trait = { version = "^0.1" } -axum = { version = "^0.8", features = ["macros"] } -base64 = { version = "^0.22" } +async-trait = { version = "0.1" } +axum = { version = "0.8", features = ["macros"] } +base64 = { version = "0.22" } bcrypt = { version = "0.17", features = ["alloc"] } -bytes = { version = "^1.10" } -chrono = { version = "^0.4" } -clap = { version = "^4.5", features = ["derive"] } -color-eyre = { version = "^0.6" } -config = { version = "^0.15", features = ["ini"] } -derive_builder = { version = "^0.20" } -dyn-clone = { version = "^1.0" } -eyre = { version = "^0.6" } -fernet = { version = "^0.2" } -mockall_double = { version = "^0.3" } -regex = { version = "^1.11"} -rmp = { version = "^0.8" } -sea-orm = { version = "^1.1", features = ["sqlx-mysql", "sqlx-postgres", "runtime-tokio"] } -serde = { version = "^1.0" } -serde_bytes = "0.11.17" -serde_json = { version = "^1.0" } -thiserror = { version = "^2.0" } -tokio = { version = "^1.44", features = ["fs", "macros", "signal", "rt-multi-thread"] } -tower = { version = "^0.5" } -tower-http = { version = "^0.6", features = ["compression-full", "request-id", "sensitive-headers", "trace", "util"] } -tracing = { version = "^0.1" } -tracing-subscriber = { version = "^0.3" } -utoipa = { version = "^5.3", features = ["axum_extras", "chrono"] } -utoipa-axum = { version = "^0.2" } -utoipa-swagger-ui = { version = "^9.0", features = ["axum", "vendored"], default-features = false } -uuid = { version = "^1.16", features = ["v4"] } -webauthn-rs = { version = "^0.5", features = ["danger-allow-state-serialisation"] } +bytes = { version = "1.10" } +chrono = { version = "0.4" } +clap = { version = "4.5", features = ["derive"] } +color-eyre = { version = "0.6" } +config = { version = "0.15", features = ["ini"] } +derive_builder = { version = "0.20" } +dyn-clone = { version = "1.0" } +eyre = { version = "0.6" } +fernet = { version = "0.2" } +mockall_double = { version = "0.3" } +openidconnect = { version = "4.0" } +regex = { version = "1.11"} +rmp = { version = "0.8" } +sea-orm = { version = "1.1", features = ["sqlx-mysql", "sqlx-postgres", "runtime-tokio"] } +serde = { version = "1.0" } +serde_bytes = { version = "0.11" } +serde_json = { version = "1.0" } +thiserror = { version = "2.0" } +tokio = { version = "1.45", features = ["fs", "macros", "signal", "rt-multi-thread"] } +tower = { version = "0.5" } +tower-http = { version = "0.6", features = ["compression-full", "request-id", "sensitive-headers", "trace", "util"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3" } +url = { version = "2.5", features = ["serde"] } +utoipa = { version = "5.3", features = ["axum_extras", "chrono"] } +utoipa-axum = { version = "0.2" } +utoipa-swagger-ui = { version = "9.0", features = ["axum", "vendored"], default-features = false } +uuid = { version = "1.16", features = ["v4"] } +webauthn-rs = { version = "0.5", features = ["danger-allow-state-serialisation"] } [dev-dependencies] -criterion = { version = "^0.5", features = ["async_tokio"] } -http-body-util = "^0.1" -mockall = { version = "^0.13" } -sea-orm = { version = "^1.1", features = ["mock"]} -tempfile = { version = "^3.19" } -tracing-test = { version = "^0.2" } +criterion = { version = "0.5", features = ["async_tokio"] } +http-body-util = "0.1" +mockall = { version = "0.13" } +sea-orm = { version = "1.1", features = ["mock"]} +tempfile = { version = "3.19" } +tracing-test = { version = "0.2" } [profile.release] opt-level = 3 diff --git a/deny.toml b/deny.toml index 705f1b4d..dee31787 100644 --- a/deny.toml +++ b/deny.toml @@ -96,6 +96,7 @@ allow = [ "BSD-3-Clause", "BSL-1.0", "CC0-1.0", + "CDLA-Permissive-2.0", "ISC", "LicenseRef-ring", "MIT", diff --git a/migration/Cargo.lock b/migration/Cargo.lock index d88ee84d..6b9ab590 100644 --- a/migration/Cargo.lock +++ b/migration/Cargo.lock @@ -471,6 +471,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -925,6 +931,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -935,6 +953,33 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "darling" version = "0.20.10" @@ -1097,6 +1142,44 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -1106,6 +1189,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1198,6 +1302,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.1.0" @@ -1368,6 +1488,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1377,8 +1498,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1388,8 +1511,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.13.3+wasi-0.2.2", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1417,6 +1542,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "half" version = "1.8.3" @@ -1572,6 +1708,25 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", ] [[package]] @@ -1581,13 +1736,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1764,6 +1922,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.8.0" @@ -1795,12 +1964,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1958,6 +2142,8 @@ dependencies = [ "async-std", "openstack_keystone", "sea-orm-migration", + "serde_json", + "uuid", ] [[package]] @@ -2065,7 +2251,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2106,6 +2292,26 @@ dependencies = [ "libm", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.15", + "http", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "object" version = "0.32.2" @@ -2130,6 +2336,37 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "openidconnect" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd50d4a5e7730e754f94d977efe61f611aadd3131f6a2b464f6e3a4167e8ef7" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http", + "itertools", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + [[package]] name = "openssl" version = "0.10.72" @@ -2186,6 +2423,7 @@ dependencies = [ "eyre", "fernet", "mockall_double", + "openidconnect", "regex", "rmp", "sea-orm", @@ -2198,6 +2436,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "url", "utoipa", "utoipa-axum", "utoipa-swagger-ui", @@ -2205,6 +2444,15 @@ dependencies = [ "webauthn-rs", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "3.9.2" @@ -2260,6 +2508,30 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -2450,6 +2722,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.3.0" @@ -2523,6 +2804,60 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quinn" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" +dependencies = [ + "bytes", + "getrandom 0.3.1", + "rand 0.9.1", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.40" @@ -2545,8 +2880,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2556,7 +2901,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2568,6 +2923,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.5.10" @@ -2630,6 +2994,73 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.45" @@ -2695,7 +3126,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -2757,7 +3188,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand", + "rand 0.8.5", "rkyv", "serde", "serde_json", @@ -2769,6 +3200,21 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -2804,6 +3250,49 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.20" @@ -2929,7 +3418,7 @@ dependencies = [ "bigdecimal", "chrono", "inherent", - "ordered-float", + "ordered-float 3.9.2", "rust_decimal", "sea-query-derive", "serde_json", @@ -2996,6 +3485,26 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + [[package]] name = "serde" version = "1.0.219" @@ -3005,6 +3514,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float 2.10.1", + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.17" @@ -3057,6 +3576,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -3078,6 +3606,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.8.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3131,7 +3689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -3225,7 +3783,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.2", "hashlink", - "indexmap", + "indexmap 2.8.0", "log", "memchr", "once_cell", @@ -3314,7 +3872,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "rust_decimal", "serde", @@ -3358,7 +3916,7 @@ dependencies = [ "memchr", "num-bigint", "once_cell", - "rand", + "rand 0.8.5", "rust_decimal", "serde", "serde_json", @@ -3467,6 +4025,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3615,9 +4176,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" dependencies = [ "backtrace", "bytes", @@ -3641,6 +4202,16 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3692,7 +4263,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap", + "indexmap 2.8.0", "serde", "serde_spanned", "toml_datetime", @@ -3827,6 +4398,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" @@ -3878,6 +4455,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[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.4" @@ -3914,7 +4497,7 @@ version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" dependencies = [ - "indexmap", + "indexmap 2.8.0", "serde", "serde_json", "utoipa-gen", @@ -4013,6 +4596,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4115,6 +4707,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webauthn-attestation-ca" version = "0.5.1" @@ -4155,8 +4757,8 @@ dependencies = [ "hex", "nom", "openssl", - "rand", - "rand_chacha", + "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_cbor_2", "serde_json", @@ -4182,6 +4784,15 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-roots" +version = "0.26.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37493cadf42a2a939ed404698ded7fb378bf301b5011f973361779a3a74f8c93" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "whoami" version = "1.5.2" @@ -4238,6 +4849,35 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-result" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4289,13 +4929,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4308,6 +4964,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4320,6 +4982,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4332,12 +5000,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4350,6 +5030,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4362,6 +5048,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4374,6 +5066,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4386,6 +5084,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.4" @@ -4577,7 +5281,7 @@ dependencies = [ "crossbeam-utils", "displaydoc", "flate2", - "indexmap", + "indexmap 2.8.0", "memchr", "thiserror 2.0.12", "zopfli", diff --git a/migration/Cargo.toml b/migration/Cargo.toml index cb8b075a..f45688fe 100644 --- a/migration/Cargo.toml +++ b/migration/Cargo.toml @@ -11,6 +11,8 @@ path = "src/lib.rs" [dependencies] async-std = { version = "1", features = ["attributes", "tokio1"] } openstack_keystone = {path = "../"} +serde_json = "1.0.140" +uuid = "1.16.0" [dependencies.sea-orm-migration] version = "1.1.0" diff --git a/migration/src/m20250414_000001_idp.rs b/migration/src/m20250414_000001_idp.rs index 35b9550d..69458181 100644 --- a/migration/src/m20250414_000001_idp.rs +++ b/migration/src/m20250414_000001_idp.rs @@ -1,6 +1,7 @@ use openstack_keystone::db::entity::prelude::Project; -use openstack_keystone::db::entity::project; +use openstack_keystone::db::entity::{federated_user, project}; use sea_orm_migration::{prelude::*, schema::*}; +use sea_query::*; #[derive(DeriveMigrationName)] pub struct Migration; @@ -39,9 +40,13 @@ impl MigrationTrait for Migration { .col(text_null(FederatedIdentityProvider::JwtValidationPubkeys)) .col(string_len_null(FederatedIdentityProvider::BoundIssuer, 255)) .col(json_null(FederatedIdentityProvider::ProviderConfig)) + .col(string_len_null( + FederatedIdentityProvider::DefaultMappingName, + 255, + )) .foreign_key( ForeignKey::create() - .name("fk-user-passkey-credential") + .name("fk-idp-project") .from( FederatedIdentityProvider::Table, FederatedIdentityProvider::DomainId, @@ -60,10 +65,116 @@ impl MigrationTrait for Migration { ) .await?; + manager + .create_table( + Table::create() + .table(FederatedMapping::Table) + .if_not_exists() + .col(string_len(FederatedMapping::Id, 64).primary_key()) + .col(string_len(FederatedMapping::Name, 255)) + .col(string_len(FederatedMapping::IdpId, 64)) + .col(string_len_null(FederatedMapping::DomainId, 64)) + .col(string_len_null(FederatedMapping::AllowedRedirectUris, 1024)) + .col(string_len(FederatedMapping::UserIdClaim, 64)) + .col(string_len(FederatedMapping::UserNameClaim, 64)) + .col(string_len_null(FederatedMapping::DomainIdClaim, 64)) + //.col(string_len_null(FederatedMapping::UserClaimJsonPointer, 128)) + .col(string_len_null(FederatedMapping::GroupsClaim, 64)) + .col(string_len_null(FederatedMapping::BoundAudiences, 1024)) + .col(string_len_null(FederatedMapping::BoundSubject, 128)) + .col(json_null(FederatedMapping::BoundClaims)) + .col(string_len_null(FederatedMapping::OidcScopes, 128)) + //.col(json_null(FederatedMapping::ClaimMappings)) + .col(string_len_null(FederatedMapping::TokenUserId, 64)) + .col(string_len_null(FederatedMapping::TokenRoleIds, 128)) + .col(string_len_null(FederatedMapping::TokenProjectId, 128)) + .foreign_key( + ForeignKey::create() + .name("fk-idp-mapping-idp") + .from(FederatedMapping::Table, FederatedMapping::IdpId) + .to( + FederatedIdentityProvider::Table, + FederatedIdentityProvider::Id, + ) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-idp-mapping-project") + .from(FederatedMapping::Table, FederatedMapping::DomainId) + .to(Project, project::Column::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx-idp-mapping-domain") + .table(FederatedMapping::Table) + .col(FederatedMapping::DomainId) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(FederatedAuthState::Table) + .if_not_exists() + .col(string_len(FederatedAuthState::IdpId, 64)) + .col(string_len(FederatedAuthState::MappingId, 64)) + .col(string_len(FederatedAuthState::State, 64).primary_key()) + .col(string_len(FederatedAuthState::Nonce, 64)) + .col(string_len(FederatedAuthState::RedirectUri, 256)) + .col(string_len(FederatedAuthState::PkceVerifier, 64)) + .col(date_time(FederatedAuthState::StartedAt)) + .col(json_null(FederatedAuthState::RequestedScope)) + .foreign_key( + ForeignKey::create() + .name("fk-idp-auth-state-idp") + .from(FederatedAuthState::Table, FederatedAuthState::IdpId) + .to( + FederatedIdentityProvider::Table, + FederatedIdentityProvider::Id, + ) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("fk-idp-auth-state-mapping") + .from(FederatedAuthState::Table, FederatedAuthState::MappingId) + .to(FederatedMapping::Table, FederatedMapping::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + //manager + // .alter_table( + // Table::alter() + // .table(FederatedUser::Table) + // .modify_column(ColumnDef::new(federated_user::Column::ProtocolId).null()) + // //.drop_foreign_key(FederatedUser::FederatedUserIdpIdFkey) + // .to_owned(), + // ) + // .await?; + Ok(()) } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(FederatedAuthState::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(FederatedMapping::Table).to_owned()) + .await?; + manager .drop_table( Table::drop() @@ -89,7 +200,47 @@ enum FederatedIdentityProvider { OidcResponseTypes, BoundIssuer, JwtValidationPubkeys, - //JwksUrl, - //JwksCaPem, ProviderConfig, + DefaultMappingName, +} + +#[derive(DeriveIden)] +enum FederatedMapping { + Table, + Id, + DomainId, + Name, + IdpId, + AllowedRedirectUris, + UserIdClaim, + UserNameClaim, + DomainIdClaim, + GroupsClaim, + BoundAudiences, + BoundSubject, + BoundClaims, + OidcScopes, + TokenUserId, + TokenRoleIds, + TokenProjectId, +} + +#[derive(DeriveIden)] +enum FederatedAuthState { + Table, + IdpId, + MappingId, + State, + Nonce, + RedirectUri, + PkceVerifier, + StartedAt, + RequestedScope, +} + +#[derive(DeriveIden)] +enum FederatedUser { + Table, + ProtocolId, + FederatedUserIdpIdFkey, } diff --git a/migration/src/m20250505_150705_seed_federation.rs b/migration/src/m20250505_150705_seed_federation.rs new file mode 100644 index 00000000..47e1204e --- /dev/null +++ b/migration/src/m20250505_150705_seed_federation.rs @@ -0,0 +1,91 @@ +use openstack_keystone::db::entity::prelude::{ + FederatedAuthState, FederatedIdentityProvider, FederatedMapping, +}; +use openstack_keystone::db::entity::{federated_identity_provider, federated_mapping}; + +use sea_orm::entity::*; +use sea_orm_migration::{prelude::*, schema::*}; +use serde_json::json; +use uuid::Uuid; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + let github = federated_identity_provider::ActiveModel { + id: Set("github".to_owned()), + name: Set("github".to_owned()), + domain_id: NotSet, + oidc_discovery_url: NotSet, + oidc_client_id: Set(Some("Ov23lit3ZDfkXrCz4FEP".to_owned())), + oidc_client_secret: Set(Some("a4a64b0d874f14f35560f5dddd0d06b98cf62bc9".to_owned())), + oidc_response_mode: NotSet, + oidc_response_types: Set(Some("code".to_owned())), + jwt_validation_pubkeys: NotSet, + bound_issuer: NotSet, + provider_config: Set(Some( + json!({"authorization_endpoint": "https://github.com/login/oauth/authorize"}) + .into(), + )), + }; + let gh = FederatedIdentityProvider::insert(github) + .exec(db) + .await? + .last_insert_id; + + let kc = federated_identity_provider::ActiveModel { + id: Set("kc".to_owned()), + name: Set("kc".to_owned()), + domain_id: NotSet, + oidc_discovery_url: Set(Some("http://localhost:8082/realms/master".to_owned())), + oidc_client_id: Set(Some("keystone".to_owned())), + oidc_client_secret: Set(Some("w7GMfkyFzLStHesMxMLgSlJexqa0gQ0F".to_owned())), + oidc_response_mode: NotSet, + oidc_response_types: Set(Some("code".to_owned())), + jwt_validation_pubkeys: NotSet, + bound_issuer: NotSet, + provider_config: NotSet, + }; + let kc1 = FederatedIdentityProvider::insert(kc) + .exec(db) + .await? + .last_insert_id; + + let kcm = federated_mapping::ActiveModel { + id: Set("kc".to_owned()), + name: Set("kc".to_owned()), + domain_id: NotSet, + idp_id: Set(kc1), + allowed_redirect_uris: Set(Some( + "http://localhost:8080/v3/identity_providers/kc/callback".to_owned(), + )), + user_id_claim: Set("sub".to_owned()), + user_name_claim: Set("preferred_username".to_owned()), + domain_id_claim: Set(Some("domain_id".to_owned())), + groups_claim: NotSet, + bound_audiences: NotSet, + bound_subject: NotSet, + bound_claims: NotSet, + oidc_scopes: NotSet, + claim_mappings: NotSet, + token_user_id: NotSet, + token_role_ids: NotSet, + token_project_id: NotSet, + }; + let kcm1 = FederatedMapping::insert(kcm).exec(db).await?.last_insert_id; + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + FederatedIdentityProvider::delete_many().exec(db).await?; + FederatedMapping::delete_many().exec(db).await?; + FederatedAuthState::delete_many().exec(db).await?; + Ok(()) + } +} diff --git a/mod.rs b/mod.rs deleted file mode 100644 index 0632d8e2..00000000 --- a/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.7 - -pub mod prelude; - -pub mod webauthn_credential; -pub mod webauthn_state; diff --git a/prelude.rs b/prelude.rs deleted file mode 100644 index e2057b99..00000000 --- a/prelude.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.7 - -pub use super::webauthn_credential::Entity as WebauthnCredential; -pub use super::webauthn_state::Entity as WebauthnState; diff --git a/src/api/common.rs b/src/api/common.rs index 960338f8..4ddfb602 100644 --- a/src/api/common.rs +++ b/src/api/common.rs @@ -14,15 +14,19 @@ //! Common API helpers //! use crate::api::error::KeystoneApiError; +use crate::api::types::ProjectScope; use crate::keystone::ServiceState; -use crate::resource::{ResourceApi, types::Domain}; +use crate::resource::{ + ResourceApi, + types::{Domain, Project}, +}; pub async fn get_domain, N: AsRef>( state: &ServiceState, id: Option, name: Option, ) -> Result { - let domain = if let Some(did) = &id { + if let Some(did) = &id { state .provider .get_resource_provider() @@ -31,7 +35,7 @@ pub async fn get_domain, N: AsRef>( .ok_or_else(|| KeystoneApiError::NotFound { resource: "domain".into(), identifier: did.as_ref().to_string(), - })? + }) } else if let Some(name) = &name { state .provider @@ -41,11 +45,60 @@ pub async fn get_domain, N: AsRef>( .ok_or_else(|| KeystoneApiError::NotFound { resource: "domain".into(), identifier: name.as_ref().to_string(), - })? + }) } else { return Err(KeystoneApiError::DomainIdOrName); + } +} + +pub async fn find_project_from_scope( + state: &ServiceState, + scope: &ProjectScope, +) -> Result, KeystoneApiError> { + let project = if let Some(pid) = &scope.id { + state + .provider + .get_resource_provider() + .get_project(&state.db, pid) + .await? + } else if let Some(name) = &scope.name { + if let Some(domain) = &scope.domain { + let domain_id = match &domain.id { + Some(id) => id.clone(), + None => { + state + .provider + .get_resource_provider() + .find_domain_by_name( + &state.db, + &domain + .name + .clone() + .ok_or(KeystoneApiError::DomainIdOrName)?, + ) + .await? + .ok_or(KeystoneApiError::NotFound { + resource: "domain".to_string(), + identifier: domain + .name + .clone() + .ok_or(KeystoneApiError::DomainIdOrName)?, + })? + .id + } + }; + state + .provider + .get_resource_provider() + .get_project_by_name(&state.db, name, &domain_id) + .await? + } else { + return Err(KeystoneApiError::ProjectDomain); + } + } else { + return Err(KeystoneApiError::ProjectIdOrName); }; - Ok(domain) + Ok(project) } #[cfg(test)] diff --git a/src/api/error.rs b/src/api/error.rs index 98dd230e..656ead49 100644 --- a/src/api/error.rs +++ b/src/api/error.rs @@ -14,6 +14,7 @@ use axum::{ Json, + extract::rejection::JsonRejection, http::StatusCode, response::{IntoResponse, Response}, }; @@ -21,6 +22,8 @@ use serde_json::json; use thiserror::Error; use tracing::error; +use crate::api::v3::federation::error::OidcError; + use crate::assignment::error::AssignmentProviderError; use crate::catalog::error::CatalogProviderError; use crate::federation::error::FederationProviderError; @@ -40,9 +43,12 @@ pub enum KeystoneApiError { identifier: String, }, - #[error("missing authorization")] + #[error("The request you have made requires authentication.")] Unauthorized, + #[error("You are not authorized to perform the requested action.")] + Forbidden, + #[error("missing x-subject-token header")] SubjectTokenMissing, @@ -79,6 +85,12 @@ pub enum KeystoneApiError { source: FederationProviderError, }, + #[error(transparent)] + Oidc { + #[from] + source: OidcError, + }, + #[error(transparent)] IdentityError { #[from] @@ -123,6 +135,13 @@ pub enum KeystoneApiError { #[error("project domain must be present")] ProjectDomain, + + #[error(transparent)] + JsonExtractorRejection(#[from] JsonRejection), + + /// Others. + #[error(transparent)] + Other(#[from] eyre::Report), } impl IntoResponse for KeystoneApiError { @@ -144,13 +163,13 @@ impl IntoResponse for KeystoneApiError { Json(json!({"error": {"code": StatusCode::UNAUTHORIZED.as_u16(), "message": self.to_string()}})), ).into_response() } - KeystoneApiError::InternalError(_) | KeystoneApiError::IdentityError { .. } | KeystoneApiError::ResourceError { .. } | KeystoneApiError::AssignmentError { .. } | KeystoneApiError::TokenError{..} | KeystoneApiError::Federation {..} => { + KeystoneApiError::InternalError(_) | KeystoneApiError::IdentityError { .. } | KeystoneApiError::ResourceError { .. } | KeystoneApiError::AssignmentError { .. } | KeystoneApiError::TokenError{..} | KeystoneApiError::Federation {..} | KeystoneApiError::Oidc{..} | KeystoneApiError::Other(..) => { (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": {"code": StatusCode::INTERNAL_SERVER_ERROR.as_u16(), "message": self.to_string()}})), ).into_response() } _ => { - // KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::Token{..} | KeystoneApiError::WebAuthN{..} | KeystoneApiError::Uuid {..} | KeystoneApiError::Serde {..} | KeystoneApiError::DomainIdOrName | KeystoneApiError::ProjectIdOrName | KeystoneApiError::ProjectDomain => + // KeystoneApiError::SubjectTokenMissing | KeystoneApiError::InvalidHeader | KeystoneApiError::InvalidToken | KeystoneApiError::Token{..} | KeystoneApiError::WebAuthN{..} | KeystoneApiError::Uuid {..} | KeystoneApiError::Serde {..} | KeystoneApiError::DomainIdOrName | KeystoneApiError::ProjectIdOrName | KeystoneApiError::ProjectDomain => (StatusCode::BAD_REQUEST, Json(json!({"error": {"code": StatusCode::BAD_REQUEST.as_u16(), "message": self.to_string()}})), ).into_response() @@ -175,6 +194,10 @@ impl KeystoneApiError { resource: "identity provider".into(), identifier: x, }, + FederationProviderError::MappingNotFound(x) => Self::NotFound { + resource: "mapping provider".into(), + identifier: x, + }, _ => Self::Federation { source }, } } @@ -219,7 +242,7 @@ pub enum TokenError { #[error("error building token user data: {}", source)] ProjectBuilder { #[from] - source: crate::api::v3::auth::token::types::ProjectBuilderError, + source: crate::api::types::ProjectBuilderError, }, #[error(transparent)] diff --git a/src/api/types.rs b/src/api/types.rs index 07554d23..670ce12c 100644 --- a/src/api/types.rs +++ b/src/api/types.rs @@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use crate::catalog::types::{Endpoint as ProviderEndpoint, Service}; +use crate::resource::types as resource_provider_types; #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] pub struct Versions { @@ -155,3 +156,72 @@ impl From)>> for Catalog { ) } } + +/// The authorization scope, including the system (Since v3.10), a project, or a domain (Since +/// v3.4). If multiple scopes are specified in the same request (e.g. project and domain or domain +/// and system) an HTTP 400 Bad Request will be returned, as a token cannot be simultaneously +/// scoped to multiple authorization targets. An ID is sufficient to uniquely identify a project +/// but if a project is specified by name, then the domain of the project must also be specified in +/// order to uniquely identify the project by name. A domain scope may be specified by either the +/// domain’s ID or name with equivalent results. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub enum Scope { + /// Project scope + #[serde(rename = "project")] + Project(ProjectScope), + /// Domain scope + #[serde(rename = "domain")] + Domain(Domain), +} + +/// Project scope information +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct ProjectScope { + /// Project ID + pub id: Option, + /// Project Name + pub name: Option, + /// project domain + pub domain: Option, +} + +/// Domain information +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(into))] +pub struct Domain { + /// Domain ID + #[builder(default)] + pub id: Option, + /// Domain Name + #[builder(default)] + pub name: Option, +} + +/// Project information +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct Project { + /// Project ID + pub id: String, + /// Project Name + pub name: String, + /// project domain + pub domain: Domain, +} + +impl From for Domain { + fn from(value: resource_provider_types::Domain) -> Self { + Self { + id: Some(value.id.clone()), + name: Some(value.name.clone()), + } + } +} + +impl From<&resource_provider_types::Domain> for Domain { + fn from(value: &resource_provider_types::Domain) -> Self { + Self { + id: Some(value.id.clone()), + name: Some(value.name.clone()), + } + } +} diff --git a/src/api/v3/auth/token/common.rs b/src/api/v3/auth/token/common.rs index f38ec61c..033ac43d 100644 --- a/src/api/v3/auth/token/common.rs +++ b/src/api/v3/auth/token/common.rs @@ -14,7 +14,8 @@ use crate::api::common; use crate::api::error::{KeystoneApiError, TokenError}; -use crate::api::v3::auth::token::types::{ProjectBuilder, Token, TokenBuilder, UserBuilder}; +use crate::api::types::ProjectBuilder; +use crate::api::v3::auth::token::types::{Token, TokenBuilder, UserBuilder}; use crate::api::v3::role::types::Role; use crate::identity::{IdentityApi, types::UserResponse}; use crate::keystone::ServiceState; diff --git a/src/api/v3/auth/token/mod.rs b/src/api/v3/auth/token/mod.rs index 097c686d..fed32057 100644 --- a/src/api/v3/auth/token/mod.rs +++ b/src/api/v3/auth/token/mod.rs @@ -20,21 +20,25 @@ use axum::{ response::IntoResponse, }; use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use tracing::debug; use utoipa_axum::{router::OpenApiRouter, routes}; use uuid::Uuid; -use crate::api::{Catalog, auth::Auth, common::get_domain, error::KeystoneApiError}; +use crate::api::types::Scope; +use crate::api::{ + Catalog, + auth::Auth, + common::{find_project_from_scope, get_domain}, + error::KeystoneApiError, +}; use crate::catalog::CatalogApi; use crate::identity::IdentityApi; use crate::identity::types::UserResponse; use crate::keystone::ServiceState; -use crate::resource::{ - ResourceApi, - types::{Domain, Project}, -}; +use crate::resource::types::{Domain, Project}; use crate::token::TokenApi; use types::{ - AuthRequest, CreateTokenParameters, Scope, Token as ApiResponseToken, TokenResponse, + AuthRequest, CreateTokenParameters, Token as ApiResponseToken, TokenResponse, ValidateTokenParameters, }; @@ -66,52 +70,11 @@ async fn post( let mut user: Option = None; let mut project: Option = None; let mut domain: Option = None; + debug!("Scope is {:?}", req.auth.scope); match req.auth.scope { Some(Scope::Project(scope)) => { - project = if let Some(pid) = &scope.id { - state - .provider - .get_resource_provider() - .get_project(&state.db, pid) - .await? - } else if let Some(name) = &scope.name { - if let Some(domain) = scope.domain { - let domain_id = match domain.id { - Some(id) => id.clone(), - None => { - state - .provider - .get_resource_provider() - .find_domain_by_name( - &state.db, - &domain - .name - .clone() - .ok_or(KeystoneApiError::DomainIdOrName)?, - ) - .await? - .ok_or(KeystoneApiError::NotFound { - resource: "domain".to_string(), - identifier: domain - .name - .clone() - .ok_or(KeystoneApiError::DomainIdOrName)?, - })? - .id - } - }; - state - .provider - .get_resource_provider() - .get_project_by_name(&state.db, name, &domain_id) - .await? - } else { - return Err(KeystoneApiError::ProjectDomain); - } - } else { - return Err(KeystoneApiError::ProjectIdOrName); - }; + project = find_project_from_scope(&state, &scope).await?; if !project.as_ref().is_some_and(|target| target.enabled) { return Err(KeystoneApiError::Unauthorized); } @@ -138,6 +101,27 @@ async fn post( ); methods.push(method.clone()); } + } else if method == "token" { + if let Some(token) = &req.auth.identity.token { + let current_token = state + .provider + .get_token_provider() + .validate_token(&token.id, Some(false), None) + .await + .map_err(|_| KeystoneApiError::NotFound { + resource: "token".into(), + identifier: String::new(), + })?; + user = state + .provider + .get_identity_provider() + .get_user(&state.db, current_token.user_id()) + .await + .map_err(|_| KeystoneApiError::NotFound { + resource: "user".into(), + identifier: current_token.user_id().clone(), + })?; + } } } @@ -157,7 +141,8 @@ async fn post( .provider .get_token_provider() .populate_role_assignments(&mut token, &state.db, &state.provider) - .await?; + .await + .map_err(|_| KeystoneApiError::Forbidden)?; state .provider diff --git a/src/api/v3/auth/token/types.rs b/src/api/v3/auth/token/types.rs index e09829c6..5023285d 100644 --- a/src/api/v3/auth/token/types.rs +++ b/src/api/v3/auth/token/types.rs @@ -23,10 +23,9 @@ use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; use crate::api::error::TokenError; -use crate::api::types::Catalog; +use crate::api::types::*; use crate::api::v3::role::types::Role; use crate::identity::types as identity_types; -use crate::resource::types as resource_provider_types; use crate::token::Token as BackendToken; /// Authorization token @@ -126,6 +125,9 @@ pub struct Identity { /// The password object, contains the authentication information. pub password: Option, + + /// The token object, contains the authentication information. + pub token: Option, } /// The password object, contains the authentication information. @@ -176,45 +178,6 @@ impl TryFrom for identity_types::UserPasswordAuthRequest { } } -/// The authorization scope, including the system (Since v3.10), a project, or a domain (Since -/// v3.4). If multiple scopes are specified in the same request (e.g. project and domain or domain -/// and system) an HTTP 400 Bad Request will be returned, as a token cannot be simultaneously -/// scoped to multiple authorization targets. An ID is sufficient to uniquely identify a project -/// but if a project is specified by name, then the domain of the project must also be specified in -/// order to uniquely identify the project by name. A domain scope may be specified by either the -/// domain’s ID or name with equivalent results. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] -pub enum Scope { - /// Project scope - #[serde(rename = "project")] - Project(ProjectScope), - /// Domain scope - #[serde(rename = "domain")] - Domain(Domain), -} - -/// Project scope information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct ProjectScope { - /// Project ID - pub id: Option, - /// Project Name - pub name: Option, - /// project domain - pub domain: Option, -} - -/// Project information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -pub struct Project { - /// Project ID - pub id: String, - /// Project Name - pub name: String, - /// project domain - pub domain: Domain, -} - /// User information #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] #[builder(setter(into))] @@ -231,36 +194,6 @@ pub struct User { pub password_expires_at: Option>, } -/// Domain information -#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] -#[builder(setter(into))] -pub struct Domain { - /// Domain ID - #[builder(default)] - pub id: Option, - /// Domain Name - #[builder(default)] - pub name: Option, -} - -impl From for Domain { - fn from(value: resource_provider_types::Domain) -> Self { - Self { - id: Some(value.id.clone()), - name: Some(value.name.clone()), - } - } -} - -impl From<&resource_provider_types::Domain> for Domain { - fn from(value: &resource_provider_types::Domain) -> Self { - Self { - id: Some(value.id.clone()), - name: Some(value.name.clone()), - } - } -} - impl TryFrom<&BackendToken> for Token { type Error = TokenError; @@ -274,6 +207,14 @@ impl TryFrom<&BackendToken> for Token { } } +/// The token object, contains the authentication information. +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct TokenAuth { + /// An authentication token. + pub id: String, +} + #[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] pub struct CreateTokenParameters { /// The authentication response excludes the service catalog. By default, the response includes diff --git a/src/api/v3/federation/auth.rs b/src/api/v3/federation/auth.rs new file mode 100644 index 00000000..bd9df174 --- /dev/null +++ b/src/api/v3/federation/auth.rs @@ -0,0 +1,296 @@ +// 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 + +use axum::{ + Json, debug_handler, + extract::{Path, State}, + http::{StatusCode, header::LOCATION}, + response::IntoResponse, +}; +use chrono::Local; +use std::collections::HashSet; +use tracing::debug; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use openidconnect::core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata}; +use openidconnect::reqwest; +use openidconnect::{ + ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, PkceCodeChallenge, RedirectUrl, Scope, +}; + +use crate::api::types::Scope as ApiScope; +use crate::api::v3::federation::error::OidcError; +use crate::api::v3::federation::types::*; +use crate::api::{ + common::{find_project_from_scope, get_domain}, + error::KeystoneApiError, +}; +use crate::federation::FederationApi; +use crate::federation::types::{ + AuthState, MappingListParameters as ProviderMappingListParameters, Scope as ProviderScope, +}; +use crate::keystone::ServiceState; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new().routes(routes!(post, get)) +} + +/// Authenticate using identity provider +#[utoipa::path( + get, + path = "/identity_providers/{idp_id}/auth", + description = "Authenticate using identity provider", + responses( + (status = CREATED, description = "identity provider object", body = IdentityProviderAuthResponse), + ), + tag="identity_providers" +)] +#[tracing::instrument(name = "api::identity_provider_auth", level = "debug", skip(state))] +#[debug_handler] +pub async fn get( + State(state): State, + Path(idp_id): Path, +) -> Result { + let idp = state + .provider + .get_federation_provider() + .get_identity_provider(&state.db, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id, + }) + })??; + + if let Some(discovery_url) = &idp.oidc_discovery_url { + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .expect("Client should build"); + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(discovery_url.to_string()).map_err(OidcError::from)?, + &http_client, + ) + .await + .map_err(|err| OidcError::discovery(&err))?; + let client = CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(idp.oidc_client_id.expect("client_id is mandatory")), + idp.oidc_client_secret.map(ClientSecret::new), + ) + // Set the URL the user will be redirected to after the authorization process. + .set_redirect_uri( + RedirectUrl::new("http://localhost:8080/v3/federation/auth/callback".to_string()) + .map_err(OidcError::from)?, + ); + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + // Generate the full authorization URL. + let (auth_url, csrf_token, nonce) = client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + // Set the desired scopes. + .add_scope(Scope::new("openid".to_string())) + // Set the PKCE code challenge. + .set_pkce_challenge(pkce_challenge) + .url(); + + state + .provider + .get_federation_provider() + .create_auth_state( + &state.db, + AuthState { + state: csrf_token.secret().clone(), + nonce: nonce.secret().clone(), + idp_id: idp.id.clone(), + mapping_id: String::from("kc"), + redirect_uri: String::new(), + pkce_verifier: pkce_verifier.into_secret(), + started_at: Local::now().into(), + scope: None, + }, + ) + .await?; + + debug!( + "url: {:?}, csrf: {:?}, nonce: {:?}", + auth_url, + csrf_token.secret(), + nonce.secret() + ); + return Ok((StatusCode::FOUND, [(LOCATION, &auth_url.to_string())]).into_response()); + //return Ok(Redirect::with_status_code(StatusCode::FOUND, &auth_url.to_string()).into_response()); + } + + Ok((StatusCode::CREATED).into_response()) +} + +/// Authenticate using identity provider +#[utoipa::path( + post, + path = "/identity_providers/{idp_id}/auth", + description = "Authenticate using identity provider", + responses( + (status = CREATED, description = "identity provider object", body = IdentityProviderAuthResponse), + ), + tag="identity_providers" +)] +#[tracing::instrument(name = "api::identity_provider_auth", level = "debug", skip(state))] +#[debug_handler] +pub async fn post( + State(state): State, + Path(idp_id): Path, + Json(req): Json, +) -> Result { + let idp = state + .provider + .get_federation_provider() + .get_identity_provider(&state.db, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id, + }) + })??; + + let mapping = if let Some(mapping_id) = req.mapping_id { + state + .provider + .get_federation_provider() + .get_mapping(&state.db, &mapping_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: mapping_id.clone(), + }) + })?? + } else if let Some(mapping_name) = req.mapping_name.or(idp.default_mapping_name) { + state + .provider + .get_federation_provider() + .list_mappings( + &state.db, + &ProviderMappingListParameters { + idp_id: Some(idp.id.clone()), + name: Some(mapping_name.clone()), + domain_id: None, + }, + ) + .await? + .first() + .ok_or(KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: mapping_name.clone(), + })? + .to_owned() + } else { + return Err(OidcError::MappingRequired)?; + }; + + let client = if let Some(discovery_url) = &idp.oidc_discovery_url { + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .expect("Client should build"); + + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(discovery_url.to_string()).map_err(OidcError::from)?, + &http_client, + ) + .await + .map_err(|err| OidcError::discovery(&err))?; + CoreClient::from_provider_metadata( + provider_metadata, + ClientId::new(idp.oidc_client_id.expect("client_id is mandatory")), + idp.oidc_client_secret.map(ClientSecret::new), + ) + // Set the URL the user will be redirected to after the authorization process. + // TODO: Check the redirect uri against mapping.allowed_redirect_uris + .set_redirect_uri(RedirectUrl::new(req.redirect_uri.clone()).map_err(OidcError::from)?) + } else { + return Err(OidcError::ClientWithoutDiscoveryNotSupported)?; + }; + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + let mut oidc_scopes: HashSet = if let Some(mapping_scopes) = mapping.oidc_scopes { + HashSet::from_iter(mapping_scopes.into_iter().map(Scope::new)) + } else { + HashSet::new() + }; + oidc_scopes.insert(Scope::new("openid".to_string())); + + // Generate the full authorization URL. + let (auth_url, csrf_token, nonce) = client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + .add_scopes(oidc_scopes) + // Set the PKCE code challenge. + .set_pkce_challenge(pkce_challenge) + .url(); + + let scope: Option = match req.scope { + Some(ApiScope::Project(scope)) => find_project_from_scope(&state, &scope) + .await? + .map(|x| ProviderScope::Project(x.id.clone())), + Some(ApiScope::Domain(scope)) => get_domain(&state, scope.id.as_ref(), scope.name.as_ref()) + .await + .map(|x| ProviderScope::Domain(x.id.clone())) + .ok(), + _ => None, + }; + + state + .provider + .get_federation_provider() + .create_auth_state( + &state.db, + AuthState { + state: csrf_token.secret().clone(), + nonce: nonce.secret().clone(), + idp_id: idp.id.clone(), + mapping_id: mapping.id.clone(), + redirect_uri: req.redirect_uri.clone(), + pkce_verifier: pkce_verifier.into_secret(), + started_at: Local::now().into(), + scope, + }, + ) + .await?; + + debug!( + "url: {:?}, csrf: {:?}, nonce: {:?}", + auth_url, + csrf_token.secret(), + nonce.secret() + ); + Ok(IdentityProviderAuthResponse { + auth_url: auth_url.to_string(), + } + .into_response()) +} diff --git a/src/api/v3/federation/error.rs b/src/api/v3/federation/error.rs new file mode 100644 index 00000000..25b31c9a --- /dev/null +++ b/src/api/v3/federation/error.rs @@ -0,0 +1,122 @@ +// 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 + +use thiserror::Error; +use tracing::error; + +use crate::api::v3::federation::types::*; + +#[derive(Error, Debug)] +pub enum OidcError { + #[error("discovery error")] + Discovery { msg: String }, + + #[error("Client without discovery is not supported")] + ClientWithoutDiscoveryNotSupported, + + #[error( + "Federated authentication requires mapping being specified in the payload or default set on the identity provider" + )] + MappingRequired, + + #[error("request token error")] + RequestToken { msg: String }, + + #[error("claim verification error")] + ClaimVerification { + #[from] + source: openidconnect::ClaimsVerificationError, + }, + + #[error("error parsing the url")] + UrlParse { + #[from] + source: url::ParseError, + }, + + #[error("server did not returned an ID token")] + NoToken, + + #[error("ID token does not contain user id claim {0}")] + UserIdClaimMissing(String), + #[error("ID token does not contain user id claim {0}")] + UserNameClaimMissing(String), + #[error("can not identify resulting domain_id for the user")] + UserDomainUnbound, + + #[error("bound subject mismatches {expected} != {found}")] + BoundSubjectMismatch { expected: String, found: String }, + #[error("bound audiences mismatch {expected} != {found}")] + BoundAudiencesMismatch { expected: String, found: String }, + #[error("bound claims mismatch")] + BoundClaimsMismatch { + claim: String, + expected: String, + found: String, + }, + + #[error(transparent)] + MappedUserDataBuilder { + #[from] + source: MappedUserDataBuilderError, + }, +} + +impl OidcError { + pub fn discovery(fail: &T) -> Self { + Self::Discovery { + msg: fail.to_string(), + } + } + pub fn request_token(fail: &T) -> Self { + Self::RequestToken { + msg: fail.to_string(), + } + } + // pub fn url(fail: url::ParseError) -> Self { + // Self::RequestToken { + // msg: fail.to_string(), + // } + // } + + // pub fn claim_verification(fail: &T) -> Self { + // Self::ClaimVerification{msg: fail.to_string()} + // } +} + +//impl +// From< +// openidconnect::DiscoveryError< +// openidconnect::HttpClientError, +// >, +// > for OidcError +//{ +// fn from( +// source: openidconnect::DiscoveryError< +// openidconnect::HttpClientError, +// >, +// ) -> Self { +// Self::OidcDiscovery { +// msg: source.to_string(), +// } +// } +//} + +//impl OidcError { +// fn discovery(source: RE) -> Self { +// Self::OidcDiscovery { +// source: source.to_string(), +// } +// } +//} diff --git a/src/api/v3/federation/identity_provider.rs b/src/api/v3/federation/identity_provider.rs new file mode 100644 index 00000000..4ea24298 --- /dev/null +++ b/src/api/v3/federation/identity_provider.rs @@ -0,0 +1,572 @@ +// 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 + +use axum::{ + Json, debug_handler, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, +}; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::api::v3::federation::types::*; +use crate::federation::FederationApi; +use crate::keystone::ServiceState; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(list, create)) + .routes(routes!(show, update, remove)) + //.routes(routes!(auth::auth)) +} + +/// List identity providers +#[utoipa::path( + get, + path = "/", + params(IdentityProviderListParameters), + description = "List Identity Providers", + responses( + (status = OK, description = "List of identity providers", body = IdentityProviderList), + (status = 500, description = "Internal error", example = json!(KeystoneApiError::InternalError(String::from("id = 1")))) + ), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_list", + level = "debug", + skip(state, _user_auth) +)] +async fn list( + Auth(_user_auth): Auth, + Query(query): Query, + State(state): State, +) -> Result { + let identity_providers: Vec = state + .provider + .get_federation_provider() + .list_identity_providers(&state.db, &query.try_into()?) + .await + .map_err(KeystoneApiError::federation)? + .into_iter() + .map(Into::into) + .collect(); + Ok(IdentityProviderList { identity_providers }) +} + +/// Get single identity provider +#[utoipa::path( + get, + path = "/{idp_id}", + description = "Get IDP by ID", + params(), + responses( + (status = OK, description = "IDP object", body = IdentityProviderResponse), + (status = 404, description = "IDP not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + tag="identity_providers" +)] +#[tracing::instrument(name = "api::identity_provider_get", level = "debug", skip(state))] +async fn show( + Auth(user_auth): Auth, + Path(idp_id): Path, + State(state): State, +) -> Result { + state + .provider + .get_federation_provider() + .get_identity_provider(&state.db, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id, + }) + })? +} + +/// Create identity provider +#[utoipa::path( + post, + path = "/", + description = "Create new identity provider", + responses( + (status = CREATED, description = "identity provider object", body = IdentityProviderResponse), + ), + tag="identity_providers" +)] +#[tracing::instrument(name = "api::identity_provider_create", level = "debug", skip(state))] +#[debug_handler] +async fn create( + Auth(user_auth): Auth, + State(state): State, + Json(req): Json, +) -> Result { + let res = state + .provider + .get_federation_provider() + .create_identity_provider(&state.db, req.into()) + .await + .map_err(KeystoneApiError::federation)?; + Ok((StatusCode::CREATED, res).into_response()) +} + +/// Update single identity provider +#[utoipa::path( + put, + path = "/{idp_id}", + description = "Update Identity Provider", + params(), + responses( + (status = OK, description = "IDP object", body = IdentityProviderResponse), + (status = 404, description = "IDP not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + tag="identity_providers" +)] +#[tracing::instrument(name = "api::identity_provider_update", level = "debug", skip(state))] +async fn update( + Auth(user_auth): Auth, + Path(idp_id): Path, + State(state): State, + Json(req): Json, +) -> Result { + let res = state + .provider + .get_federation_provider() + .update_identity_provider(&state.db, &idp_id, req.into()) + .await + .map_err(KeystoneApiError::federation)?; + Ok(res.into_response()) +} + +/// Delete Identity provider +#[utoipa::path( + delete, + path = "/{idp_id}", + description = "Delete identity provider by ID", + params(), + responses( + (status = 204, description = "Deleted"), + (status = 404, description = "identity provider not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + tag="identity_providers" +)] +#[tracing::instrument(name = "api::identity_provider_delete", level = "debug", skip(state))] +async fn remove( + Auth(user_auth): Auth, + Path(id): Path, + State(state): State, +) -> Result { + state + .provider + .get_federation_provider() + .delete_identity_provider(&state.db, &id) + .await + .map_err(KeystoneApiError::federation)?; + Ok((StatusCode::NO_CONTENT).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode, header}, + }; + use http_body_util::BodyExt; // for `collect` + use sea_orm::DatabaseConnection; + + use std::sync::Arc; + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + + use super::*; + use crate::config::Config; + use crate::federation::{ + MockFederationProvider, error::FederationProviderError, types as provider_types, + }; + use crate::keystone::{Service, ServiceState}; + use crate::provider::Provider; + use crate::token::{MockTokenProvider, Token, UnscopedToken}; + + fn get_mocked_state(federation_mock: MockFederationProvider) -> ServiceState { + let mut token_mock = MockTokenProvider::default(); + token_mock.expect_validate_token().returning(|_, _, _| { + Ok(Token::Unscoped(UnscopedToken { + user_id: "bar".into(), + ..Default::default() + })) + }); + + let provider = Provider::mocked_builder() + .federation(federation_mock) + .token(token_mock) + .build() + .unwrap(); + + Arc::new( + Service::new( + Config::default(), + DatabaseConnection::Disconnected, + provider, + ) + .unwrap(), + ) + } + + #[tokio::test] + async fn test_list() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf( + |_: &DatabaseConnection, _: &provider_types::IdentityProviderListParameters| true, + ) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + default_mapping_name: Some("dummy".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + assert_eq!( + vec![IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + oidc_discovery_url: None, + oidc_client_id: None, + oidc_response_mode: None, + oidc_response_types: None, + jwt_validation_pubkeys: None, + bound_issuer: None, + default_mapping_name: Some("dummy".into()), + provider_config: None + }], + res.identity_providers + ); + } + + #[tokio::test] + async fn test_list_qp() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_identity_providers() + .withf( + |_: &DatabaseConnection, qp: &provider_types::IdentityProviderListParameters| { + provider_types::IdentityProviderListParameters { + name: Some("name".into()), + domain_id: Some("did".into()), + } == *qp + }, + ) + .returning(|_, _| { + Ok(vec![provider_types::IdentityProvider { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?name=name&domain_id=did") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + async fn test_get() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_identity_provider() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "foo") + .returning(|_, _| Ok(None)); + + federation_mock + .expect_get_identity_provider() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + default_mapping_name: Some("dummy".into()), + ..Default::default() + })) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + oidc_discovery_url: None, + oidc_client_id: None, + oidc_response_mode: None, + oidc_response_types: None, + jwt_validation_pubkeys: None, + bound_issuer: None, + default_mapping_name: Some("dummy".into()), + provider_config: None + }, + res.identity_provider, + ); + } + + #[tokio::test] + async fn test_create() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_create_identity_provider() + .withf( + |_: &DatabaseConnection, req: &provider_types::IdentityProvider| req.name == "name", + ) + .returning(|_, _| { + Ok(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = IdentityProviderCreateRequest { + identity_provider: IdentityProviderCreate { + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("POST") + .header(header::CONTENT_TYPE, "application/json") + .uri("/") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(res.identity_provider.name, req.identity_provider.name); + assert_eq!( + res.identity_provider.domain_id, + req.identity_provider.domain_id + ); + } + + #[tokio::test] + async fn test_update() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_update_identity_provider() + .withf( + |_: &DatabaseConnection, + id: &'_ str, + req: &provider_types::IdentityProviderUpdate| { + id == "1" && req.name == Some("name".to_string()) + }, + ) + .returning(|_, _, _| { + Ok(provider_types::IdentityProvider { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = IdentityProviderUpdateRequest { + identity_provider: IdentityProviderUpdate { + name: Some("name".into()), + oidc_client_id: Some(None), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("PUT") + .header(header::CONTENT_TYPE, "application/json") + .uri("/1") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + async fn test_delete() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_delete_identity_provider() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "foo") + .returning(|_, _| { + Err(FederationProviderError::IdentityProviderNotFound( + "foo".into(), + )) + }); + + federation_mock + .expect_delete_identity_provider() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "bar") + .returning(|_, _| Ok(())); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + } +} diff --git a/src/api/v3/federation/mapping.rs b/src/api/v3/federation/mapping.rs new file mode 100644 index 00000000..70d6cbd8 --- /dev/null +++ b/src/api/v3/federation/mapping.rs @@ -0,0 +1,587 @@ +// 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 + +use axum::{ + Json, debug_handler, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, +}; +use utoipa_axum::{router::OpenApiRouter, routes}; + +use crate::api::auth::Auth; +use crate::api::error::KeystoneApiError; +use crate::api::v3::federation::types::*; +use crate::federation::FederationApi; +use crate::keystone::ServiceState; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new() + .routes(routes!(list, create)) + .routes(routes!(show, update, remove)) +} + +/// List mappings +#[utoipa::path( + get, + path = "/", + params(MappingListParameters), + description = "List federation mappings", + responses( + (status = OK, description = "List of mappings", body = MappingList), + (status = 500, description = "Internal error", example = json!(KeystoneApiError::InternalError(String::from("id = 1")))) + ), + tag="mappings" +)] +#[tracing::instrument(name = "api::mapping_list", level = "debug", skip(state, _user_auth))] +async fn list( + Auth(_user_auth): Auth, + Query(query): Query, + State(state): State, +) -> Result { + let mappings: Vec = state + .provider + .get_federation_provider() + .list_mappings(&state.db, &query.try_into()?) + .await + .map_err(KeystoneApiError::federation)? + .into_iter() + .map(Into::into) + .collect(); + Ok(MappingList { mappings }) +} + +/// Get single mapping +#[utoipa::path( + get, + path = "/{idp_id}", + description = "Get mapping by ID", + params(), + responses( + (status = OK, description = "mapping object", body = MappingResponse), + (status = 404, description = "mapping not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + tag="mappings" +)] +#[tracing::instrument(name = "api::mapping_get", level = "debug", skip(state))] +async fn show( + Auth(user_auth): Auth, + Path(idp_id): Path, + State(state): State, +) -> Result { + state + .provider + .get_federation_provider() + .get_mapping(&state.db, &idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: idp_id, + }) + })? +} + +/// Create mapping +#[utoipa::path( + post, + path = "/", + description = "Create new mapping", + responses( + (status = CREATED, description = "mapping object", body = MappingResponse), + ), + tag="mappings" +)] +#[tracing::instrument(name = "api::mapping_create", level = "debug", skip(state))] +#[debug_handler] +async fn create( + Auth(user_auth): Auth, + State(state): State, + Json(req): Json, +) -> Result { + let res = state + .provider + .get_federation_provider() + .create_mapping(&state.db, req.into()) + .await + .map_err(KeystoneApiError::federation)?; + Ok((StatusCode::CREATED, res).into_response()) +} + +/// Update single mapping +#[utoipa::path( + put, + path = "/{idp_id}", + description = "Update existing mapping", + params(), + responses( + (status = OK, description = "mapping object", body = MappingResponse), + (status = 404, description = "mapping not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + tag="mappings" +)] +#[tracing::instrument(name = "api::mapping_update", level = "debug", skip(state))] +async fn update( + Auth(user_auth): Auth, + Path(idp_id): Path, + State(state): State, + Json(req): Json, +) -> Result { + let res = state + .provider + .get_federation_provider() + .update_mapping(&state.db, &idp_id, req.into()) + .await + .map_err(KeystoneApiError::federation)?; + Ok(res.into_response()) +} + +/// Delete Identity provider +#[utoipa::path( + delete, + path = "/{idp_id}", + description = "Delete mapping by ID", + params(), + responses( + (status = 204, description = "Deleted"), + (status = 404, description = "mapping not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) + ), + tag="mappings" +)] +#[tracing::instrument(name = "api::mapping_delete", level = "debug", skip(state))] +async fn remove( + Auth(user_auth): Auth, + Path(id): Path, + State(state): State, +) -> Result { + state + .provider + .get_federation_provider() + .delete_mapping(&state.db, &id) + .await + .map_err(KeystoneApiError::federation)?; + Ok((StatusCode::NO_CONTENT).into_response()) +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{Request, StatusCode, header}, + }; + use http_body_util::BodyExt; // for `collect` + use sea_orm::DatabaseConnection; + + use std::sync::Arc; + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + use tower_http::trace::TraceLayer; + + use super::*; + use crate::config::Config; + use crate::federation::{ + MockFederationProvider, error::FederationProviderError, types as provider_types, + }; + use crate::keystone::{Service, ServiceState}; + use crate::provider::Provider; + use crate::token::{MockTokenProvider, Token, UnscopedToken}; + + fn get_mocked_state(federation_mock: MockFederationProvider) -> ServiceState { + let mut token_mock = MockTokenProvider::default(); + token_mock.expect_validate_token().returning(|_, _, _| { + Ok(Token::Unscoped(UnscopedToken { + user_id: "bar".into(), + ..Default::default() + })) + }); + + let provider = Provider::mocked_builder() + .federation(federation_mock) + .token(token_mock) + .build() + .unwrap(); + + Arc::new( + Service::new( + Config::default(), + DatabaseConnection::Disconnected, + provider, + ) + .unwrap(), + ) + } + + #[tokio::test] + async fn test_list() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_mappings() + .withf(|_: &DatabaseConnection, _: &provider_types::MappingListParameters| true) + .returning(|_, _| { + Ok(vec![provider_types::Mapping { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + }]) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingList = serde_json::from_slice(&body).unwrap(); + assert_eq!( + vec![Mapping { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_user_id: None, + token_role_ids: None, + token_project_id: None + }], + res.mappings + ); + } + + #[tokio::test] + async fn test_list_qp() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_list_mappings() + .withf( + |_: &DatabaseConnection, qp: &provider_types::MappingListParameters| { + provider_types::MappingListParameters { + name: Some("name".into()), + domain_id: Some("did".into()), + idp_id: Some("idp".into()), + } == *qp + }, + ) + .returning(|_, _| { + Ok(vec![provider_types::Mapping { + id: "id".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_user_id: None, + token_role_ids: None, + token_project_id: None, + }]) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/?name=name&domain_id=did&idp_id=idp") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: MappingList = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + async fn test_get() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_get_mapping() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "foo") + .returning(|_, _| Ok(None)); + + federation_mock + .expect_get_mapping() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "bar") + .returning(|_, _| { + Ok(Some(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + })) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NOT_FOUND); + + let response = api + .as_service() + .oneshot( + Request::builder() + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!( + Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp_id".into(), + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_user_id: None, + token_role_ids: None, + token_project_id: None, + }, + res.mapping, + ); + } + + #[tokio::test] + async fn test_create() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_create_mapping() + .withf(|_: &DatabaseConnection, req: &provider_types::Mapping| req.name == "name") + .returning(|_, _| { + Ok(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = MappingCreateRequest { + mapping: MappingCreate { + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("POST") + .header(header::CONTENT_TYPE, "application/json") + .uri("/") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let res: MappingResponse = serde_json::from_slice(&body).unwrap(); + assert_eq!(res.mapping.name, req.mapping.name); + assert_eq!(res.mapping.domain_id, req.mapping.domain_id); + } + + #[tokio::test] + async fn test_update() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_update_mapping() + .withf( + |_: &DatabaseConnection, id: &'_ str, req: &provider_types::MappingUpdate| { + id == "1" && req.name == Some("name".to_string()) + }, + ) + .returning(|_, _, _| { + Ok(provider_types::Mapping { + id: "bar".into(), + name: "name".into(), + domain_id: Some("did".into()), + ..Default::default() + }) + }); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let req = MappingUpdateRequest { + mapping: MappingUpdate { + name: Some("name".into()), + ..Default::default() + }, + }; + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("PUT") + .header(header::CONTENT_TYPE, "application/json") + .uri("/1") + .header("x-auth-token", "foo") + .body(Body::from(serde_json::to_string(&req).unwrap())) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let _res: MappingResponse = serde_json::from_slice(&body).unwrap(); + } + + #[tokio::test] + async fn test_delete() { + let mut federation_mock = MockFederationProvider::default(); + federation_mock + .expect_delete_mapping() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "foo") + .returning(|_, _| Err(FederationProviderError::MappingNotFound("foo".into()))); + + federation_mock + .expect_delete_mapping() + .withf(|_: &DatabaseConnection, id: &'_ str| id == "bar") + .returning(|_, _| Ok(())); + + let state = get_mocked_state(federation_mock); + + let mut api = openapi_router() + .layer(TraceLayer::new_for_http()) + .with_state(state.clone()); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/foo") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!( + response.status(), + StatusCode::NOT_FOUND, + "{:?}", + response.into_body().collect().await.unwrap() + ); + + let response = api + .as_service() + .oneshot( + Request::builder() + .method("DELETE") + .uri("/bar") + .header("x-auth-token", "foo") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::NO_CONTENT); + } +} diff --git a/src/api/v3/federation/mod.rs b/src/api/v3/federation/mod.rs index 0fd81df9..4d3cd32c 100644 --- a/src/api/v3/federation/mod.rs +++ b/src/api/v3/federation/mod.rs @@ -12,558 +12,21 @@ // // SPDX-License-Identifier: Apache-2.0 -use axum::{ - Json, debug_handler, - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, -}; -use utoipa_axum::{router::OpenApiRouter, routes}; +use utoipa_axum::router::OpenApiRouter; -use crate::api::auth::Auth; -use crate::api::error::KeystoneApiError; -use crate::federation::FederationApi; use crate::keystone::ServiceState; -use types::*; +pub mod auth; +pub mod error; +pub mod identity_provider; +pub mod mapping; +pub mod oidc; mod types; pub(super) fn openapi_router() -> OpenApiRouter { OpenApiRouter::new() - .routes(routes!(list, create)) - .routes(routes!(show, update, remove)) -} - -/// List identity providers -#[utoipa::path( - get, - path = "/", - params(IdentityProviderListParameters), - description = "List Identity Providers", - responses( - (status = OK, description = "List of identity providers", body = IdentityProviderList), - (status = 500, description = "Internal error", example = json!(KeystoneApiError::InternalError(String::from("id = 1")))) - ), - tag="identity_providers" -)] -#[tracing::instrument( - name = "api::identity_provider_list", - level = "debug", - skip(state, _user_auth) -)] -async fn list( - Auth(_user_auth): Auth, - Query(query): Query, - State(state): State, -) -> Result { - let providers: Vec = state - .provider - .get_federation_provider() - .list_identity_providers(&state.db, &query.try_into()?) - .await - .map_err(KeystoneApiError::federation)? - .into_iter() - .map(Into::into) - .collect(); - Ok(IdentityProviderList { providers }) -} - -/// Get single identity provider -#[utoipa::path( - get, - path = "/{idp_id}", - description = "Get IDP by ID", - params(), - responses( - (status = OK, description = "IDP object", body = IdentityProviderResponse), - (status = 404, description = "IDP not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) - ), - tag="identity_providers" -)] -#[tracing::instrument(name = "api::identity_provider_get", level = "debug", skip(state))] -async fn show( - Auth(user_auth): Auth, - Path(idp_id): Path, - State(state): State, -) -> Result { - state - .provider - .get_federation_provider() - .get_identity_provider(&state.db, &idp_id) - .await - .map(|x| { - x.ok_or_else(|| KeystoneApiError::NotFound { - resource: "identity provider".into(), - identifier: idp_id, - }) - })? -} - -/// Create identity provider -#[utoipa::path( - post, - path = "/", - description = "Create new identity provider", - responses( - (status = CREATED, description = "identity provider object", body = IdentityProviderResponse), - ), - tag="identity_providers" -)] -#[tracing::instrument(name = "api::identity_provider_create", level = "debug", skip(state))] -#[debug_handler] -async fn create( - Auth(user_auth): Auth, - State(state): State, - Json(req): Json, -) -> Result { - let res = state - .provider - .get_federation_provider() - .create_identity_provider(&state.db, req.into()) - .await - .map_err(KeystoneApiError::federation)?; - Ok((StatusCode::CREATED, res).into_response()) -} - -/// Update single identity provider -#[utoipa::path( - put, - path = "/{idp_id}", - description = "Update Identity Provider", - params(), - responses( - (status = OK, description = "IDP object", body = IdentityProviderResponse), - (status = 404, description = "IDP not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) - ), - tag="identity_providers" -)] -#[tracing::instrument(name = "api::identity_provider_update", level = "debug", skip(state))] -async fn update( - Auth(user_auth): Auth, - Path(idp_id): Path, - State(state): State, - Json(req): Json, -) -> Result { - let res = state - .provider - .get_federation_provider() - .update_identity_provider(&state.db, &idp_id, req.into()) - .await - .map_err(KeystoneApiError::federation)?; - Ok(res.into_response()) -} - -/// Delete Identity provider -#[utoipa::path( - delete, - path = "/{idp_id}", - description = "Delete identity provider by ID", - params(), - responses( - (status = 204, description = "Deleted"), - (status = 404, description = "identity provider not found", example = json!(KeystoneApiError::NotFound(String::from("id = 1")))) - ), - tag="identity_providers" -)] -#[tracing::instrument(name = "api::identity_provider_delete", level = "debug", skip(state))] -async fn remove( - Auth(user_auth): Auth, - Path(id): Path, - State(state): State, -) -> Result { - state - .provider - .get_federation_provider() - .delete_identity_provider(&state.db, &id) - .await - .map_err(KeystoneApiError::federation)?; - Ok((StatusCode::NO_CONTENT).into_response()) -} - -#[cfg(test)] -mod tests { - use axum::{ - body::Body, - http::{Request, StatusCode, header}, - }; - use http_body_util::BodyExt; // for `collect` - use sea_orm::DatabaseConnection; - - use std::sync::Arc; - use tower::ServiceExt; // for `call`, `oneshot`, and `ready` - use tower_http::trace::TraceLayer; - - use super::*; - use crate::config::Config; - use crate::federation::{ - MockFederationProvider, error::FederationProviderError, types as provider_types, - }; - use crate::keystone::{Service, ServiceState}; - use crate::provider::Provider; - use crate::token::{MockTokenProvider, Token, UnscopedToken}; - - fn get_mocked_state(federation_mock: MockFederationProvider) -> ServiceState { - let mut token_mock = MockTokenProvider::default(); - token_mock.expect_validate_token().returning(|_, _, _| { - Ok(Token::Unscoped(UnscopedToken { - user_id: "bar".into(), - ..Default::default() - })) - }); - - let provider = Provider::mocked_builder() - .federation(federation_mock) - .token(token_mock) - .build() - .unwrap(); - - Arc::new( - Service::new( - Config::default(), - DatabaseConnection::Disconnected, - provider, - ) - .unwrap(), - ) - } - - #[tokio::test] - async fn test_list() { - let mut federation_mock = MockFederationProvider::default(); - federation_mock - .expect_list_identity_providers() - .withf( - |_: &DatabaseConnection, _: &provider_types::IdentityProviderListParameters| true, - ) - .returning(|_, _| { - Ok(vec![provider_types::IdentityProvider { - id: "id".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - }]) - }); - - let state = get_mocked_state(federation_mock); - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); - assert_eq!( - vec![IdentityProvider { - id: "id".into(), - name: "name".into(), - domain_id: Some("did".into()), - oidc_discovery_url: None, - oidc_client_id: None, - oidc_response_mode: None, - oidc_response_types: None, - jwt_validation_pubkeys: None, - bound_issuer: None, - provider_config: None - }], - res.providers - ); - } - - #[tokio::test] - async fn test_list_qp() { - let mut federation_mock = MockFederationProvider::default(); - federation_mock - .expect_list_identity_providers() - .withf( - |_: &DatabaseConnection, qp: &provider_types::IdentityProviderListParameters| { - provider_types::IdentityProviderListParameters { - name: Some("name".into()), - domain_id: Some("did".into()), - } == *qp - }, - ) - .returning(|_, _| { - Ok(vec![provider_types::IdentityProvider { - id: "id".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - }]) - }); - - let state = get_mocked_state(federation_mock); - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/?name=name&domain_id=did") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let _res: IdentityProviderList = serde_json::from_slice(&body).unwrap(); - } - - #[tokio::test] - async fn test_get() { - let mut federation_mock = MockFederationProvider::default(); - federation_mock - .expect_get_identity_provider() - .withf(|_: &DatabaseConnection, id: &'_ str| id == "foo") - .returning(|_, _| Ok(None)); - - federation_mock - .expect_get_identity_provider() - .withf(|_: &DatabaseConnection, id: &'_ str| id == "bar") - .returning(|_, _| { - Ok(Some(provider_types::IdentityProvider { - id: "bar".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - })) - }); - - let state = get_mocked_state(federation_mock); - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/foo") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - let response = api - .as_service() - .oneshot( - Request::builder() - .uri("/bar") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!( - IdentityProvider { - id: "bar".into(), - name: "name".into(), - domain_id: Some("did".into()), - oidc_discovery_url: None, - oidc_client_id: None, - oidc_response_mode: None, - oidc_response_types: None, - jwt_validation_pubkeys: None, - bound_issuer: None, - provider_config: None - }, - res.identity_provider, - ); - } - - #[tokio::test] - async fn test_create() { - let mut federation_mock = MockFederationProvider::default(); - federation_mock - .expect_create_identity_provider() - .withf( - |_: &DatabaseConnection, req: &provider_types::IdentityProvider| req.name == "name", - ) - .returning(|_, _| { - Ok(provider_types::IdentityProvider { - id: "bar".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - }) - }); - - let state = get_mocked_state(federation_mock); - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let req = IdentityProviderCreateRequest { - identity_provider: IdentityProviderCreate { - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - }, - }; - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("POST") - .header(header::CONTENT_TYPE, "application/json") - .uri("/") - .header("x-auth-token", "foo") - .body(Body::from(serde_json::to_string(&req).unwrap())) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::CREATED); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); - assert_eq!(res.identity_provider.name, req.identity_provider.name); - assert_eq!( - res.identity_provider.domain_id, - req.identity_provider.domain_id - ); - } - - #[tokio::test] - async fn test_update() { - let mut federation_mock = MockFederationProvider::default(); - federation_mock - .expect_update_identity_provider() - .withf( - |_: &DatabaseConnection, - id: &'_ str, - req: &provider_types::IdentityProviderUpdate| { - id == "1" && req.name == Some("name".to_string()) - }, - ) - .returning(|_, _, _| { - Ok(provider_types::IdentityProvider { - id: "bar".into(), - name: "name".into(), - domain_id: Some("did".into()), - ..Default::default() - }) - }); - - let state = get_mocked_state(federation_mock); - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let req = IdentityProviderUpdateRequest { - identity_provider: IdentityProviderUpdate { - name: Some("name".into()), - oidc_client_id: Some(None), - ..Default::default() - }, - }; - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("PUT") - .header(header::CONTENT_TYPE, "application/json") - .uri("/1") - .header("x-auth-token", "foo") - .body(Body::from(serde_json::to_string(&req).unwrap())) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let body = response.into_body().collect().await.unwrap().to_bytes(); - let res: IdentityProviderResponse = serde_json::from_slice(&body).unwrap(); - } - - #[tokio::test] - async fn test_delete() { - let mut federation_mock = MockFederationProvider::default(); - federation_mock - .expect_delete_identity_provider() - .withf(|_: &DatabaseConnection, id: &'_ str| id == "foo") - .returning(|_, _| { - Err(FederationProviderError::IdentityProviderNotFound( - "foo".into(), - )) - }); - - federation_mock - .expect_delete_identity_provider() - .withf(|_: &DatabaseConnection, id: &'_ str| id == "bar") - .returning(|_, _| Ok(())); - - let state = get_mocked_state(federation_mock); - - let mut api = openapi_router() - .layer(TraceLayer::new_for_http()) - .with_state(state.clone()); - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("DELETE") - .uri("/foo") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NOT_FOUND); - - let response = api - .as_service() - .oneshot( - Request::builder() - .method("DELETE") - .uri("/bar") - .header("x-auth-token", "foo") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - - assert_eq!(response.status(), StatusCode::NO_CONTENT); - } + .nest("/identity_providers", identity_provider::openapi_router()) + .nest("/mappings", mapping::openapi_router()) + .merge(auth::openapi_router()) + .merge(oidc::openapi_router()) } diff --git a/src/api/v3/federation/oidc.rs b/src/api/v3/federation/oidc.rs new file mode 100644 index 00000000..2cb9bd54 --- /dev/null +++ b/src/api/v3/federation/oidc.rs @@ -0,0 +1,398 @@ +// 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 + +use axum::{Json, debug_handler, extract::State, http::StatusCode, response::IntoResponse}; +use base64::{Engine as _, engine::general_purpose::URL_SAFE}; +use eyre::WrapErr; +use serde_json::Value; +use tracing::debug; +use url::Url; +use utoipa_axum::{router::OpenApiRouter, routes}; +use uuid::Uuid; + +use openidconnect::core::{CoreGenderClaim, CoreProviderMetadata}; +use openidconnect::reqwest; +use openidconnect::{ + AuthorizationCode, ClientId, ClientSecret, IdTokenClaims, IssuerUrl, Nonce, PkceCodeVerifier, + RedirectUrl, TokenResponse, +}; + +use crate::api::v3::auth::token::types::{ + Token as ApiResponseToken, TokenResponse as KeystoneTokenResponse, +}; +use crate::api::v3::federation::error::OidcError; +use crate::api::v3::federation::types::*; +use crate::api::{Catalog, error::KeystoneApiError}; +use crate::catalog::CatalogApi; +use crate::federation::FederationApi; +use crate::federation::types::Scope as ProviderScope; +use crate::federation::types::{ + identity_provider::IdentityProvider as ProviderIdentityProvider, + mapping::Mapping as ProviderMapping, +}; +use crate::identity::IdentityApi; +use crate::identity::error::IdentityProviderError; +use crate::identity::types::{FederationBuilder, FederationProtocol, UserCreateBuilder}; +use crate::keystone::ServiceState; +use crate::resource::ResourceApi; +use crate::token::TokenApi; + +pub(super) fn openapi_router() -> OpenApiRouter { + OpenApiRouter::new().routes(routes!(callback)) +} + +/// Authenticate callback +#[utoipa::path( + post, + path = "/oidc/callback", + description = "OIDC authentication callback", + responses( + (status = OK, description = "Authentication Token object", body = KeystoneTokenResponse, + headers( + ("x-subject-token" = String, description = "Keystone token"), + ), + ), + ), + tag="identity_providers" +)] +#[tracing::instrument( + name = "api::identity_provider_auth_callback", + level = "debug", + skip(state) +)] +#[debug_handler] +pub async fn callback( + State(state): State, + Json(query): Json, +) -> Result { + let auth_state = state + .provider + .get_federation_provider() + .get_auth_state(&state.db, &query.state) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "auth state".into(), + identifier: query.state.clone(), + })?; + + let idp = state + .provider + .get_federation_provider() + .get_identity_provider(&state.db, &auth_state.idp_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "identity provider".into(), + identifier: auth_state.idp_id.clone(), + }) + })??; + + let mapping = state + .provider + .get_federation_provider() + .get_mapping(&state.db, &auth_state.mapping_id) + .await + .map(|x| { + x.ok_or_else(|| KeystoneApiError::NotFound { + resource: "mapping".into(), + identifier: auth_state.mapping_id.clone(), + }) + })??; + + debug!("Got code {:?}, state: {:?}", query.code, auth_state); + let http_client = reqwest::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .expect("Client should build"); + + let client = if let Some(discovery_url) = &idp.oidc_discovery_url { + let provider_metadata = CoreProviderMetadata::discover_async( + IssuerUrl::new(discovery_url.to_string()).map_err(OidcError::from)?, + &http_client, + ) + .await + .map_err(|err| OidcError::discovery(&err))?; + OidcClient::from_provider_metadata( + provider_metadata, + ClientId::new(idp.oidc_client_id.clone().expect("client_id is mandatory")), + idp.oidc_client_secret.clone().map(ClientSecret::new), + ) + .set_redirect_uri(RedirectUrl::new(auth_state.redirect_uri).map_err(OidcError::from)?) + } else { + return Err(OidcError::ClientWithoutDiscoveryNotSupported)?; + }; + + // Set the URL the user will be redirected to after the authorization process. + + let token_response = client + .exchange_code(AuthorizationCode::new(query.code)) + .expect("valid code") + // Set the PKCE code verifier. + .set_pkce_verifier(PkceCodeVerifier::new(auth_state.pkce_verifier)) + .request_async(&http_client) + .await + .map_err(|err| OidcError::request_token(&err))?; + + debug!("Response is {:?}", token_response); + + //// Extract the ID token claims after verifying its authenticity and nonce. + let id_token = token_response.id_token().ok_or(OidcError::NoToken)?; + let claims = id_token + .claims(&client.id_token_verifier(), &Nonce::new(auth_state.nonce)) + .map_err(OidcError::from)?; + debug!("id_token: {:?}, claims: {:?}", id_token, claims,); + if let Some(bound_issuer) = &idp.bound_issuer { + if Url::parse(bound_issuer) + .map_err(OidcError::from) + .wrap_err_with(|| { + format!( + "while parsing the mapping bound_issuer url: {}", + bound_issuer + ) + })? + == *claims.issuer().url() + {} + } + + let claims_as_json = serde_json::to_value(claims)?; + debug!("Json: {:?}", claims_as_json); + + validate_bound_claims(&mapping, claims, &claims_as_json)?; + let mapped_user_data = map_user_data(&idp, &mapping, &claims_as_json)?; + + let user = if let Some(existing_user) = state + .provider + .get_identity_provider() + .find_federated_user(&state.db, &idp.id, &mapped_user_data.unique_id) + .await? + { + // The user exists already + existing_user + + // TODO: update user? + } else { + // New user + let mut federated_user: FederationBuilder = FederationBuilder::default(); + federated_user.idp_id(idp.id); + federated_user.unique_id(mapped_user_data.unique_id.clone()); + federated_user.protocols(vec![FederationProtocol { + protocol_id: "oidc".into(), + unique_id: mapped_user_data.unique_id.clone(), + }]); + let mut user_builder: UserCreateBuilder = UserCreateBuilder::default(); + user_builder.id(String::new()); + user_builder.domain_id(mapped_user_data.domain_id); + user_builder.enabled(true); + user_builder.name(mapped_user_data.user_name); + user_builder.federated(Vec::from([federated_user + .build() + .map_err(IdentityProviderError::from)?])); + + state + .provider + .get_identity_provider() + .create_user( + &state.db, + user_builder.build().map_err(IdentityProviderError::from)?, + ) + .await? + }; + // TODO: Persist group memberships + + let (project, domain) = match &auth_state.scope { + Some(ProviderScope::Project(pid)) => ( + Some( + state + .provider + .get_resource_provider() + .get_project(&state.db, pid.as_ref()) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "project".into(), + identifier: pid.clone(), + })?, + ), + None, + ), + Some(ProviderScope::Domain(did)) => ( + None, + Some( + state + .provider + .get_resource_provider() + .get_domain(&state.db, did.as_ref()) + .await? + .ok_or_else(|| KeystoneApiError::NotFound { + resource: "domain".into(), + identifier: did.clone(), + })?, + ), + ), + _ => (None, None), + }; + let mut token = state.provider.get_token_provider().issue_token( + user.id.clone(), + Vec::from(["oidc".into()]), + Vec::::from([URL_SAFE + .encode(Uuid::new_v4().as_bytes()) + .trim_end_matches('=') + .to_string()]), + project.as_ref(), + domain.as_ref(), + )?; + + state + .provider + .get_token_provider() + .populate_role_assignments(&mut token, &state.db, &state.provider) + .await + .map_err(|_| KeystoneApiError::Forbidden)?; + + state + .provider + .get_token_provider() + .expand_project_information(&mut token, &state.db, &state.provider) + .await?; + + state + .provider + .get_token_provider() + .expand_domain_information(&mut token, &state.db, &state.provider) + .await?; + + let mut api_token = KeystoneTokenResponse { + token: ApiResponseToken::from_user_auth( + &state, + &token, + &user, + project.as_ref(), + domain.as_ref(), + ) + .await?, + }; + let catalog: Catalog = state + .provider + .get_catalog_provider() + .get_catalog(&state.db, true) + .await? + .into(); + api_token.token.catalog = Some(catalog); + + debug!("response is {:?}", api_token); + Ok(( + StatusCode::OK, + [( + "X-Subject-Token", + state.provider.get_token_provider().encode_token(&token)?, + )], + Json(api_token), + ) + .into_response()) +} + +fn validate_bound_claims( + mapping: &ProviderMapping, + claims: &IdTokenClaims, + claims_as_json: &Value, +) -> Result<(), OidcError> { + if let Some(bound_subject) = &mapping.bound_subject { + if bound_subject != claims.subject().as_str() { + return Err(OidcError::BoundSubjectMismatch { + expected: bound_subject.to_string(), + found: claims.subject().as_str().into(), + }); + } + } + if let Some(bound_audiences) = &mapping.bound_audiences { + let mut bound_audiences_match: bool = false; + for claim_audience in claims.audiences() { + if bound_audiences.iter().any(|x| x == claim_audience.as_str()) { + bound_audiences_match = true; + } + } + if !bound_audiences_match { + return Err(OidcError::BoundAudiencesMismatch { + expected: bound_audiences.join(","), + found: claims + .audiences() + .iter() + .map(|x| x.as_str()) + .collect::>() + .join(","), + }); + } + } + if let Some(bound_claims) = &mapping.bound_claims { + if let Some(required_claims) = bound_claims.as_object() { + for (claim, value) in required_claims.iter() { + if !claims_as_json + .get(claim) + .map(|x| x == value) + .is_some_and(|val| val) + { + return Err(OidcError::BoundClaimsMismatch { + claim: claim.to_string(), + expected: value.to_string(), + found: claims_as_json + .get(claim) + .map(|x| x.to_string()) + .unwrap_or_default(), + }); + } + } + } + } + Ok(()) +} + +fn map_user_data( + idp: &ProviderIdentityProvider, + mapping: &ProviderMapping, + claims_as_json: &Value, +) -> Result { + let mut builder = MappedUserDataBuilder::default(); + builder.unique_id( + claims_as_json + .get(&mapping.user_id_claim) + .and_then(|x| x.as_str()) + .ok_or_else(|| OidcError::UserIdClaimMissing(mapping.user_id_claim.clone()))?, + ); + + builder.user_name( + claims_as_json + .get(&mapping.user_name_claim) + .and_then(|x| x.as_str()) + .ok_or_else(|| OidcError::UserNameClaimMissing(mapping.user_name_claim.clone()))?, + ); + + builder.domain_id( + mapping + .domain_id + .as_ref() + .or(idp.domain_id.as_ref()) + .or(mapping + .domain_id_claim + .as_ref() + .and_then(|claim| { + claims_as_json + .get(claim) + .and_then(|x| x.as_str().map(|v| v.to_string())) + }) + .as_ref()) + .ok_or(OidcError::UserDomainUnbound)?, + ); + + Ok(builder.build()?) +} diff --git a/src/api/v3/federation/types.rs b/src/api/v3/federation/types.rs index cd052afa..1e1f1b96 100644 --- a/src/api/v3/federation/types.rs +++ b/src/api/v3/federation/types.rs @@ -11,7 +11,77 @@ // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use openidconnect::core::{ + CoreAuthDisplay, CoreAuthPrompt, CoreErrorResponseType, CoreGenderClaim, CoreJsonWebKey, + CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreRevocableToken, + CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType, +}; +use openidconnect::{ + AdditionalClaims, EndpointMaybeSet, EndpointNotSet, EndpointSet, ExtraTokenFields, + IdTokenFields, StandardErrorResponse, StandardTokenResponse, +}; + +pub mod auth; pub mod identity_provider; +pub mod mapping; +pub use auth::*; pub use identity_provider::*; +pub use mapping::*; + +pub(super) type OidcIdTokenFields = IdTokenFields< + AllOtherClaims, + ExtraFields, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJwsSigningAlgorithm, +>; + +pub(super) type OidcTokenResponse = StandardTokenResponse; + +pub(super) type OidcClient< + HasAuthUrl = EndpointSet, + HasDeviceAuthUrl = EndpointNotSet, + HasIntrospectionUrl = EndpointNotSet, + HasRevocationUrl = EndpointNotSet, + HasTokenUrl = EndpointMaybeSet, + HasUserInfoUrl = EndpointMaybeSet, +> = openidconnect::Client< + AllOtherClaims, + CoreAuthDisplay, + CoreGenderClaim, + CoreJweContentEncryptionAlgorithm, + CoreJsonWebKey, + CoreAuthPrompt, + StandardErrorResponse, + OidcTokenResponse, + CoreTokenIntrospectionResponse, + CoreRevocableToken, + CoreRevocationErrorResponse, + HasAuthUrl, + HasDeviceAuthUrl, + HasIntrospectionUrl, + HasRevocationUrl, + HasTokenUrl, + HasUserInfoUrl, +>; + +#[derive(Debug, Deserialize, Serialize)] +pub(super) struct AllOtherClaims(HashMap); +impl AdditionalClaims for AllOtherClaims {} + +#[derive(Debug, Deserialize, Serialize)] +pub(super) struct ExtraFields(HashMap); +impl ExtraTokenFields for ExtraFields {} + +#[derive(Builder, Clone)] +#[builder(setter(into))] +pub(super) struct MappedUserData { + pub(super) unique_id: String, + pub(super) user_name: String, + pub(super) domain_id: String, +} diff --git a/src/api/v3/federation/types/auth.rs b/src/api/v3/federation/types/auth.rs new file mode 100644 index 00000000..e28ba89c --- /dev/null +++ b/src/api/v3/federation/types/auth.rs @@ -0,0 +1,50 @@ +// 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 + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct IdentityProviderAuthRequest { + /// Redirect URI to include in the auth request + pub redirect_uri: String, + /// IDP mapping id + pub mapping_id: Option, + /// IDP mapping name + pub mapping_name: Option, + /// Authentication scope + pub scope: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct IdentityProviderAuthResponse { + pub auth_url: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct AuthCallbackParameters { + pub state: String, + pub code: String, +} + +impl IntoResponse for IdentityProviderAuthResponse { + fn into_response(self) -> Response { + (StatusCode::OK, Json(self)).into_response() + } +} diff --git a/src/api/v3/federation/types/identity_provider.rs b/src/api/v3/federation/types/identity_provider.rs index cf58e449..34c75fd8 100644 --- a/src/api/v3/federation/types/identity_provider.rs +++ b/src/api/v3/federation/types/identity_provider.rs @@ -59,6 +59,10 @@ pub struct IdentityProvider { #[builder(default)] pub bound_issuer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub default_mapping_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] pub provider_config: Option, @@ -107,6 +111,10 @@ pub struct IdentityProviderCreate { #[builder(default)] pub bound_issuer: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub default_mapping_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] #[builder(default)] pub provider_config: Option, @@ -138,6 +146,10 @@ pub struct IdentityProviderUpdate { #[builder(default)] pub bound_issuer: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + #[builder(default)] + pub default_mapping_name: Option>, + #[builder(default)] pub provider_config: Option>, } @@ -168,6 +180,7 @@ impl From for IdentityProvider { oidc_response_types: value.oidc_response_types, jwt_validation_pubkeys: value.jwt_validation_pubkeys, bound_issuer: value.bound_issuer, + default_mapping_name: value.default_mapping_name, provider_config: value.provider_config, } } @@ -186,6 +199,7 @@ impl From for types::IdentityProvider { oidc_response_types: value.identity_provider.oidc_response_types, jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, bound_issuer: value.identity_provider.bound_issuer, + default_mapping_name: value.identity_provider.default_mapping_name, provider_config: value.identity_provider.provider_config, } } @@ -202,6 +216,7 @@ impl From for types::IdentityProviderUpdate { oidc_response_types: value.identity_provider.oidc_response_types, jwt_validation_pubkeys: value.identity_provider.jwt_validation_pubkeys, bound_issuer: value.identity_provider.bound_issuer, + default_mapping_name: value.identity_provider.default_mapping_name, provider_config: value.identity_provider.provider_config, } } @@ -229,7 +244,7 @@ impl From for KeystoneApiError { #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] pub struct IdentityProviderList { /// Collection of identity provider objects - pub providers: Vec, + pub identity_providers: Vec, } impl IntoResponse for IdentityProviderList { diff --git a/src/api/v3/federation/types/mapping.rs b/src/api/v3/federation/types/mapping.rs new file mode 100644 index 00000000..e2a41960 --- /dev/null +++ b/src/api/v3/federation/types/mapping.rs @@ -0,0 +1,358 @@ +// 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 + +use axum::{ + Json, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use utoipa::{IntoParams, ToSchema}; + +use crate::api::error::KeystoneApiError; +use crate::federation::types; + +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct Mapping { + /// Federation mapping ID + pub id: String, + + /// Mapping name + pub name: String, + + /// domain_id of the mapping + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub domain_id: Option, + + /// IDP ID + pub idp_id: String, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_redirect_uris: Option>, + + pub user_id_claim: String, + pub user_name_claim: String, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub domain_id_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub groups_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_audiences: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_subject: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_claims: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub oidc_scopes: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_user_id: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_role_ids: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_project_id: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct MappingResponse { + /// IDP object + pub mapping: Mapping, +} + +#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct MappingCreate { + /// Mapping name + pub name: String, + + /// domain_id of the mapping + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub domain_id: Option, + + /// IDP ID + pub idp_id: String, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_redirect_uris: Option>, + + pub user_id_claim: String, + pub user_name_claim: String, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub domain_id_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub groups_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_audiences: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_subject: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_claims: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub oidc_scopes: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_user_id: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_role_ids: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_project_id: Option, +} + +#[derive(Builder, Clone, Default, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(into))] +pub struct MappingUpdate { + /// Mapping name + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + + /// domain_id of the mapping + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub domain_id: Option>, + + /// IDP ID + #[serde(skip_serializing_if = "Option::is_none")] + pub idp_id: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub allowed_redirect_uris: Option>>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub user_id_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub user_name_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub domain_id_claim: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub groups_claim: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_audiences: Option>>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_subject: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub bound_claims: Option, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub oidc_scopes: Option>>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_user_id: Option>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_role_ids: Option>>, + + #[builder(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub token_project_id: Option>, +} + +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct MappingCreateRequest { + /// Mapping object + pub mapping: MappingCreate, +} + +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +#[builder(setter(strip_option, into))] +pub struct MappingUpdateRequest { + /// Mapping object + pub mapping: MappingUpdate, +} + +impl From for Mapping { + fn from(value: types::Mapping) -> Self { + Self { + id: value.id, + name: value.name, + domain_id: value.domain_id, + idp_id: value.idp_id, + allowed_redirect_uris: value.allowed_redirect_uris, + user_id_claim: value.user_id_claim, + user_name_claim: value.user_name_claim, + domain_id_claim: value.domain_id_claim, + groups_claim: value.groups_claim, + bound_audiences: value.bound_audiences, + bound_subject: value.bound_subject, + bound_claims: value.bound_claims, + oidc_scopes: value.oidc_scopes, + token_user_id: value.token_user_id, + token_role_ids: value.token_role_ids, + token_project_id: value.token_project_id, + } + } +} + +impl From for types::Mapping { + fn from(value: MappingCreateRequest) -> Self { + Self { + id: String::new(), + name: value.mapping.name, + domain_id: value.mapping.domain_id, + idp_id: value.mapping.idp_id, + allowed_redirect_uris: value.mapping.allowed_redirect_uris, + user_id_claim: value.mapping.user_id_claim, + user_name_claim: value.mapping.user_name_claim, + domain_id_claim: value.mapping.domain_id_claim, + groups_claim: value.mapping.groups_claim, + bound_audiences: value.mapping.bound_audiences, + bound_subject: value.mapping.bound_subject, + bound_claims: value.mapping.bound_claims, + oidc_scopes: value.mapping.oidc_scopes, + token_user_id: value.mapping.token_user_id, + token_role_ids: value.mapping.token_role_ids, + token_project_id: value.mapping.token_project_id, + } + } +} + +impl From for types::MappingUpdate { + fn from(value: MappingUpdateRequest) -> Self { + Self { + name: value.mapping.name, + idp_id: value.mapping.idp_id, + allowed_redirect_uris: value.mapping.allowed_redirect_uris, + user_id_claim: value.mapping.user_id_claim, + user_name_claim: value.mapping.user_name_claim, + domain_id_claim: value.mapping.domain_id_claim, + groups_claim: value.mapping.groups_claim, + bound_audiences: value.mapping.bound_audiences, + bound_subject: value.mapping.bound_subject, + bound_claims: value.mapping.bound_claims, + oidc_scopes: value.mapping.oidc_scopes, + token_user_id: value.mapping.token_user_id, + token_role_ids: value.mapping.token_role_ids, + token_project_id: value.mapping.token_project_id, + } + } +} + +impl IntoResponse for types::Mapping { + fn into_response(self) -> Response { + ( + StatusCode::OK, + Json(MappingResponse { + mapping: Mapping::from(self), + }), + ) + .into_response() + } +} + +impl From for KeystoneApiError { + fn from(err: MappingBuilderError) -> Self { + Self::InternalError(err.to_string()) + } +} + +/// Identity Providers +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct MappingList { + /// Collection of identity provider objects + pub mappings: Vec, +} + +impl IntoResponse for MappingList { + fn into_response(self) -> Response { + (StatusCode::OK, Json(self)).into_response() + } +} + +/// List identity provider query parameters +#[derive(Clone, Debug, Default, Deserialize, Serialize, IntoParams)] +pub struct MappingListParameters { + /// Filters the response by IDP name. + pub name: Option, + + /// Filters the response by a domain ID. + pub domain_id: Option, + /// Filters the response by a idp ID. + pub idp_id: Option, +} + +impl From for KeystoneApiError { + fn from(err: types::MappingListParametersBuilderError) -> Self { + Self::InternalError(err.to_string()) + } +} + +impl TryFrom for types::MappingListParameters { + type Error = KeystoneApiError; + + fn try_from(value: MappingListParameters) -> Result { + Ok(Self { + name: value.name, + domain_id: value.domain_id, + idp_id: value.idp_id, + }) + } +} diff --git a/src/api/v3/mod.rs b/src/api/v3/mod.rs index 063a2b2e..08a7d536 100644 --- a/src/api/v3/mod.rs +++ b/src/api/v3/mod.rs @@ -35,10 +35,7 @@ pub(super) fn openapi_router() -> OpenApiRouter { OpenApiRouter::new() .nest("/auth", auth::openapi_router()) .nest("/groups", group::openapi_router()) - .nest( - "/federation/identity_providers", - federation::openapi_router(), - ) + .nest("/federation", federation::openapi_router()) .nest("/role_assignments", role_assignment::openapi_router()) .nest("/roles", role::openapi_router()) .nest("/users", user::openapi_router()) diff --git a/src/api/v3/role_assignment/types.rs b/src/api/v3/role_assignment/types.rs index c30bcf7e..64277aa2 100644 --- a/src/api/v3/role_assignment/types.rs +++ b/src/api/v3/role_assignment/types.rs @@ -41,6 +41,7 @@ pub struct Assignment { #[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] pub struct Role { pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, } @@ -64,11 +65,17 @@ pub struct Domain { pub id: String, } +#[derive(Builder, Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] +pub struct System { + pub id: String, +} + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum Scope { Project(Project), Domain(Domain), + System(System), } impl TryFrom for Assignment { @@ -113,6 +120,22 @@ impl TryFrom for Assignment { id: value.target_id.clone(), })); } + types::AssignmentType::UserSystem => { + builder.user(User { + id: value.actor_id.clone(), + }); + builder.scope(Scope::System(System { + id: value.target_id.clone(), + })); + } + types::AssignmentType::GroupSystem => { + builder.group(Group { + id: value.actor_id.clone(), + }); + builder.scope(Scope::System(System { + id: value.target_id.clone(), + })); + } } Ok(builder.build()?) } @@ -169,6 +192,13 @@ pub struct RoleAssignmentListParameters { /// Filters the response by a user ID. #[serde(rename = "user.id")] pub user_id: Option, + + /// If set to true, then the names of any entities returned will be include as well as their + /// IDs. Any value other than 0 (including no value) will be interpreted as true. + /// + /// New in version 3.6 + #[serde(default)] + pub include_names: Option, } impl TryFrom for types::RoleAssignmentListParameters { @@ -198,6 +228,9 @@ impl TryFrom for types::RoleAssignmentListParamete if let Some(val) = value.effective { builder.effective(val); } + if let Some(val) = value.include_names { + builder.include_names(val); + } Ok(builder.build()?) } } diff --git a/src/assignment/backends/error.rs b/src/assignment/backends/error.rs index 094c6ecf..6f57e65d 100644 --- a/src/assignment/backends/error.rs +++ b/src/assignment/backends/error.rs @@ -44,4 +44,7 @@ pub enum AssignmentDatabaseError { #[from] source: sea_orm::DbErr, }, + + #[error("invalid assignment type: {0}")] + InvalidAssignmentType(String), } diff --git a/src/assignment/backends/sql/assignment.rs b/src/assignment/backends/sql/assignment.rs index 7dda79a4..801f54fe 100644 --- a/src/assignment/backends/sql/assignment.rs +++ b/src/assignment/backends/sql/assignment.rs @@ -23,9 +23,10 @@ use crate::assignment::types::*; use crate::config::Config; use crate::db::entity::{ assignment as db_assignment, - prelude::{Assignment as DbAssignment, Role as DbRole}, + prelude::{Assignment as DbAssignment, Role as DbRole, SystemAssignment as DbSystemAssignment}, role as db_role, sea_orm_active_enums::Type as DbAssignmentType, + system_assignment as db_system_assignment, }; pub async fn list( @@ -33,28 +34,83 @@ pub async fn list( db: &DatabaseConnection, params: &RoleAssignmentListParameters, ) -> Result, AssignmentDatabaseError> { - let mut select = DbAssignment::find(); + let mut select_assignment = DbAssignment::find(); + let mut select_system_assignment = DbSystemAssignment::find(); if let Some(val) = ¶ms.role_id { - select = select.filter(db_assignment::Column::RoleId.eq(val)); + select_assignment = select_assignment.filter(db_assignment::Column::RoleId.eq(val)); + select_system_assignment = + select_system_assignment.filter(db_system_assignment::Column::RoleId.eq(val)); } if let Some(val) = ¶ms.user_id { - select = select.filter(db_assignment::Column::ActorId.eq(val)); + select_assignment = select_assignment.filter(db_assignment::Column::ActorId.eq(val)); + select_system_assignment = + select_system_assignment.filter(db_system_assignment::Column::ActorId.eq(val)); } else if let Some(val) = ¶ms.group_id { - select = select.filter(db_assignment::Column::ActorId.eq(val)); + select_assignment = select_assignment.filter(db_assignment::Column::ActorId.eq(val)); + select_system_assignment = + select_system_assignment.filter(db_system_assignment::Column::ActorId.eq(val)); } if let Some(val) = ¶ms.project_id { - select = select.filter(db_assignment::Column::TargetId.eq(val)); + select_assignment = select_assignment + .filter(db_assignment::Column::TargetId.eq(val)) + .filter(db_assignment::Column::Type.is_in([ + DbAssignmentType::UserProject, + DbAssignmentType::GroupProject, + ])); } else if let Some(val) = ¶ms.domain_id { - select = select.filter(db_assignment::Column::TargetId.eq(val)); + select_assignment = select_assignment + .filter(db_assignment::Column::TargetId.eq(val)) + .filter( + db_assignment::Column::Type + .is_in([DbAssignmentType::UserDomain, DbAssignmentType::GroupDomain]), + ); + } else { + select_system_assignment = + select_system_assignment.filter(db_system_assignment::Column::TargetId.eq("system")); } - let db_entities: Vec = select.all(db).await?; - let results: Result, _> = db_entities - .into_iter() - .map(TryInto::::try_into) - .collect(); - + let results: Result, _> = if let Some(true) = ¶ms.include_names { + let db_assignments: Vec<(db_assignment::Model, Option)> = + select_assignment.find_also_related(DbRole).all(db).await?; + let db_system_assignments: Vec<(db_system_assignment::Model, Option)> = + if params.project_id.is_none() && params.domain_id.is_none() { + // get system scope assignments only when no project or domain is specified + select_system_assignment + .find_also_related(DbRole) + .all(db) + .await? + } else { + Vec::new() + }; + db_assignments + .into_iter() + .map(|item| TryInto::::try_into((item.0, item.1))) + .chain( + db_system_assignments + .into_iter() + .map(|item| TryInto::::try_into((item.0, item.1))), + ) + .collect() + } else { + let db_assignments: Vec = select_assignment.all(db).await?; + let db_system_assignments: Vec = + if params.project_id.is_none() && params.domain_id.is_none() { + // get system scope assignments only when no project or domain is specified + select_system_assignment.all(db).await? + } else { + Vec::new() + }; + db_assignments + .into_iter() + .map(TryInto::::try_into) + .chain( + db_system_assignments + .into_iter() + .map(TryInto::::try_into), + ) + .collect() + }; results } @@ -138,7 +194,22 @@ impl TryFrom for Assignment { builder.actor_id(value.actor_id.clone()); builder.target_id(value.target_id.clone()); builder.inherited(value.inherited); - builder.r#type(value.r#type); + builder.r#type(AssignmentType::try_from(value.r#type)?); + + Ok(builder.build()?) + } +} + +impl TryFrom for Assignment { + type Error = AssignmentDatabaseError; + + fn try_from(value: db_system_assignment::Model) -> Result { + let mut builder = AssignmentBuilder::default(); + builder.role_id(value.role_id.clone()); + builder.actor_id(value.actor_id.clone()); + builder.target_id(value.target_id.clone()); + builder.inherited(value.inherited); + builder.r#type(AssignmentType::try_from(value.r#type.as_ref())?); Ok(builder.build()?) } @@ -153,7 +224,7 @@ impl TryFrom<&db_assignment::Model> for Assignment { builder.actor_id(value.actor_id.clone()); builder.target_id(value.target_id.clone()); builder.inherited(value.inherited); - builder.r#type(value.r#type.clone()); + builder.r#type(AssignmentType::try_from(value.r#type.clone())?); Ok(builder.build()?) } @@ -168,7 +239,7 @@ impl TryFrom<(&db_assignment::Model, Option<&String>)> for Assignment { builder.actor_id(value.0.actor_id.clone()); builder.target_id(value.0.target_id.clone()); builder.inherited(value.0.inherited); - builder.r#type(value.0.r#type.clone()); + builder.r#type(AssignmentType::try_from(value.0.r#type.clone())?); if let Some(val) = value.1 { builder.role_name(val.clone()); } @@ -188,7 +259,27 @@ impl TryFrom<(db_assignment::Model, Option)> for Assignment { builder.actor_id(value.0.actor_id.clone()); builder.target_id(value.0.target_id.clone()); builder.inherited(value.0.inherited); - builder.r#type(value.0.r#type); + builder.r#type(AssignmentType::try_from(value.0.r#type)?); + if let Some(val) = &value.1 { + builder.role_name(val.name.clone()); + } + + Ok(builder.build()?) + } +} + +impl TryFrom<(db_system_assignment::Model, Option)> for Assignment { + type Error = AssignmentDatabaseError; + + fn try_from( + value: (db_system_assignment::Model, Option), + ) -> Result { + let mut builder = AssignmentBuilder::default(); + builder.role_id(value.0.role_id.clone()); + builder.actor_id(value.0.actor_id.clone()); + builder.target_id(value.0.target_id.clone()); + builder.inherited(value.0.inherited); + builder.r#type(AssignmentType::try_from(value.0.r#type.as_ref())?); if let Some(val) = &value.1 { builder.role_name(val.name.clone()); } @@ -197,13 +288,24 @@ impl TryFrom<(db_assignment::Model, Option)> for Assignment { } } -impl From for AssignmentType { - fn from(value: DbAssignmentType) -> Self { +impl TryFrom for AssignmentType { + type Error = AssignmentDatabaseError; + fn try_from(value: DbAssignmentType) -> Result { match value { - DbAssignmentType::GroupDomain => Self::GroupDomain, - DbAssignmentType::GroupProject => Self::GroupProject, - DbAssignmentType::UserDomain => Self::UserDomain, - DbAssignmentType::UserProject => Self::UserProject, + DbAssignmentType::GroupDomain => Ok(Self::GroupDomain), + DbAssignmentType::GroupProject => Ok(Self::GroupProject), + DbAssignmentType::UserDomain => Ok(Self::UserDomain), + DbAssignmentType::UserProject => Ok(Self::UserProject), + } + } +} + +impl TryFrom<&str> for AssignmentType { + type Error = AssignmentDatabaseError; + fn try_from(value: &str) -> Result { + match value { + "UserSystem" => Ok(Self::UserSystem), + _ => Err(AssignmentDatabaseError::InvalidAssignmentType(value.into())), } } } @@ -214,7 +316,9 @@ mod tests { use std::collections::BTreeMap; use crate::config::Config; - use crate::db::entity::{assignment, implied_role, role, sea_orm_active_enums}; + use crate::db::entity::{ + assignment, implied_role, role, sea_orm_active_enums, system_assignment, + }; use super::*; @@ -228,6 +332,16 @@ mod tests { } } + fn get_role_system_assignment_mock(role_id: String) -> system_assignment::Model { + system_assignment::Model { + role_id: role_id.clone(), + actor_id: "actor".into(), + target_id: "system".into(), + r#type: "UserSystem".into(), + inherited: false, + } + } + fn get_role_mock(role_id: String, role_name: String) -> BTreeMap { BTreeMap::from([ ("id".to_string(), Value::String(Some(Box::new(role_id)))), @@ -261,92 +375,221 @@ mod tests { ) } + fn get_role_system_assignment_with_role_mock( + role_id: String, + ) -> (system_assignment::Model, role::Model) { + ( + system_assignment::Model { + role_id: role_id.clone(), + actor_id: "actor".into(), + target_id: "system".into(), + r#type: "UserSystem".into(), + inherited: false, + }, + role::Model { + id: role_id.clone(), + name: role_id.clone(), + extra: None, + domain_id: String::new(), + description: None, + }, + ) + } + #[tokio::test] - async fn test_list() { + async fn test_list_no_params() { // Create MockDatabase with mock query results let db = MockDatabase::new(DatabaseBackend::Postgres) .append_query_results([vec![get_role_assignment_mock("1".into())]]) - .append_query_results([vec![get_role_assignment_mock("1".into())]]) - .append_query_results([vec![get_role_assignment_mock("1".into())]]) - .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .append_query_results([vec![get_role_system_assignment_mock("1".into())]]) .into_connection(); let config = Config::default(); assert_eq!( list(&config, &db, &RoleAssignmentListParameters::default()) .await .unwrap(), - vec![Assignment { - role_id: "1".into(), - role_name: None, - actor_id: "actor".into(), - target_id: "target".into(), - r#type: AssignmentType::UserProject, - inherited: false, - }] + vec![ + Assignment { + role_id: "1".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "target".into(), + r#type: AssignmentType::UserProject, + inherited: false, + }, + Assignment { + role_id: "1".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "system".into(), + r#type: AssignmentType::UserSystem, + inherited: false, + } + ] ); - assert!( + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "system_assignment"."type", "system_assignment"."actor_id", "system_assignment"."target_id", "system_assignment"."role_id", "system_assignment"."inherited" FROM "system_assignment" WHERE "system_assignment"."target_id" = $1"#, + ["system".into()] + ), + ] + ); + } + + #[tokio::test] + async fn test_list_role_id() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .append_query_results([vec![get_role_system_assignment_mock("1".into())]]) + .into_connection(); + let config = Config::default(); + assert_eq!( list( &config, &db, &RoleAssignmentListParameters { - role_id: Some("foo".into()), + role_id: Some("1".into()), ..Default::default() } ) .await - .is_ok() + .unwrap(), + vec![ + Assignment { + role_id: "1".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "target".into(), + r#type: AssignmentType::UserProject, + inherited: false, + }, + Assignment { + role_id: "1".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "system".into(), + r#type: AssignmentType::UserSystem, + inherited: false, + } + ] ); - assert!( + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."role_id" = $1"#, + ["1".into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "system_assignment"."type", "system_assignment"."actor_id", "system_assignment"."target_id", "system_assignment"."role_id", "system_assignment"."inherited" FROM "system_assignment" WHERE "system_assignment"."role_id" = $1 AND "system_assignment"."target_id" = $2"#, + ["1".into(), "system".into()] + ), + ] + ); + } + + #[tokio::test] + async fn test_list_project_id() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_role_assignment_mock("1".into())]]) + .into_connection(); + let config = Config::default(); + assert_eq!( list( &config, &db, &RoleAssignmentListParameters { - role_id: Some("foo".into()), - group_id: Some("actor".into()), + project_id: Some("target".into()), ..Default::default() } ) .await - .is_ok() + .unwrap(), + vec![Assignment { + role_id: "1".into(), + role_name: None, + actor_id: "actor".into(), + target_id: "target".into(), + r#type: AssignmentType::UserProject, + inherited: false, + }] ); - assert!( + // Checking transaction log + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."target_id" = $1 AND "assignment"."type" IN (CAST($2 AS "type"), CAST($3 AS "type"))"#, + ["target".into(), "UserProject".into(), "GroupProject".into()] + ),] + ); + } + + #[tokio::test] + async fn test_list_include_names() { + // Create MockDatabase with mock query results + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_role_assignment_with_role_mock("1".into())]]) + .append_query_results([vec![get_role_system_assignment_with_role_mock("1".into())]]) + .into_connection(); + let config = Config::default(); + assert_eq!( list( &config, &db, &RoleAssignmentListParameters { - role_id: Some("foo".into()), - user_id: Some("actor".into()), - project_id: Some("target".into()), + include_names: Some(true), ..Default::default() } ) .await - .is_ok() + .unwrap(), + vec![ + Assignment { + role_id: "1".into(), + role_name: Some("1".into()), + actor_id: "actor".into(), + target_id: "target".into(), + r#type: AssignmentType::UserProject, + inherited: false, + }, + Assignment { + role_id: "1".into(), + role_name: Some("1".into()), + actor_id: "actor".into(), + target_id: "system".into(), + r#type: AssignmentType::UserSystem, + inherited: false, + } + ] ); - // Checking transaction log assert_eq!( db.into_transaction_log(), [ Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment""#, + r#"SELECT CAST("assignment"."type" AS "text") AS "A_type", "assignment"."actor_id" AS "A_actor_id", "assignment"."target_id" AS "A_target_id", "assignment"."role_id" AS "A_role_id", "assignment"."inherited" AS "A_inherited", "role"."id" AS "B_id", "role"."name" AS "B_name", "role"."extra" AS "B_extra", "role"."domain_id" AS "B_domain_id", "role"."description" AS "B_description" FROM "assignment" LEFT JOIN "role" ON "assignment"."role_id" = "role"."id""#, [] ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."role_id" = $1"#, - ["foo".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."role_id" = $1 AND "assignment"."actor_id" = $2"#, - ["foo".into(), "actor".into()] - ), - Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."role_id" = $1 AND "assignment"."actor_id" = $2 AND "assignment"."target_id" = $3"#, - ["foo".into(), "actor".into(), "target".into()] + r#"SELECT "system_assignment"."type" AS "A_type", "system_assignment"."actor_id" AS "A_actor_id", "system_assignment"."target_id" AS "A_target_id", "system_assignment"."role_id" AS "A_role_id", "system_assignment"."inherited" AS "A_inherited", "role"."id" AS "B_id", "role"."name" AS "B_name", "role"."extra" AS "B_extra", "role"."domain_id" AS "B_domain_id", "role"."description" AS "B_description" FROM "system_assignment" LEFT JOIN "role" ON "system_assignment"."role_id" = "role"."id" WHERE "system_assignment"."target_id" = $1"#, + ["system".into()] ), ] ); @@ -409,7 +652,7 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."actor_id" IN ($1, $2, $3) AND "assignment"."role_id" = $4 AND "assignment"."target_id" = $5"#, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."actor_id" IN ($1, $2, $3) AND "assignment"."role_id" = $4 AND "assignment"."target_id" = $5"#, [ "uid1".into(), "gid1".into(), @@ -475,7 +718,7 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."actor_id" IN ($1, $2, $3) AND ("assignment"."target_id" = $4 OR ("assignment"."target_id" = $5 AND "assignment"."inherited" = $6))"#, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."actor_id" IN ($1, $2, $3) AND ("assignment"."target_id" = $4 OR ("assignment"."target_id" = $5 AND "assignment"."inherited" = $6))"#, [ "uid1".into(), "gid1".into(), @@ -533,7 +776,7 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment""#, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment""#, [] ), Transaction::from_sql_and_values( @@ -594,7 +837,7 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."target_id" = $1 OR ("assignment"."target_id" = $2 AND "assignment"."inherited" = $3)"#, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE "assignment"."target_id" = $1 OR ("assignment"."target_id" = $2 AND "assignment"."inherited" = $3)"#, ["pid1".into(), "pid2".into(), true.into()] ), Transaction::from_sql_and_values( @@ -651,7 +894,7 @@ mod tests { ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT CAST("assignment"."type" AS text), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE ("assignment"."target_id" = $1 AND "assignment"."inherited" = $2) OR ("assignment"."target_id" = $3 AND "assignment"."inherited" = $4)"#, + r#"SELECT CAST("assignment"."type" AS "text"), "assignment"."actor_id", "assignment"."target_id", "assignment"."role_id", "assignment"."inherited" FROM "assignment" WHERE ("assignment"."target_id" = $1 AND "assignment"."inherited" = $2) OR ("assignment"."target_id" = $3 AND "assignment"."inherited" = $4)"#, ["pid1".into(), false.into(), "pid2".into(), true.into()] ), ] diff --git a/src/assignment/mod.rs b/src/assignment/mod.rs index c5d06d07..9336971a 100644 --- a/src/assignment/mod.rs +++ b/src/assignment/mod.rs @@ -44,7 +44,7 @@ pub trait AssignmentApi: Send + Sync + Clone { &self, db: &DatabaseConnection, params: &RoleListParameters, - ) -> Result, AssignmentProviderError>; + ) -> Result, AssignmentProviderError>; /// Get a single role async fn get_role<'a>( @@ -59,7 +59,7 @@ pub trait AssignmentApi: Send + Sync + Clone { db: &DatabaseConnection, provider: &Provider, params: &RoleAssignmentListParameters, - ) -> Result, AssignmentProviderError>; + ) -> Result, AssignmentProviderError>; } #[cfg(test)] @@ -127,7 +127,7 @@ impl AssignmentApi for AssignmentProvider { &self, db: &DatabaseConnection, params: &RoleListParameters, - ) -> Result, AssignmentProviderError> { + ) -> Result, AssignmentProviderError> { self.backend_driver.list_roles(db, params).await } @@ -148,32 +148,39 @@ impl AssignmentApi for AssignmentProvider { db: &DatabaseConnection, provider: &Provider, params: &RoleAssignmentListParameters, - ) -> Result, AssignmentProviderError> { - let mut request = RoleAssignmentListForMultipleActorTargetParametersBuilder::default(); - let mut actors: Vec = Vec::new(); - let mut targets: Vec = Vec::new(); - if let Some(role_id) = ¶ms.role_id { - request.role_id(role_id); - } + ) -> Result, AssignmentProviderError> { if let Some(true) = ¶ms.effective { + let mut request = RoleAssignmentListForMultipleActorTargetParametersBuilder::default(); + let mut actors: Vec = Vec::new(); + let mut targets: Vec = Vec::new(); + if let Some(role_id) = ¶ms.role_id { + request.role_id(role_id); + } if let Some(uid) = ¶ms.user_id { - let users = provider - .get_identity_provider() - .list_groups_for_user(db, uid) - .await?; - actors.extend(users.into_iter().map(|x| x.id)); - }; - } - if let Some(val) = ¶ms.project_id { - targets.push(RoleAssignmentTarget { - target_id: val.clone(), - ..Default::default() - }); + actors.push(uid.into()); + } + if let Some(true) = ¶ms.effective { + if let Some(uid) = ¶ms.user_id { + let users = provider + .get_identity_provider() + .list_groups_for_user(db, uid) + .await?; + actors.extend(users.into_iter().map(|x| x.id)); + }; + } + if let Some(val) = ¶ms.project_id { + targets.push(RoleAssignmentTarget { + target_id: val.clone(), + ..Default::default() + }); + } + request.targets(targets); + request.actors(actors); + self.backend_driver + .list_assignments_for_multiple_actors_and_targets(db, &request.build()?) + .await + } else { + self.backend_driver.list_assignments(db, params).await } - request.targets(targets); - request.actors(actors); - self.backend_driver - .list_assignments_for_multiple_actors_and_targets(db, &request.build()?) - .await } } diff --git a/src/assignment/types/assignment.rs b/src/assignment/types/assignment.rs index 11dfbbd8..b43278ac 100644 --- a/src/assignment/types/assignment.rs +++ b/src/assignment/types/assignment.rs @@ -41,6 +41,8 @@ pub enum AssignmentType { GroupProject, UserDomain, UserProject, + UserSystem, + GroupSystem, } /// Parameters for listing role assignments for role/target/actor @@ -74,6 +76,13 @@ pub struct RoleAssignmentListParameters { /// membership. #[builder(default)] pub effective: Option, + + /// If set to true, then the names of any entities returned will be include as well as their + /// IDs. Any value other than 0 (including no value) will be interpreted as true. + /// + /// New in version 3.6 + #[builder(default)] + pub include_names: Option, } /// Querying effective role assignments for list of actors (typically user with all groups user is diff --git a/src/db/entity.rs b/src/db/entity.rs index efbfed0c..fcdac9d4 100644 --- a/src/db/entity.rs +++ b/src/db/entity.rs @@ -29,7 +29,9 @@ pub mod credential; pub mod endpoint; pub mod endpoint_group; pub mod expiring_user_group_membership; +pub mod federated_auth_state; pub mod federated_identity_provider; +pub mod federated_mapping; pub mod federated_user; pub mod federation_protocol; pub mod group; @@ -148,7 +150,33 @@ impl Default for federated_identity_provider::Model { oidc_response_types: None, jwt_validation_pubkeys: None, bound_issuer: None, + default_mapping_name: None, provider_config: None, } } } + +//impl Default for federated_mapping::Model { +// fn default() -> Self { +// Self { +// id: String::new(), +// name: String::new(), +// idp_id: String::new(), +// domain_id: None, +// allowed_redirect_uris: None, +// user_id_claim: String::new(), +// user_name_claim: String::new(), +// domain_id_claim: None, +// //user_claim_json_pointer: None, +// groups_claim: None, +// bound_audiences: None, +// bound_subject: None, +// bound_claims: None, +// oidc_scopes: None, +// claim_mappings: None, +// token_user_id: None, +// token_role_ids: None, +// token_project_id: None, +// } +// } +//} diff --git a/src/db/entity/federated_auth_state.rs b/src/db/entity/federated_auth_state.rs new file mode 100644 index 00000000..88d4f02f --- /dev/null +++ b/src/db/entity/federated_auth_state.rs @@ -0,0 +1,51 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.7 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "federated_auth_state")] +pub struct Model { + pub idp_id: String, + pub mapping_id: String, + #[sea_orm(primary_key, auto_increment = false)] + pub state: String, + pub nonce: String, + pub redirect_uri: String, + pub pkce_verifier: String, + pub started_at: DateTime, + pub requested_scope: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::federated_identity_provider::Entity", + from = "Column::IdpId", + to = "super::federated_identity_provider::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + FederatedIdentityProvider, + #[sea_orm( + belongs_to = "super::federated_mapping::Entity", + from = "Column::MappingId", + to = "super::federated_mapping::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + FederatedMapping, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::FederatedIdentityProvider.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::FederatedMapping.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/db/entity/federated_identity_provider.rs b/src/db/entity/federated_identity_provider.rs index c8b4961f..06e2b941 100644 --- a/src/db/entity/federated_identity_provider.rs +++ b/src/db/entity/federated_identity_provider.rs @@ -17,6 +17,7 @@ pub struct Model { #[sea_orm(column_type = "Text", nullable)] pub jwt_validation_pubkeys: Option, pub bound_issuer: Option, + pub default_mapping_name: Option, pub provider_config: Option, } diff --git a/src/db/entity/federated_mapping.rs b/src/db/entity/federated_mapping.rs new file mode 100644 index 00000000..5fde51af --- /dev/null +++ b/src/db/entity/federated_mapping.rs @@ -0,0 +1,61 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.7 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "federated_mapping")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: String, + pub name: String, + pub idp_id: String, + pub domain_id: Option, + pub allowed_redirect_uris: Option, + pub user_id_claim: String, + pub user_name_claim: String, + pub domain_id_claim: Option, + //pub user_claim_json_pointer: Option, + pub groups_claim: Option, + pub bound_audiences: Option, + pub bound_subject: Option, + pub bound_claims: Option, + pub oidc_scopes: Option, + //pub claim_mappings: Option, + pub token_user_id: Option, + pub token_role_ids: Option, + pub token_project_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::federated_identity_provider::Entity", + from = "Column::IdpId", + to = "super::federated_identity_provider::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + FederatedIdentityProvider, + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::DomainId", + to = "super::project::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Project, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::FederatedIdentityProvider.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/db/entity/federation_protocol.rs b/src/db/entity/federation_protocol.rs index 22dc9b2c..fc361280 100644 --- a/src/db/entity/federation_protocol.rs +++ b/src/db/entity/federation_protocol.rs @@ -25,9 +25,6 @@ pub struct Model { pub idp_id: String, pub mapping_id: String, pub remote_id_attribute: Option, - pub oidc_client_id: Option, - pub oidc_client_secret: Option, - pub oidc_provider_metadata_url: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/db/entity/mapping.rs b/src/db/entity/mapping.rs index ea1b8a85..28830108 100644 --- a/src/db/entity/mapping.rs +++ b/src/db/entity/mapping.rs @@ -24,9 +24,6 @@ pub struct Model { #[sea_orm(column_type = "Text", nullable)] pub rules: Option, pub schema_version: String, - pub oidc_user_claim: Option, - pub oidc_groups_claim: Option, - pub oidc_scopes: Option, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/src/db/entity/prelude.rs b/src/db/entity/prelude.rs index 8a107844..ab8af09a 100644 --- a/src/db/entity/prelude.rs +++ b/src/db/entity/prelude.rs @@ -28,7 +28,9 @@ pub use super::credential::Entity as Credential; pub use super::endpoint::Entity as Endpoint; pub use super::endpoint_group::Entity as EndpointGroup; pub use super::expiring_user_group_membership::Entity as ExpiringUserGroupMembership; +pub use super::federated_auth_state::Entity as FederatedAuthState; pub use super::federated_identity_provider::Entity as FederatedIdentityProvider; +pub use super::federated_mapping::Entity as FederatedMapping; pub use super::federated_user::Entity as FederatedUser; pub use super::federation_protocol::Entity as FederationProtocol; pub use super::group::Entity as Group; diff --git a/src/db/entity/system_assignment.rs b/src/db/entity/system_assignment.rs index 9bf82346..8224040b 100644 --- a/src/db/entity/system_assignment.rs +++ b/src/db/entity/system_assignment.rs @@ -32,6 +32,19 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::role::Entity", + from = "Column::RoleId", + to = "super::role::Column::Id" + )] + Role, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Role.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/src/federation/backends/error.rs b/src/federation/backends/error.rs index acdb05ae..d40ce072 100644 --- a/src/federation/backends/error.rs +++ b/src/federation/backends/error.rs @@ -33,9 +33,27 @@ pub enum FederationDatabaseError { #[error("identity provider {0} not found")] IdentityProviderNotFound(String), + #[error("mapping provider {0} not found")] + MappingNotFound(String), + + #[error("auth state {0} not found")] + AuthStateNotFound(String), + + #[error(transparent)] + AuthStateBuilder { + #[from] + source: AuthStateBuilderError, + }, + #[error(transparent)] IdentityProviderBuilder { #[from] source: IdentityProviderBuilderError, }, + + #[error(transparent)] + MappingBuilder { + #[from] + source: MappingBuilderError, + }, } diff --git a/src/federation/backends/sql.rs b/src/federation/backends/sql.rs index d77808a7..487be950 100644 --- a/src/federation/backends/sql.rs +++ b/src/federation/backends/sql.rs @@ -19,15 +19,15 @@ use super::super::types::*; use crate::config::Config; use crate::federation::FederationProviderError; +mod auth_state; mod identity_provider; +mod mapping; #[derive(Clone, Debug, Default)] pub struct SqlBackend { pub config: Config, } -impl SqlBackend {} - #[async_trait] impl FederationBackend for SqlBackend { /// Set config @@ -87,6 +87,91 @@ impl FederationBackend for SqlBackend { .await .map_err(FederationProviderError::database) } + + /// List Mapping + #[tracing::instrument(level = "debug", skip(self, db))] + async fn list_mappings( + &self, + db: &DatabaseConnection, + params: &MappingListParameters, + ) -> Result, FederationProviderError> { + Ok(mapping::list(&self.config, db, params).await?) + } + + /// Get single mapping by ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError> { + Ok(mapping::get(&self.config, db, id).await?) + } + + /// Create mapping + #[tracing::instrument(level = "debug", skip(self, db))] + async fn create_mapping( + &self, + db: &DatabaseConnection, + idp: Mapping, + ) -> Result { + Ok(mapping::create(&self.config, db, idp).await?) + } + + /// Update mapping + #[tracing::instrument(level = "debug", skip(self, db))] + async fn update_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + idp: MappingUpdate, + ) -> Result { + Ok(mapping::update(&self.config, db, id, idp).await?) + } + + /// Delete mapping + #[tracing::instrument(level = "debug", skip(self, db))] + async fn delete_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError> { + mapping::delete(&self.config, db, id) + .await + .map_err(FederationProviderError::database) + } + + /// Get auth state by ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError> { + Ok(auth_state::get(&self.config, db, id).await?) + } + + /// Create new auth state + #[tracing::instrument(level = "debug", skip(self, db))] + async fn create_auth_state( + &self, + db: &DatabaseConnection, + state: AuthState, + ) -> Result { + Ok(auth_state::create(&self.config, db, state).await?) + } + + /// Delete auth state + #[tracing::instrument(level = "debug", skip(self, db))] + async fn delete_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError> { + auth_state::delete(&self.config, db, id) + .await + .map_err(FederationProviderError::database) + } } #[cfg(test)] diff --git a/src/federation/backends/sql/auth_state.rs b/src/federation/backends/sql/auth_state.rs new file mode 100644 index 00000000..02ff5038 --- /dev/null +++ b/src/federation/backends/sql/auth_state.rs @@ -0,0 +1,342 @@ +// 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 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_auth_state as db_federated_auth_state, + prelude::FederatedAuthState as DbFederatedAuthState, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + state: I, +) -> Result, FederationDatabaseError> { + let select = DbFederatedAuthState::find_by_id(state.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + rec: AuthState, +) -> Result { + let scope: Option = if let Some(scope) = rec.scope { + Some(serde_json::to_value(&scope)?) + } else { + None + }; + let entry = db_federated_auth_state::ActiveModel { + state: Set(rec.state.clone()), + idp_id: Set(rec.idp_id.clone()), + mapping_id: Set(rec.mapping_id.clone()), + nonce: Set(rec.nonce.clone()), + redirect_uri: Set(rec.redirect_uri.clone()), + pkce_verifier: Set(rec.pkce_verifier.clone()), + started_at: Set(rec.started_at.naive_utc()), + requested_scope: scope.map(Set).unwrap_or(NotSet).into(), + }; + + let db_entry: db_federated_auth_state::Model = entry.insert(db).await?; + + db_entry.try_into() +} + +pub async fn delete>( + _conf: &Config, + db: &DatabaseConnection, + id: S, +) -> Result<(), FederationDatabaseError> { + let res = DbFederatedAuthState::delete_by_id(id.as_ref()) + .exec(db) + .await?; + if res.rows_affected == 1 { + Ok(()) + } else { + Err(FederationDatabaseError::AuthStateNotFound( + id.as_ref().to_string(), + )) + } +} + +impl TryFrom for AuthState { + type Error = FederationDatabaseError; + + fn try_from(value: db_federated_auth_state::Model) -> Result { + let mut builder = AuthStateBuilder::default(); + builder.state(value.state.clone()); + builder.nonce(value.nonce.clone()); + builder.idp_id(value.idp_id.clone()); + builder.mapping_id(value.mapping_id.clone()); + builder.redirect_uri(value.redirect_uri.clone()); + builder.pkce_verifier(value.pkce_verifier.clone()); + builder.started_at(value.started_at.and_utc()); + if let Some(scope) = value.requested_scope { + builder.scope(serde_json::from_value::(scope)?); + } + Ok(builder.build()?) + } +} + +//#[cfg(test)] +//mod tests { +// use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; +// use serde_json::json; +// +// use crate::config::Config; +// use crate::db::entity::federated_mapping; +// +// use super::*; +// +// fn get_mapping_mock>(id: S) -> federated_mapping::Model { +// federated_mapping::Model { +// id: id.as_ref().into(), +// name: "name".into(), +// domain_id: Some("did".into()), +// idp_id: "idp".into(), +// user_claim: "sub".into(), +// ..Default::default() +// } +// } +// +// #[tokio::test] +// async fn test_get() { +// let db = MockDatabase::new(DatabaseBackend::Postgres) +// .append_query_results([vec![get_mapping_mock("1")]]) +// .into_connection(); +// let config = Config::default(); +// assert_eq!( +// get(&config, &db, "1").await.unwrap().unwrap(), +// Mapping { +// id: "1".into(), +// name: "name".into(), +// domain_id: Some("did".into()), +// idp_id: "idp".into(), +// user_claim: "sub".into(), +// ..Default::default() +// } +// ); +// +// assert_eq!( +// db.into_transaction_log(), +// [Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, +// ["1".into(), 1u64.into()] +// ),] +// ); +// } +// +// #[tokio::test] +// async fn test_list() { +// let db = MockDatabase::new(DatabaseBackend::Postgres) +// .append_query_results([vec![get_mapping_mock("1")]]) +// .append_query_results([vec![get_mapping_mock("1")]]) +// .into_connection(); +// let config = Config::default(); +// assert!( +// list(&config, &db, &MappingListParameters::default()) +// .await +// .is_ok() +// ); +// assert_eq!( +// list( +// &config, +// &db, +// &MappingListParameters { +// name: Some("mapping_name".into()), +// domain_id: Some("did".into()), +// idp_id: Some("idp".into()) +// } +// ) +// .await +// .unwrap(), +// vec![Mapping { +// id: "1".into(), +// name: "name".into(), +// domain_id: Some("did".into()), +// idp_id: "idp".into(), +// user_claim: "sub".into(), +// ..Default::default() +// }] +// ); +// +// assert_eq!( +// db.into_transaction_log(), +// [ +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping""#, +// [] +// ), +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."name" = $1 AND "federated_mapping"."domain_id" = $2 AND "federated_mapping"."idp_id" = $3"#, +// ["mapping_name".into(), "did".into(), "idp".into()] +// ), +// ] +// ); +// } +// +// #[tokio::test] +// async fn test_create() { +// let db = MockDatabase::new(DatabaseBackend::Postgres) +// .append_query_results([vec![get_mapping_mock("1")]]) +// .into_connection(); +// let config = Config::default(); +// +// let req = Mapping { +// id: "1".into(), +// name: "mapping".into(), +// domain_id: Some("foo_domain".into()), +// idp_id: "idp".into(), +// allowed_redirect_uris: Some(vec!["url".into()]), +// user_claim: "sub".into(), +// user_claim_json_pointer: Some(".".into()), +// groups_claim: Some("groups".into()), +// bound_audiences: Some(vec!["a1".into(), "a2".into()]), +// bound_subject: Some("subject".into()), +// bound_claims: Some(json!({"department": "foo"})), +// claim_mappings: Some(json!({"foo": "bar"})), +// oidc_scopes: Some(vec!["oidc".into(), "oauth".into()]), +// token_user_id: Some("uid".into()), +// token_role_ids: Some(vec!["r1".into(), "r2".into()]), +// token_project_id: Some("pid".into()), +// }; +// +// assert_eq!( +// create(&config, &db, req).await.unwrap(), +// get_mapping_mock("1").try_into().unwrap() +// ); +// assert_eq!( +// db.into_transaction_log(), +// [Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"INSERT INTO "federated_mapping" ("id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_claim", "user_claim_json_pointer", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "claim_mappings", "token_user_id", "token_role_ids", "token_project_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING "id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_claim", "user_claim_json_pointer", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "claim_mappings", "token_user_id", "token_role_ids", "token_project_id""#, +// [ +// "1".into(), +// "mapping".into(), +// "idp".into(), +// "foo_domain".into(), +// "url".into(), +// "sub".into(), +// ".".into(), +// "groups".into(), +// "a1,a2".into(), +// "subject".into(), +// json!({"department": "foo"}).into(), +// "oidc,oauth".into(), +// json!({"foo": "bar"}).into(), +// "uid".into(), +// "r1,r2".into(), +// "pid".into(), +// ] +// ),] +// ); +// } +// +// #[tokio::test] +// async fn test_update() { +// let db = MockDatabase::new(DatabaseBackend::Postgres) +// .append_query_results([vec![get_mapping_mock("1")], vec![get_mapping_mock("1")]]) +// .append_exec_results([MockExecResult { +// rows_affected: 1, +// ..Default::default() +// }]) +// .into_connection(); +// let config = Config::default(); +// +// let req = MappingUpdate { +// name: Some("name".into()), +// idp_id: Some("idp".into()), +// allowed_redirect_uris: Some(Some(vec!["url".into()])), +// user_claim: Some("sub".into()), +// user_claim_json_pointer: Some(Some(".".into())), +// groups_claim: Some(Some("groups".into())), +// bound_audiences: Some(Some(vec!["a1".into(), "a2".into()])), +// bound_subject: Some(Some("subject".into())), +// bound_claims: Some(json!({"department": "foo"})), +// claim_mappings: Some(json!({"foo": "bar"})), +// oidc_scopes: Some(Some(vec!["oidc".into(), "oauth".into()])), +// token_user_id: Some(Some("uid".into())), +// token_role_ids: Some(Some(vec!["r1".into(), "r2".into()])), +// token_project_id: Some(Some("pid".into())), +// }; +// +// assert_eq!( +// update(&config, &db, "1", req).await.unwrap(), +// get_mapping_mock("1").try_into().unwrap() +// ); +// assert_eq!( +// db.into_transaction_log(), +// [ +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_claim", "federated_mapping"."user_claim_json_pointer", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."claim_mappings", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, +// ["1".into(), 1u64.into()] +// ), +// Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"UPDATE "federated_mapping" SET "name" = $1, "idp_id" = $2, "allowed_redirect_uris" = $3, "user_claim" = $4, "user_claim_json_pointer" = $5, "groups_claim" = $6, "bound_audiences" = $7, "bound_subject" = $8, "bound_claims" = $9, "oidc_scopes" = $10, "claim_mappings" = $11, "token_user_id" = $12, "token_role_ids" = $13, "token_project_id" = $14 WHERE "federated_mapping"."id" = $15 RETURNING "id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_claim", "user_claim_json_pointer", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "claim_mappings", "token_user_id", "token_role_ids", "token_project_id""#, +// [ +// "name".into(), +// "idp".into(), +// "url".into(), +// "sub".into(), +// ".".into(), +// "groups".into(), +// "a1,a2".into(), +// "subject".into(), +// json!({"department": "foo"}).into(), +// "oidc,oauth".into(), +// json!({"foo": "bar"}).into(), +// "uid".into(), +// "r1,r2".into(), +// "pid".into(), +// "1".into() +// ] +// ), +// ] +// ); +// } +// +// #[tokio::test] +// async fn test_delete() { +// let db = MockDatabase::new(DatabaseBackend::Postgres) +// .append_exec_results([MockExecResult { +// rows_affected: 1, +// ..Default::default() +// }]) +// .into_connection(); +// let config = Config::default(); +// +// delete(&config, &db, "id").await.unwrap(); +// assert_eq!( +// db.into_transaction_log(), +// [Transaction::from_sql_and_values( +// DatabaseBackend::Postgres, +// r#"DELETE FROM "federated_mapping" WHERE "federated_mapping"."id" = $1"#, +// ["id".into()] +// ),] +// ); +// } +//} diff --git a/src/federation/backends/sql/identity_provider.rs b/src/federation/backends/sql/identity_provider.rs index ce771ce1..d02fb4aa 100644 --- a/src/federation/backends/sql/identity_provider.rs +++ b/src/federation/backends/sql/identity_provider.rs @@ -19,6 +19,7 @@ use sea_orm::query::*; use crate::config::Config; use crate::db::entity::{ federated_identity_provider as db_federated_identity_provider, + identity_provider as db_old_identity_provider, prelude::FederatedIdentityProvider as DbFederatedIdentityProvider, }; use crate::federation::backends::error::FederationDatabaseError; @@ -100,6 +101,12 @@ pub async fn create( .unwrap_or(NotSet) .into(), bound_issuer: idp.bound_issuer.clone().map(Set).unwrap_or(NotSet).into(), + default_mapping_name: idp + .default_mapping_name + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), provider_config: idp .provider_config .clone() @@ -109,6 +116,16 @@ pub async fn create( let db_entry: db_federated_identity_provider::Model = entry.insert(db).await?; + let _old_idp = db_old_identity_provider::ActiveModel { + id: Set(idp.id.clone()), + enabled: Set(false), + description: Set(Some(idp.name.clone())), + domain_id: Set(idp.domain_id.clone().unwrap_or("<>".into())), + authorization_ttl: NotSet, + } + .insert(db) + .await?; + db_entry.try_into() } @@ -215,6 +232,9 @@ impl TryFrom for IdentityProvider { if let Some(val) = &value.provider_config { builder.provider_config(val.clone()); } + if let Some(val) = &value.default_mapping_name { + builder.default_mapping_name(val.clone()); + } Ok(builder.build()?) } } @@ -225,7 +245,7 @@ mod tests { use serde_json::json; use crate::config::Config; - use crate::db::entity::federated_identity_provider; + use crate::db::entity::{federated_identity_provider, identity_provider}; use super::*; @@ -238,6 +258,19 @@ mod tests { } } + fn get_old_idp_mock>(id: S) -> identity_provider::Model { + identity_provider::Model { + id: id.as_ref().into(), + enabled: true, + description: Some("name".into()), + domain_id: "did".into(), + authorization_ttl: None, + } + } + + #[test] + fn test_from_db_model() {} + #[tokio::test] async fn test_get() { let db = MockDatabase::new(DatabaseBackend::Postgres) @@ -259,7 +292,7 @@ mod tests { db.into_transaction_log(), [Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, ["1".into(), 1u64.into()] ),] ); @@ -302,12 +335,12 @@ mod tests { [ Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."provider_config" FROM "federated_identity_provider""#, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider""#, [] ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."name" = $1 AND "federated_identity_provider"."domain_id" = $2"#, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."name" = $1 AND "federated_identity_provider"."domain_id" = $2"#, ["idp_name".into(), "did".into()] ), ] @@ -319,6 +352,7 @@ mod tests { // Create MockDatabase with mock query results let db = MockDatabase::new(DatabaseBackend::Postgres) .append_query_results([vec![get_idp_mock("1")]]) + .append_query_results([vec![get_old_idp_mock("1")]]) .into_connection(); let config = Config::default(); @@ -333,6 +367,7 @@ mod tests { oidc_response_types: Some(vec!["t1".into(), "t2".into()]), jwt_validation_pubkeys: Some(vec!["jt1".into(), "jt2".into()]), bound_issuer: Some("bi".into()), + default_mapping_name: Some("dummy".into()), provider_config: Some(json!({"foo": "bar"})), }; @@ -343,23 +378,31 @@ mod tests { // Checking transaction log assert_eq!( db.into_transaction_log(), - [Transaction::from_sql_and_values( - DatabaseBackend::Postgres, - r#"INSERT INTO "federated_identity_provider" ("id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwt_validation_pubkeys", "bound_issuer", "provider_config") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwt_validation_pubkeys", "bound_issuer", "provider_config""#, - [ - "1".into(), - "idp".into(), - "foo_domain".into(), - "url".into(), - "oidccid".into(), - "oidccs".into(), - "oidcrm".into(), - "t1,t2".into(), - "jt1,jt2".into(), - "bi".into(), - json!({"foo": "bar"}).into() - ] - ),] + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federated_identity_provider" ("id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config""#, + [ + "1".into(), + "idp".into(), + "foo_domain".into(), + "url".into(), + "oidccid".into(), + "oidccs".into(), + "oidcrm".into(), + "t1,t2".into(), + "jt1,jt2".into(), + "bi".into(), + "dummy".into(), + json!({"foo": "bar"}).into() + ] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "identity_provider" ("id", "enabled", "description", "domain_id") VALUES ($1, $2, $3, $4) RETURNING "id", "enabled", "description", "domain_id", "authorization_ttl""#, + ["1".into(), false.into(), "idp".into(), "foo_domain".into(),] + ), + ] ); } @@ -384,6 +427,7 @@ mod tests { oidc_response_types: Some(Some(vec!["t1".into(), "t2".into()])), jwt_validation_pubkeys: Some(Some(vec!["jt1".into(), "jt2".into()])), bound_issuer: Some(Some("bi".into())), + default_mapping_name: Some(Some("dummy".into())), provider_config: Some(Some(json!({"foo": "bar"}))), }; @@ -397,12 +441,12 @@ mod tests { [ Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, + r#"SELECT "federated_identity_provider"."id", "federated_identity_provider"."name", "federated_identity_provider"."domain_id", "federated_identity_provider"."oidc_discovery_url", "federated_identity_provider"."oidc_client_id", "federated_identity_provider"."oidc_client_secret", "federated_identity_provider"."oidc_response_mode", "federated_identity_provider"."oidc_response_types", "federated_identity_provider"."jwt_validation_pubkeys", "federated_identity_provider"."bound_issuer", "federated_identity_provider"."default_mapping_name", "federated_identity_provider"."provider_config" FROM "federated_identity_provider" WHERE "federated_identity_provider"."id" = $1 LIMIT $2"#, ["1".into(), 1u64.into()] ), Transaction::from_sql_and_values( DatabaseBackend::Postgres, - r#"UPDATE "federated_identity_provider" SET "name" = $1, "oidc_discovery_url" = $2, "oidc_client_id" = $3, "oidc_client_secret" = $4, "oidc_response_mode" = $5, "oidc_response_types" = $6, "jwt_validation_pubkeys" = $7, "bound_issuer" = $8, "provider_config" = $9 WHERE "federated_identity_provider"."id" = $10 RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwt_validation_pubkeys", "bound_issuer", "provider_config""#, + r#"UPDATE "federated_identity_provider" SET "name" = $1, "oidc_discovery_url" = $2, "oidc_client_id" = $3, "oidc_client_secret" = $4, "oidc_response_mode" = $5, "oidc_response_types" = $6, "jwt_validation_pubkeys" = $7, "bound_issuer" = $8, "provider_config" = $9 WHERE "federated_identity_provider"."id" = $10 RETURNING "id", "name", "domain_id", "oidc_discovery_url", "oidc_client_id", "oidc_client_secret", "oidc_response_mode", "oidc_response_types", "jwt_validation_pubkeys", "bound_issuer", "default_mapping_name", "provider_config""#, [ "idp".into(), "url".into(), diff --git a/src/federation/backends/sql/mapping.rs b/src/federation/backends/sql/mapping.rs new file mode 100644 index 00000000..65d27c1c --- /dev/null +++ b/src/federation/backends/sql/mapping.rs @@ -0,0 +1,533 @@ +// 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 + +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{ + federated_mapping as db_federated_mapping, prelude::FederatedMapping as DbFederatedMapping, +}; +use crate::federation::backends::error::FederationDatabaseError; +use crate::federation::types::*; + +pub async fn get>( + _conf: &Config, + db: &DatabaseConnection, + id: I, +) -> Result, FederationDatabaseError> { + let select = DbFederatedMapping::find_by_id(id.as_ref()); + + let entry: Option = select.one(db).await?; + entry.map(TryInto::try_into).transpose() +} + +pub async fn list( + _conf: &Config, + db: &DatabaseConnection, + params: &MappingListParameters, +) -> Result, FederationDatabaseError> { + let mut select = DbFederatedMapping::find(); + + if let Some(val) = ¶ms.name { + select = select.filter(db_federated_mapping::Column::Name.eq(val)); + } + + if let Some(val) = ¶ms.domain_id { + select = select.filter(db_federated_mapping::Column::DomainId.eq(val)); + } + + if let Some(val) = ¶ms.idp_id { + select = select.filter(db_federated_mapping::Column::IdpId.eq(val)); + } + + let db_entities: Vec = select.all(db).await?; + let results: Result, _> = db_entities + .into_iter() + .map(TryInto::::try_into) + .collect(); + + results +} + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + mapping: Mapping, +) -> Result { + let entry = db_federated_mapping::ActiveModel { + id: Set(mapping.id.clone()), + domain_id: Set(mapping.domain_id.clone()), + name: Set(mapping.name.clone()), + idp_id: Set(mapping.idp_id.clone()), + allowed_redirect_uris: mapping + .allowed_redirect_uris + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + user_id_claim: Set(mapping.user_id_claim.clone()), + user_name_claim: Set(mapping.user_name_claim.clone()), + domain_id_claim: mapping + .domain_id_claim + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + groups_claim: mapping + .groups_claim + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + bound_audiences: mapping + .bound_audiences + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + bound_subject: mapping + .bound_subject + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + bound_claims: mapping + .bound_claims + .clone() + .map(|x| Set(Some(x))) + .unwrap_or(NotSet), + oidc_scopes: mapping + .oidc_scopes + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + token_user_id: mapping + .token_user_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + token_role_ids: mapping + .token_role_ids + .clone() + .map(|x| Set(x.join(","))) + .unwrap_or(NotSet) + .into(), + token_project_id: mapping + .token_project_id + .clone() + .map(Set) + .unwrap_or(NotSet) + .into(), + }; + + let db_entry: db_federated_mapping::Model = entry.insert(db).await?; + + db_entry.try_into() +} + +pub async fn update>( + _conf: &Config, + db: &DatabaseConnection, + id: S, + mapping: MappingUpdate, +) -> Result { + if let Some(current) = DbFederatedMapping::find_by_id(id.as_ref()).one(db).await? { + let mut entry: db_federated_mapping::ActiveModel = current.into(); + if let Some(val) = mapping.name { + entry.name = Set(val.to_owned()); + } + if let Some(val) = mapping.idp_id { + entry.idp_id = Set(val.to_owned()); + } + if let Some(val) = mapping.allowed_redirect_uris { + entry.allowed_redirect_uris = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.user_id_claim { + entry.user_id_claim = Set(val.to_owned()); + } + if let Some(val) = mapping.user_name_claim { + entry.user_name_claim = Set(val.to_owned()); + } + if let Some(val) = mapping.domain_id_claim { + entry.domain_id_claim = Set(Some(val.to_owned())); + } + if let Some(val) = mapping.groups_claim { + entry.groups_claim = Set(val.to_owned()); + } + if let Some(val) = mapping.bound_audiences { + entry.bound_audiences = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.bound_subject { + entry.bound_subject = Set(val.to_owned()); + } + if let Some(val) = &mapping.bound_claims { + entry.bound_claims = Set(Some(val.clone())); + } + if let Some(val) = mapping.oidc_scopes { + entry.oidc_scopes = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.token_user_id { + entry.token_user_id = Set(val.to_owned()); + } + if let Some(val) = mapping.token_role_ids { + entry.token_role_ids = Set(val.clone().map(|x| x.join(","))); + } + if let Some(val) = mapping.token_project_id { + entry.token_project_id = Set(val.to_owned()); + } + + let db_entry: db_federated_mapping::Model = entry.update(db).await?; + db_entry.try_into() + } else { + Err(FederationDatabaseError::MappingNotFound( + id.as_ref().to_string(), + )) + } +} + +pub async fn delete>( + _conf: &Config, + db: &DatabaseConnection, + id: S, +) -> Result<(), FederationDatabaseError> { + let res = DbFederatedMapping::delete_by_id(id.as_ref()) + .exec(db) + .await?; + if res.rows_affected == 1 { + Ok(()) + } else { + Err(FederationDatabaseError::MappingNotFound( + id.as_ref().to_string(), + )) + } +} + +impl TryFrom for Mapping { + type Error = FederationDatabaseError; + + fn try_from(value: db_federated_mapping::Model) -> Result { + let mut builder = MappingBuilder::default(); + builder.id(value.id.clone()); + builder.name(value.name.clone()); + builder.idp_id(value.idp_id.clone()); + if let Some(val) = &value.domain_id { + builder.domain_id(val); + } + if let Some(val) = &value.allowed_redirect_uris { + if !val.is_empty() { + builder.allowed_redirect_uris(Vec::from_iter(val.split(",").map(Into::into))); + } + } + builder.user_id_claim(value.user_id_claim.clone()); + builder.user_name_claim(value.user_name_claim.clone()); + if let Some(val) = &value.domain_id_claim { + builder.domain_id_claim(val); + } + if let Some(val) = &value.groups_claim { + builder.groups_claim(val); + } + if let Some(val) = &value.bound_audiences { + if !val.is_empty() { + builder.bound_audiences(Vec::from_iter(val.split(",").map(Into::into))); + } + } + if let Some(val) = &value.bound_subject { + builder.bound_subject(val); + } + if let Some(val) = &value.bound_claims { + builder.bound_claims(val.clone()); + } + if let Some(val) = &value.oidc_scopes { + if !val.is_empty() { + builder.oidc_scopes(Vec::from_iter(val.split(",").map(Into::into))); + } + } + if let Some(val) = &value.token_user_id { + builder.token_user_id(val.clone()); + } + if let Some(val) = &value.token_role_ids { + if !val.is_empty() { + builder.token_role_ids(Vec::from_iter(val.split(",").map(Into::into))); + } + } + if let Some(val) = &value.token_project_id { + builder.token_project_id(val.clone()); + } + Ok(builder.build()?) + } +} + +#[cfg(test)] +mod tests { + use sea_orm::{DatabaseBackend, MockDatabase, MockExecResult, Transaction}; + use serde_json::json; + + use crate::config::Config; + use crate::db::entity::federated_mapping; + + use super::*; + + fn get_mapping_mock>(id: S) -> federated_mapping::Model { + federated_mapping::Model { + id: id.as_ref().into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + allowed_redirect_uris: None, + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: None, + bound_audiences: None, + bound_subject: None, + bound_claims: None, + oidc_scopes: None, + token_user_id: None, + token_role_ids: None, + token_project_id: None, + } + } + + #[tokio::test] + async fn test_get() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")]]) + .into_connection(); + let config = Config::default(); + assert_eq!( + get(&config, &db, "1").await.unwrap().unwrap(), + Mapping { + id: "1".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + } + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ),] + ); + } + + #[tokio::test] + async fn test_list() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")]]) + .append_query_results([vec![get_mapping_mock("1")]]) + .into_connection(); + let config = Config::default(); + assert!( + list(&config, &db, &MappingListParameters::default()) + .await + .is_ok() + ); + assert_eq!( + list( + &config, + &db, + &MappingListParameters { + name: Some("mapping_name".into()), + domain_id: Some("did".into()), + idp_id: Some("idp".into()) + } + ) + .await + .unwrap(), + vec![Mapping { + id: "1".into(), + name: "name".into(), + domain_id: Some("did".into()), + idp_id: "idp".into(), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + ..Default::default() + }] + ); + + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping""#, + [] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."name" = $1 AND "federated_mapping"."domain_id" = $2 AND "federated_mapping"."idp_id" = $3"#, + ["mapping_name".into(), "did".into(), "idp".into()] + ), + ] + ); + } + + #[tokio::test] + async fn test_create() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")]]) + .into_connection(); + let config = Config::default(); + + let req = Mapping { + id: "1".into(), + name: "mapping".into(), + domain_id: Some("foo_domain".into()), + idp_id: "idp".into(), + allowed_redirect_uris: Some(vec!["url".into()]), + user_id_claim: "sub".into(), + user_name_claim: "preferred_username".into(), + domain_id_claim: Some("domain_id".into()), + groups_claim: Some("groups".into()), + bound_audiences: Some(vec!["a1".into(), "a2".into()]), + bound_subject: Some("subject".into()), + bound_claims: Some(json!({"department": "foo"})), + //claim_mappings: Some(json!({"foo": "bar"})), + oidc_scopes: Some(vec!["oidc".into(), "oauth".into()]), + token_user_id: Some("uid".into()), + token_role_ids: Some(vec!["r1".into(), "r2".into()]), + token_project_id: Some("pid".into()), + }; + + assert_eq!( + create(&config, &db, req).await.unwrap(), + get_mapping_mock("1").try_into().unwrap() + ); + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"INSERT INTO "federated_mapping" ("id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING "id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id""#, + [ + "1".into(), + "mapping".into(), + "idp".into(), + "foo_domain".into(), + "url".into(), + "sub".into(), + "preferred_username".into(), + "domain_id".into(), + "groups".into(), + "a1,a2".into(), + "subject".into(), + json!({"department": "foo"}).into(), + "oidc,oauth".into(), + "uid".into(), + "r1,r2".into(), + "pid".into(), + ] + ),] + ); + } + + #[tokio::test] + async fn test_update() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([vec![get_mapping_mock("1")], vec![get_mapping_mock("1")]]) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + let req = MappingUpdate { + name: Some("name".into()), + idp_id: Some("idp".into()), + allowed_redirect_uris: Some(Some(vec!["url".into()])), + user_id_claim: Some("sub".into()), + user_name_claim: Some("preferred_username".into()), + domain_id_claim: Some("domain_id".into()), + groups_claim: Some(Some("groups".into())), + bound_audiences: Some(Some(vec!["a1".into(), "a2".into()])), + bound_subject: Some(Some("subject".into())), + bound_claims: Some(json!({"department": "foo"})), + //claim_mappings: Some(json!({"foo": "bar"})), + oidc_scopes: Some(Some(vec!["oidc".into(), "oauth".into()])), + token_user_id: Some(Some("uid".into())), + token_role_ids: Some(Some(vec!["r1".into(), "r2".into()])), + token_project_id: Some(Some("pid".into())), + }; + + assert_eq!( + update(&config, &db, "1", req).await.unwrap(), + get_mapping_mock("1").try_into().unwrap() + ); + assert_eq!( + db.into_transaction_log(), + [ + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"SELECT "federated_mapping"."id", "federated_mapping"."name", "federated_mapping"."idp_id", "federated_mapping"."domain_id", "federated_mapping"."allowed_redirect_uris", "federated_mapping"."user_id_claim", "federated_mapping"."user_name_claim", "federated_mapping"."domain_id_claim", "federated_mapping"."groups_claim", "federated_mapping"."bound_audiences", "federated_mapping"."bound_subject", "federated_mapping"."bound_claims", "federated_mapping"."oidc_scopes", "federated_mapping"."token_user_id", "federated_mapping"."token_role_ids", "federated_mapping"."token_project_id" FROM "federated_mapping" WHERE "federated_mapping"."id" = $1 LIMIT $2"#, + ["1".into(), 1u64.into()] + ), + Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"UPDATE "federated_mapping" SET "name" = $1, "idp_id" = $2, "allowed_redirect_uris" = $3, "user_id_claim" = $4, "user_name_claim" = $5, "domain_id_claim" = $6, "groups_claim" = $7, "bound_audiences" = $8, "bound_subject" = $9, "bound_claims" = $10, "oidc_scopes" = $11, "token_user_id" = $12, "token_role_ids" = $13, "token_project_id" = $14 WHERE "federated_mapping"."id" = $15 RETURNING "id", "name", "idp_id", "domain_id", "allowed_redirect_uris", "user_id_claim", "user_name_claim", "domain_id_claim", "groups_claim", "bound_audiences", "bound_subject", "bound_claims", "oidc_scopes", "token_user_id", "token_role_ids", "token_project_id""#, + [ + "name".into(), + "idp".into(), + "url".into(), + "sub".into(), + "preferred_username".into(), + "domain_id".into(), + "groups".into(), + "a1,a2".into(), + "subject".into(), + json!({"department": "foo"}).into(), + "oidc,oauth".into(), + "uid".into(), + "r1,r2".into(), + "pid".into(), + "1".into() + ] + ), + ] + ); + } + + #[tokio::test] + async fn test_delete() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + rows_affected: 1, + ..Default::default() + }]) + .into_connection(); + let config = Config::default(); + + delete(&config, &db, "id").await.unwrap(); + assert_eq!( + db.into_transaction_log(), + [Transaction::from_sql_and_values( + DatabaseBackend::Postgres, + r#"DELETE FROM "federated_mapping" WHERE "federated_mapping"."id" = $1"#, + ["id".into()] + ),] + ); + } +} diff --git a/src/federation/error.rs b/src/federation/error.rs index 4221b704..eb9c8488 100644 --- a/src/federation/error.rs +++ b/src/federation/error.rs @@ -33,6 +33,10 @@ pub enum FederationProviderError { #[error("identity provider {0} not found")] IdentityProviderNotFound(String), + /// IDP mapping not found + #[error("mapping {0} not found")] + MappingNotFound(String), + /// Identity provider error #[error(transparent)] FederationDatabase { @@ -47,6 +51,7 @@ impl FederationProviderError { FederationDatabaseError::IdentityProviderNotFound(x) => { Self::IdentityProviderNotFound(x) } + FederationDatabaseError::MappingNotFound(x) => Self::MappingNotFound(x), _ => Self::FederationDatabase { source }, } } diff --git a/src/federation/mod.rs b/src/federation/mod.rs index 3bb4d6e7..237f7900 100644 --- a/src/federation/mod.rs +++ b/src/federation/mod.rs @@ -20,7 +20,7 @@ use uuid::Uuid; pub mod backends; pub mod error; -pub(crate) mod types; +pub mod types; use crate::config::Config; use crate::federation::backends::sql::SqlBackend; @@ -65,6 +65,55 @@ pub trait FederationApi: Send + Sync + Clone { db: &DatabaseConnection, id: &'a str, ) -> Result<(), FederationProviderError>; + + async fn list_mappings( + &self, + db: &DatabaseConnection, + params: &MappingListParameters, + ) -> Result, FederationProviderError>; + + async fn get_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError>; + + async fn create_mapping( + &self, + db: &DatabaseConnection, + idp: Mapping, + ) -> Result; + + async fn update_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + idp: MappingUpdate, + ) -> Result; + + async fn delete_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + async fn get_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError>; + + async fn create_auth_state( + &self, + db: &DatabaseConnection, + state: AuthState, + ) -> Result; + + async fn delete_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError>; } #[cfg(test)] @@ -105,6 +154,59 @@ mock! { db: &DatabaseConnection, id: &'a str, ) -> Result<(), FederationProviderError>; + + async fn list_mappings( + &self, + db: &DatabaseConnection, + params: &MappingListParameters, + ) -> Result, FederationProviderError>; + + /// Get single mapping by ID + async fn get_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError>; + + /// Create mapping + async fn create_mapping( + &self, + db: &DatabaseConnection, + idp: Mapping, + ) -> Result; + + /// Update mapping + async fn update_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + idp: MappingUpdate, + ) -> Result; + + /// Delete mapping + async fn delete_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + async fn get_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError>; + + async fn create_auth_state( + &self, + db: &DatabaseConnection, + state: AuthState, + ) -> Result; + + async fn delete_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError>; } impl Clone for FederationProvider { @@ -197,4 +299,89 @@ impl FederationApi for FederationProvider { ) -> Result<(), FederationProviderError> { self.backend_driver.delete_identity_provider(db, id).await } + + /// List mappings + #[tracing::instrument(level = "info", skip(self, db))] + async fn list_mappings( + &self, + db: &DatabaseConnection, + params: &MappingListParameters, + ) -> Result, FederationProviderError> { + self.backend_driver.list_mappings(db, params).await + } + + /// Get single mapping by ID + #[tracing::instrument(level = "info", skip(self, db))] + async fn get_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError> { + self.backend_driver.get_mapping(db, id).await + } + + /// Create mapping + #[tracing::instrument(level = "debug", skip(self, db))] + async fn create_mapping( + &self, + db: &DatabaseConnection, + idp: Mapping, + ) -> Result { + let mut mod_idp = idp; + mod_idp.id = Uuid::new_v4().into(); + + self.backend_driver.create_mapping(db, mod_idp).await + } + + /// Update mapping + #[tracing::instrument(level = "debug", skip(self, db))] + async fn update_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + idp: MappingUpdate, + ) -> Result { + // TODO: Check update of idp_id to enure it belongs to the same domain + self.backend_driver.update_mapping(db, id, idp).await + } + + /// Delete identity provider + #[tracing::instrument(level = "debug", skip(self, db))] + async fn delete_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError> { + self.backend_driver.delete_mapping(db, id).await + } + + /// Get auth state by ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn get_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError> { + self.backend_driver.get_auth_state(db, id).await + } + + /// Create new auth state + #[tracing::instrument(level = "debug", skip(self, db))] + async fn create_auth_state( + &self, + db: &DatabaseConnection, + state: AuthState, + ) -> Result { + self.backend_driver.create_auth_state(db, state).await + } + + /// Delete auth state + #[tracing::instrument(level = "debug", skip(self, db))] + async fn delete_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError> { + self.backend_driver.delete_auth_state(db, id).await + } } diff --git a/src/federation/types.rs b/src/federation/types.rs index 808b0424..c192669d 100644 --- a/src/federation/types.rs +++ b/src/federation/types.rs @@ -12,7 +12,9 @@ // // SPDX-License-Identifier: Apache-2.0 +pub mod auth_state; pub mod identity_provider; +pub mod mapping; use async_trait::async_trait; use dyn_clone::DynClone; @@ -21,7 +23,9 @@ use sea_orm::DatabaseConnection; use crate::config::Config; use crate::federation::FederationProviderError; +pub use auth_state::*; pub use identity_provider::*; +pub use mapping::*; #[async_trait] pub trait FederationBackend: DynClone + Send + Sync + std::fmt::Debug { @@ -63,6 +67,63 @@ pub trait FederationBackend: DynClone + Send + Sync + std::fmt::Debug { db: &DatabaseConnection, id: &'a str, ) -> Result<(), FederationProviderError>; + + /// List Identity Providers + async fn list_mappings( + &self, + db: &DatabaseConnection, + params: &MappingListParameters, + ) -> Result, FederationProviderError>; + + /// Get single mapping by ID + async fn get_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError>; + + /// Create mapping + async fn create_mapping( + &self, + db: &DatabaseConnection, + idp: Mapping, + ) -> Result; + + /// Update mapping + async fn update_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + idp: MappingUpdate, + ) -> Result; + + /// Delete mapping + async fn delete_mapping<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError>; + + /// Get authentication state + async fn get_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result, FederationProviderError>; + + /// Create new authentication state + async fn create_auth_state( + &self, + db: &DatabaseConnection, + state: AuthState, + ) -> Result; + + /// Delete authentication state + async fn delete_auth_state<'a>( + &self, + db: &DatabaseConnection, + id: &'a str, + ) -> Result<(), FederationProviderError>; } dyn_clone::clone_trait_object!(FederationBackend); diff --git a/src/federation/types/auth_state.rs b/src/federation/types/auth_state.rs new file mode 100644 index 00000000..f7c39e4e --- /dev/null +++ b/src/federation/types/auth_state.rs @@ -0,0 +1,55 @@ +// 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 + +use chrono::{DateTime, Utc}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct AuthState { + /// IDP ID + pub idp_id: String, + + /// Mapping ID + pub mapping_id: String, + + /// Auth state (Primary key, CSRF) + pub state: String, + + /// Nonce + pub nonce: String, + + /// Requested redirect uri + pub redirect_uri: String, + + /// PKCE verifier value + pub pkce_verifier: String, + + /// Timestamp when the auth was initiated + #[builder(default)] + pub started_at: DateTime, + + /// Requested scope + #[builder(default)] + pub scope: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Scope { + Project(String), + Domain(String), + System(String), +} diff --git a/src/federation/types/identity_provider.rs b/src/federation/types/identity_provider.rs index e48ca107..3a8921f1 100644 --- a/src/federation/types/identity_provider.rs +++ b/src/federation/types/identity_provider.rs @@ -49,12 +49,15 @@ pub struct IdentityProvider { #[builder(default)] pub bound_issuer: Option, + #[builder(default)] + pub default_mapping_name: Option, + #[builder(default)] pub provider_config: Option, } #[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] -#[builder(setter(strip_option, into))] +#[builder(setter(into))] pub struct IdentityProviderUpdate { /// Provider name pub name: Option, @@ -80,6 +83,9 @@ pub struct IdentityProviderUpdate { #[builder(default)] pub bound_issuer: Option>, + #[builder(default)] + pub default_mapping_name: Option>, + #[builder(default)] pub provider_config: Option>, } diff --git a/src/federation/types/mapping.rs b/src/federation/types/mapping.rs new file mode 100644 index 00000000..72687ce9 --- /dev/null +++ b/src/federation/types/mapping.rs @@ -0,0 +1,129 @@ +// 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 + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(strip_option, into))] +pub struct Mapping { + /// Federation IDP mapping ID + pub id: String, + + /// Mapping name + pub name: String, + + #[builder(default)] + pub domain_id: Option, + + /// IDP ID + pub idp_id: String, + + #[builder(default)] + pub allowed_redirect_uris: Option>, + + #[builder(default)] + pub user_id_claim: String, + + #[builder(default)] + pub user_name_claim: String, + + #[builder(default)] + pub domain_id_claim: Option, + + #[builder(default)] + pub groups_claim: Option, + + #[builder(default)] + pub bound_audiences: Option>, + + #[builder(default)] + pub bound_subject: Option, + + #[builder(default)] + pub bound_claims: Option, + + #[builder(default)] + pub oidc_scopes: Option>, + + //#[builder(default)] + //pub claim_mappings: Option, + #[builder(default)] + pub token_user_id: Option, + + #[builder(default)] + pub token_role_ids: Option>, + + #[builder(default)] + pub token_project_id: Option, +} + +#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize, PartialEq)] +#[builder(setter(into))] +pub struct MappingUpdate { + /// Mapping name + pub name: Option, + + // TODO: on update must check that domain_id match + #[builder(default)] + pub idp_id: Option, + + #[builder(default)] + pub allowed_redirect_uris: Option>>, + + #[builder(default)] + pub user_id_claim: Option, + + #[builder(default)] + pub user_name_claim: Option, + + #[builder(default)] + pub domain_id_claim: Option, + + #[builder(default)] + pub groups_claim: Option>, + + #[builder(default)] + pub bound_audiences: Option>>, + + #[builder(default)] + pub bound_subject: Option>, + + #[builder(default)] + pub bound_claims: Option, + + #[builder(default)] + pub oidc_scopes: Option>>, + + #[builder(default)] + pub token_user_id: Option>, + + #[builder(default)] + pub token_role_ids: Option>>, + + #[builder(default)] + pub token_project_id: Option>, +} + +#[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[builder(setter(strip_option, into))] +pub struct MappingListParameters { + /// Filters the response by Mapping name. + pub name: Option, + /// Filters the response by a domain_id ID. + pub domain_id: Option, + /// Filters the response by IDP ID. + pub idp_id: Option, +} diff --git a/src/identity/backends/error.rs b/src/identity/backends/error.rs index b8b95ede..c0510656 100644 --- a/src/identity/backends/error.rs +++ b/src/identity/backends/error.rs @@ -40,6 +40,12 @@ pub enum IdentityDatabaseError { source: UserResponseBuilderError, }, + #[error("building user federation")] + FederatedUserBuilderError { + #[from] + source: FederationBuilderError, + }, + #[error("database data")] Database { #[from] diff --git a/src/identity/backends/sql.rs b/src/identity/backends/sql.rs index 2d1f3074..cb9fb9b3 100644 --- a/src/identity/backends/sql.rs +++ b/src/identity/backends/sql.rs @@ -117,6 +117,17 @@ impl IdentityBackend for SqlBackend { Ok(get_user(&self.config, db, user_id).await?) } + /// Find federated user by IDP and Unique ID + #[tracing::instrument(level = "debug", skip(self, db))] + async fn find_federated_user<'a>( + &self, + db: &DatabaseConnection, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError> { + Ok(find_federated_user(&self.config, db, idp_id, unique_id).await?) + } + /// Create user #[tracing::instrument(level = "debug", skip(self, db))] async fn create_user( @@ -410,13 +421,70 @@ pub async fn get_user( Ok(None) } +pub async fn find_federated_user, U: AsRef>( + conf: &Config, + db: &DatabaseConnection, + idp_id: I, + unique_id: U, +) -> Result, IdentityDatabaseError> { + if let Some(federated_entry) = + federated_user::find_by_idp_and_unique_id(conf, db, idp_id, unique_id).await? + { + return get_user(conf, db, &federated_entry.user_id).await; + } + Ok(None) +} + async fn create_user( conf: &Config, db: &DatabaseConnection, user: UserCreate, ) -> Result { let main_user = user::create(conf, db, &user).await?; - if let Some(_federated) = &user.federated { + if let Some(federation_data) = &user.federated { + let mut federated_entities: Vec = Vec::new(); + for federated_user in federation_data { + if federated_user.protocols.is_empty() { + federated_entities.push( + federated_user::create( + conf, + db, + db_federated_user::ActiveModel { + id: NotSet, + user_id: Set(user.id.clone()), + idp_id: Set(federated_user.idp_id.clone()), + protocol_id: Set("oidc".into()), + unique_id: Set(federated_user.unique_id.clone()), + display_name: Set(Some(user.name.clone())), + }, + ) + .await?, + ); + } else { + for proto in &federated_user.protocols { + federated_entities.push( + federated_user::create( + conf, + db, + db_federated_user::ActiveModel { + id: NotSet, + user_id: Set(user.id.clone()), + idp_id: Set(federated_user.idp_id.clone()), + protocol_id: Set(proto.protocol_id.clone()), + unique_id: Set(proto.unique_id.clone()), + display_name: Set(Some(user.name.clone())), + }, + ) + .await?, + ); + } + } + } + + let builder = + common::get_federated_user_builder(&main_user, federated_entities, Vec::new()); + + Ok(builder.build()?) } else { // Local user let local_user = local_user::create(conf, db, &user).await?; @@ -432,18 +500,18 @@ async fn create_user( passwords.push(password_entry); } - return Ok(common::get_local_user_builder( + Ok(common::get_local_user_builder( conf, &main_user, local_user, Some(passwords), Vec::new(), ) - .build()?); + .build()?) } - let ub = common::get_user_builder(&main_user, Vec::new()).build()?; + // let ub = common::get_user_builder(&main_user, Vec::new()).build()?; - Ok(ub) + // Ok(ub) } #[cfg(test)] diff --git a/src/identity/backends/sql/federated_user.rs b/src/identity/backends/sql/federated_user.rs index 755a98e9..c870d834 100644 --- a/src/identity/backends/sql/federated_user.rs +++ b/src/identity/backends/sql/federated_user.rs @@ -12,5 +12,42 @@ // // SPDX-License-Identifier: Apache-2.0 +use sea_orm::DatabaseConnection; +use sea_orm::entity::*; +use sea_orm::query::*; + +use crate::config::Config; +use crate::db::entity::{federated_user, prelude::FederatedUser}; +use crate::identity::backends::error::IdentityDatabaseError; + +pub async fn create( + _conf: &Config, + db: &DatabaseConnection, + federation: A, +) -> Result +where + A: Into, +{ + let db_user: federated_user::Model = federation.into().insert(db).await?; + + Ok(db_user) +} + +/// Get federated user entry by the idp_id and the unique_id +pub async fn find_by_idp_and_unique_id, U: AsRef>( + _conf: &Config, + db: &DatabaseConnection, + idp_id: I, + unique_id: U, +) -> Result, IdentityDatabaseError> { + Ok(FederatedUser::find() + .filter(federated_user::Column::IdpId.eq(idp_id.as_ref())) + .filter(federated_user::Column::UniqueId.eq(unique_id.as_ref())) + .all(db) + .await? + .first() + .cloned()) +} + #[cfg(test)] mod tests {} diff --git a/src/identity/backends/sql/user.rs b/src/identity/backends/sql/user.rs index 40db792d..1bb9266c 100644 --- a/src/identity/backends/sql/user.rs +++ b/src/identity/backends/sql/user.rs @@ -15,6 +15,7 @@ use chrono::Local; use sea_orm::DatabaseConnection; use sea_orm::entity::*; +use serde_json::json; use crate::config::Config; use crate::db::entity::{prelude::User as DbUser, user}; @@ -52,7 +53,10 @@ pub(super) async fn create( let entry: user::ActiveModel = user::ActiveModel { id: Set(user.id.clone()), enabled: Set(user.enabled), - extra: Set(Some(serde_json::to_string(&user.extra)?)), + extra: Set(Some(serde_json::to_string( + // For keystone it is important to have at least "{}" + &user.extra.as_ref().or(Some(&json!({}))), + )?)), default_project_id: Set(user.default_project_id.clone()), last_active_at, created_at: Set(Some(now)), diff --git a/src/identity/error.rs b/src/identity/error.rs index 1dfb9697..f5feb8e3 100644 --- a/src/identity/error.rs +++ b/src/identity/error.rs @@ -15,7 +15,8 @@ use thiserror::Error; use crate::identity::backends::error::*; -use crate::identity::types::{DomainBuilderError, UserResponseBuilderError}; +use crate::identity::types::*; +//{DomainBuilderError, UserResponseBuilderError}; use crate::resource::error::ResourceProviderError; #[derive(Error, Debug)] @@ -50,6 +51,18 @@ pub enum IdentityProviderError { source: UserResponseBuilderError, }, + #[error(transparent)] + UserCreateBuilder { + #[from] + source: UserCreateBuilderError, + }, + + #[error(transparent)] + FederatedUserBuilder { + #[from] + source: FederationBuilderError, + }, + #[error(transparent)] DomainBuilder { #[from] diff --git a/src/identity/mod.rs b/src/identity/mod.rs index 97ca4136..a14670d0 100644 --- a/src/identity/mod.rs +++ b/src/identity/mod.rs @@ -61,6 +61,13 @@ pub trait IdentityApi: Send + Sync + Clone { user_id: &'a str, ) -> Result, IdentityProviderError>; + async fn find_federated_user<'a>( + &self, + db: &DatabaseConnection, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError>; + async fn create_user( &self, db: &DatabaseConnection, @@ -185,6 +192,13 @@ mock! { user_id: &'a str, ) -> Result, IdentityProviderError>; + async fn find_federated_user<'a>( + &self, + db: &DatabaseConnection, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError>; + async fn create_user( &self, db: &DatabaseConnection, @@ -364,6 +378,19 @@ impl IdentityApi for IdentityProvider { self.backend_driver.get_user(db, user_id).await } + /// Find federated user by IDP and Unique ID + #[tracing::instrument(level = "info", skip(self, db))] + async fn find_federated_user<'a>( + &self, + db: &DatabaseConnection, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError> { + self.backend_driver + .find_federated_user(db, idp_id, unique_id) + .await + } + /// Create user #[tracing::instrument(level = "info", skip(self, db))] async fn create_user( @@ -372,7 +399,7 @@ impl IdentityApi for IdentityProvider { user: UserCreate, ) -> Result { let mut mod_user = user; - mod_user.id = Uuid::new_v4().into(); + mod_user.id = Uuid::new_v4().simple().to_string(); if mod_user.enabled.is_none() { mod_user.enabled = Some(true); } @@ -417,7 +444,7 @@ impl IdentityApi for IdentityProvider { group: GroupCreate, ) -> Result { let mut res = group; - res.id = Some(Uuid::new_v4().into()); + res.id = Some(Uuid::new_v4().simple().to_string()); self.backend_driver.create_group(db, res).await } diff --git a/src/identity/types.rs b/src/identity/types.rs index f97dd18d..29fe47e6 100644 --- a/src/identity/types.rs +++ b/src/identity/types.rs @@ -24,11 +24,12 @@ use crate::config::Config; use crate::identity::IdentityProviderError; pub use crate::identity::types::group::{Group, GroupCreate, GroupListParameters}; -pub use crate::identity::types::user::{ - DomainBuilder, DomainBuilderError, UserCreate, UserListParameters, UserOptions, - UserPasswordAuthRequest, UserPasswordAuthRequestBuilder, UserResponse, UserResponseBuilder, - UserResponseBuilderError, -}; +pub use crate::identity::types::user::*; +//pub use crate::identity::types::user::{ +// DomainBuilder, DomainBuilderError, UserCreate, UserListParameters, UserOptions, +// UserPasswordAuthRequest, UserPasswordAuthRequestBuilder, UserResponse, UserResponseBuilder, +// UserResponseBuilderError, +//}; #[async_trait] pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { @@ -56,6 +57,14 @@ pub trait IdentityBackend: DynClone + Send + Sync + std::fmt::Debug { user_id: &'a str, ) -> Result, IdentityProviderError>; + /// Find federated user by IDP and Unique ID + async fn find_federated_user<'a>( + &self, + db: &DatabaseConnection, + idp_id: &'a str, + unique_id: &'a str, + ) -> Result, IdentityProviderError>; + /// Create user async fn create_user( &self, diff --git a/src/identity/types/user.rs b/src/identity/types/user.rs index 2a3b8585..5266e864 100644 --- a/src/identity/types/user.rs +++ b/src/identity/types/user.rs @@ -76,18 +76,20 @@ pub struct UserCreate { } #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -#[builder(setter(strip_option, into))] +#[builder(setter(into))] pub struct UserUpdate { /// The user name. Must be unique within the owning domain. - pub name: Option, + #[builder(default)] + pub name: Option>, /// If the user is enabled, this value is true. If the user is disabled, this value is false. + #[builder(default)] pub enabled: Option, /// The resource description #[builder(default)] - pub description: Option, + pub description: Option>, /// The ID of the default project for the user. #[builder(default)] - pub default_project_id: Option, + pub default_project_id: Option>, /// User password #[builder(default)] pub password: Option, @@ -128,6 +130,9 @@ pub struct Federation { /// Protocols #[builder(default)] pub protocols: Vec, + + #[builder] + pub unique_id: String, } #[derive(Builder, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] diff --git a/src/token/error.rs b/src/token/error.rs index b6e68e55..6a3238db 100644 --- a/src/token/error.rs +++ b/src/token/error.rs @@ -129,4 +129,7 @@ pub enum TokenProviderError { #[from] source: crate::resource::error::ResourceProviderError, }, + + #[error("actor has no roles on scope")] + ActorHasNoRolesOnTarget, } diff --git a/src/token/mod.rs b/src/token/mod.rs index 908b8df3..4ef946a8 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -220,7 +220,7 @@ impl TokenApi for TokenProvider { ) -> Result<(), TokenProviderError> { match token { Token::ProjectScope(data) => { - let token_roles = provider + data.roles = provider .get_assignment_provider() .list_role_assignments( db, @@ -231,18 +231,20 @@ impl TokenApi for TokenProvider { .build() .map_err(AssignmentProviderError::from)?, ) - .await?; - data.roles = token_roles + .await? .into_iter() .map(|x| Role { id: x.role_id.clone(), name: x.role_name.clone().unwrap_or_default(), ..Default::default() }) - .collect::>(); + .collect(); + if data.roles.is_empty() { + return Err(TokenProviderError::ActorHasNoRolesOnTarget); + } } Token::DomainScope(data) => { - let token_roles = provider + data.roles = provider .get_assignment_provider() .list_role_assignments( db, @@ -253,18 +255,20 @@ impl TokenApi for TokenProvider { .build() .map_err(AssignmentProviderError::from)?, ) - .await?; - data.roles = token_roles + .await? .into_iter() .map(|x| Role { id: x.role_id.clone(), name: x.role_name.clone().unwrap_or_default(), ..Default::default() }) - .collect::>(); + .collect(); + if data.roles.is_empty() { + return Err(TokenProviderError::ActorHasNoRolesOnTarget); + } } Token::ApplicationCredential(data) => { - let token_roles = provider + data.roles = provider .get_assignment_provider() .list_role_assignments( db, @@ -275,15 +279,17 @@ impl TokenApi for TokenProvider { .build() .map_err(AssignmentProviderError::from)?, ) - .await?; - data.roles = token_roles + .await? .into_iter() .map(|x| Role { id: x.role_id.clone(), name: x.role_name.clone().unwrap_or_default(), ..Default::default() }) - .collect::>(); + .collect(); + if data.roles.is_empty() { + return Err(TokenProviderError::ActorHasNoRolesOnTarget); + } } _ => {} }